Skip to content

Commit cd050ad

Browse files
Tony QiuTony Qiu
authored andcommitted
add email encryption and decryption, now newsletter will actually send emails
1 parent b2f0d30 commit cd050ad

File tree

9 files changed

+165
-7
lines changed

9 files changed

+165
-7
lines changed

backend/.env.example

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Django Configuration
2+
SECRET_KEY=your-secret-key-here
3+
PRODUCTION=0
4+
DEBUG=True
5+
6+
# Database Configuration
7+
# Set USE_SQLITE=1 for local development with SQLite
8+
USE_SQLITE=1
9+
10+
# PostgreSQL Configuration (for production)
11+
POSTGRES_DB=postgres
12+
POSTGRES_USER=postgres
13+
POSTGRES_PASSWORD=your-supabase-password
14+
POSTGRES_HOST=your-project.supabase.co
15+
POSTGRES_PORT=6543
16+
SUPABASE_DB_URL=postgresql://postgres:password@your-project.supabase.co:6543/postgres
17+
18+
# Email Service (Resend)
19+
RESEND_API_KEY=your-resend-api-key
20+
RESEND_FROM_EMAIL=onboarding@resend.dev
21+
22+
# OpenAI API
23+
OPENAI_API_KEY=your-openai-api-key
24+
25+
# AWS S3 Configuration
26+
AWS_S3_BUCKET_NAME=your-s3-bucket-name
27+
AWS_DEFAULT_REGION=us-east-2
28+
29+
# Email Encryption
30+
EMAIL_ENCRYPTION_KEY=32-character-base64-encryption-key
31+
EMAIL_HASH_KEY=32-character-base64-hash-key
32+
33+
# Instagram Scraping
34+
USERNAME=your-instagram-username
35+
PASSWORD=your-instagram-password
36+
CSRFTOKEN=your-csrf-token
37+
SESSIONID=your-session-id
38+
DS_USER_ID=your-ds-user-id
39+
MID=your-mid
40+
IG_DID=your-ig-did
41+
DOC_ID=
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Generated manually for encrypted email support
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("newsletter", "0002_auto_20251013_0521"),
9+
]
10+
11+
operations = [
12+
# Add the encrypted email field as nullable first
13+
migrations.AddField(
14+
model_name="newslettersubscriber",
15+
name="email_encrypted",
16+
field=models.TextField(
17+
blank=True, null=True, help_text="Encrypted email address"
18+
),
19+
),
20+
# Since we have no existing data, we can make it non-nullable
21+
migrations.AlterField(
22+
model_name="newslettersubscriber",
23+
name="email_encrypted",
24+
field=models.TextField(help_text="Encrypted email address"),
25+
),
26+
]

backend/apps/newsletter/models.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@
33

44
from django.db import models
55

6+
from utils.encryption_utils import email_encryption
7+
68

79
class NewsletterSubscriber(models.Model):
10+
email_encrypted = models.TextField(help_text="Encrypted email address")
811
email_hash = models.CharField(
9-
max_length=128, unique=True, help_text="SHA-256 hash of the email"
12+
max_length=128,
13+
unique=True,
14+
help_text="SHA-256 hash of the email for uniqueness checks",
1015
)
1116
subscribed_at = models.DateTimeField(auto_now_add=True)
1217
is_active = models.BooleanField(default=True)
@@ -30,7 +35,7 @@ def __str__(self):
3035

3136
@staticmethod
3237
def hash_email(email):
33-
"""Create a SHA-256 hash of the email address"""
38+
"""Create a SHA-256 hash of the email address for uniqueness checks"""
3439
return hashlib.sha256(email.lower().strip().encode("utf-8")).hexdigest()
3540

3641
@classmethod
@@ -44,10 +49,25 @@ def get_by_email(cls, email):
4449

4550
@classmethod
4651
def create_subscriber(cls, email):
47-
"""Create a new subscriber with hashed email"""
52+
"""Create a new subscriber with encrypted email"""
4853
email_hash = cls.hash_email(email)
49-
return cls.objects.create(email_hash=email_hash, is_active=True)
54+
email_encrypted = email_encryption.encrypt_email(email)
55+
return cls.objects.create(
56+
email_encrypted=email_encrypted, email_hash=email_hash, is_active=True
57+
)
58+
59+
def get_email(self):
60+
"""Decrypt and return the actual email address"""
61+
return email_encryption.decrypt_email(self.email_encrypted)
5062

5163
def get_email_display(self):
5264
"""Return a masked version of the email for display purposes"""
53-
return f"***@{self.email_hash[:4]}..."
65+
email = self.get_email()
66+
if email:
67+
# Show first 2 chars and domain
68+
local, domain = email.split("@", 1)
69+
masked_local = (
70+
local[:2] + "*" * (len(local) - 2) if len(local) > 2 else local
71+
)
72+
return f"{masked_local}@{domain}"
73+
return "***@***"

backend/apps/newsletter/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def newsletter_subscribe(request):
5151
from services.email_service import email_service
5252

5353
email_sent = email_service.send_welcome_email(
54-
email, str(subscriber.unsubscribe_token)
54+
subscriber.get_email(), str(subscriber.unsubscribe_token)
5555
)
5656

5757
if email_sent:

backend/config/db.sqlite3

12 KB
Binary file not shown.

backend/requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ openai
2020
# Email service
2121
resend==0.8.0
2222

23+
# Encryption
24+
cryptography==41.0.7
25+
2326
# AWS S3 for image storage
2427
boto3==1.34.0
2528
Pillow>=10.0.0

backend/scripts/send_newsletter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def send_to_subscriber(subscriber):
4949
return False, str(e)
5050

5151
for subscriber in active_subscribers:
52-
email_sent, error = send_to_subscriber(subscriber)
52+
email_sent, error = send_to_subscriber(subscriber.get_email())
5353

5454
if email_sent:
5555
success_count += 1

backend/utils/encryption_utils.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""
2+
Encryption utilities for sensitive data like email addresses.
3+
Uses Fernet (symmetric encryption) for encrypting/decrypting emails.
4+
"""
5+
6+
import base64
7+
import hashlib
8+
import hmac
9+
import os
10+
11+
from cryptography.fernet import Fernet
12+
from dotenv import load_dotenv
13+
14+
load_dotenv()
15+
16+
EMAIL_ENCRYPTION_KEY = os.getenv("EMAIL_ENCRYPTION_KEY")
17+
EMAIL_HASH_KEY = os.getenv("EMAIL_HASH_KEY").encode("utf-8")
18+
19+
20+
class EmailEncryption:
21+
"""Handles encryption and decryption of email addresses."""
22+
23+
def __init__(self):
24+
self.cipher = Fernet(EMAIL_ENCRYPTION_KEY)
25+
26+
def encrypt_email(self, email):
27+
"""Encrypt an email address."""
28+
if not email:
29+
return None
30+
31+
# Normalize email (lowercase, strip whitespace)
32+
normalized_email = email.lower().strip()
33+
34+
# Encrypt the email
35+
encrypted_bytes = self.cipher.encrypt(normalized_email.encode("utf-8"))
36+
37+
# Return base64 encoded string for database storage
38+
return base64.b64encode(encrypted_bytes).decode("utf-8")
39+
40+
def decrypt_email(self, encrypted_email):
41+
"""Decrypt an email address."""
42+
if not encrypted_email:
43+
return None
44+
45+
try:
46+
# Decode from base64
47+
encrypted_bytes = base64.b64decode(encrypted_email.encode("utf-8"))
48+
49+
# Decrypt
50+
decrypted_bytes = self.cipher.decrypt(encrypted_bytes)
51+
52+
# Return as string
53+
return decrypted_bytes.decode("utf-8")
54+
except Exception as e:
55+
print(f"Error decrypting email: {e}")
56+
return None
57+
58+
def create_email_hash(self, email):
59+
normalized_email = email.lower().strip()
60+
return hmac.new(
61+
EMAIL_HASH_KEY, normalized_email.encode("utf-8"), hashlib.sha256
62+
).hexdigest()
63+
64+
65+
# Global instance
66+
email_encryption = EmailEncryption()

frontend/.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# API Configuration
2+
VITE_API_BASE_URL=http://localhost:8000

0 commit comments

Comments
 (0)