Skip to content

Commit be812ca

Browse files
Tony QiuTony Qiu
authored andcommitted
hash emails in db
1 parent fdcd293 commit be812ca

File tree

3 files changed

+93
-14
lines changed

3 files changed

+93
-14
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Generated by Django 4.2.7 on 2025-10-13 05:21
2+
3+
import hashlib
4+
from django.db import migrations, models
5+
6+
7+
def hash_existing_emails(apps, schema_editor):
8+
"""Hash existing email addresses in the database"""
9+
NewsletterSubscriber = apps.get_model('newsletter', 'NewsletterSubscriber')
10+
11+
for subscriber in NewsletterSubscriber.objects.all():
12+
if hasattr(subscriber, 'email') and subscriber.email:
13+
# Hash the existing email
14+
email_hash = hashlib.sha256(subscriber.email.lower().strip().encode('utf-8')).hexdigest()
15+
subscriber.email_hash = email_hash
16+
subscriber.save()
17+
18+
19+
def reverse_hash_emails(apps, schema_editor):
20+
"""This is not reversible - we can't unhash emails"""
21+
pass
22+
23+
24+
class Migration(migrations.Migration):
25+
26+
dependencies = [
27+
("newsletter", "0001_initial"),
28+
]
29+
30+
operations = [
31+
# Add the new email_hash field
32+
migrations.AddField(
33+
model_name='newslettersubscriber',
34+
name='email_hash',
35+
field=models.CharField(default='', max_length=128, help_text='SHA-256 hash of the email'),
36+
preserve_default=False,
37+
),
38+
39+
# Hash existing emails
40+
migrations.RunPython(hash_existing_emails, reverse_hash_emails),
41+
42+
# Make email_hash unique
43+
migrations.AlterField(
44+
model_name='newslettersubscriber',
45+
name='email_hash',
46+
field=models.CharField(max_length=128, unique=True, help_text='SHA-256 hash of the email'),
47+
),
48+
49+
# Remove the old email field
50+
migrations.RemoveField(
51+
model_name='newslettersubscriber',
52+
name='email',
53+
),
54+
]

backend/apps/newsletter/models.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import hashlib
12
import uuid
23

4+
from django.contrib.auth.hashers import make_password, check_password
35
from django.db import models
46

57

68
class NewsletterSubscriber(models.Model):
7-
email = models.EmailField(unique=True)
9+
email_hash = models.CharField(max_length=128, unique=True, help_text="SHA-256 hash of the email")
810
subscribed_at = models.DateTimeField(auto_now_add=True)
911
is_active = models.BooleanField(default=True)
1012
unsubscribe_token = models.UUIDField(
@@ -23,4 +25,28 @@ class Meta:
2325
ordering = ["-subscribed_at"]
2426

2527
def __str__(self):
26-
return self.email
28+
return f"NewsletterSubscriber({self.email_hash[:8]}...)"
29+
30+
@staticmethod
31+
def hash_email(email):
32+
"""Create a SHA-256 hash of the email address"""
33+
return hashlib.sha256(email.lower().strip().encode('utf-8')).hexdigest()
34+
35+
@classmethod
36+
def get_by_email(cls, email):
37+
"""Get subscriber by email address (using hash lookup)"""
38+
email_hash = cls.hash_email(email)
39+
try:
40+
return cls.objects.get(email_hash=email_hash)
41+
except cls.DoesNotExist:
42+
return None
43+
44+
@classmethod
45+
def create_subscriber(cls, email):
46+
"""Create a new subscriber with hashed email"""
47+
email_hash = cls.hash_email(email)
48+
return cls.objects.create(email_hash=email_hash, is_active=True)
49+
50+
def get_email_display(self):
51+
"""Return a masked version of the email for display purposes"""
52+
return f"***@{self.email_hash[:4]}..."

backend/apps/newsletter/views.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,43 +31,42 @@ def newsletter_subscribe(request):
3131
)
3232

3333
try:
34-
# Check if already subscribed
35-
subscriber, created = NewsletterSubscriber.objects.get_or_create(
36-
email=email.lower().strip(),
37-
defaults={"is_active": True},
38-
)
34+
subscriber = NewsletterSubscriber.get_by_email(email)
35+
created = False
3936

40-
if not created:
37+
if subscriber:
4138
if subscriber.is_active:
4239
return Response(
4340
{"message": "You're already subscribed to our newsletter!"},
4441
status=status.HTTP_200_OK,
4542
)
4643
else:
47-
# Reactivate subscription
4844
subscriber.is_active = True
4945
subscriber.save()
46+
else:
47+
subscriber = NewsletterSubscriber.create_subscriber(email)
48+
created = True
5049

5150
# Send welcome email with mock events
5251
from services.email_service import email_service
5352

5453
email_sent = email_service.send_welcome_email(
55-
subscriber.email, str(subscriber.unsubscribe_token)
54+
email, str(subscriber.unsubscribe_token)
5655
)
5756

5857
if email_sent:
5958
return Response(
6059
{
6160
"message": "Successfully subscribed! Check your email for upcoming events.",
62-
"email": subscriber.email,
61+
"email": email,
6362
},
6463
status=status.HTTP_201_CREATED if created else status.HTTP_200_OK,
6564
)
6665
else:
6766
return Response(
6867
{
6968
"message": "Subscribed successfully, but email could not be sent. Please check back later.",
70-
"email": subscriber.email,
69+
"email": email,
7170
},
7271
status=status.HTTP_201_CREATED if created else status.HTTP_200_OK,
7372
)
@@ -91,7 +90,7 @@ def newsletter_unsubscribe(request, token):
9190
return Response(
9291
{
9392
"already_unsubscribed": not subscriber.is_active,
94-
"email": subscriber.email,
93+
"email": subscriber.get_email_display(),
9594
"message": "Already unsubscribed"
9695
if not subscriber.is_active
9796
else "Ready to unsubscribe",
@@ -119,7 +118,7 @@ def newsletter_unsubscribe(request, token):
119118
return Response(
120119
{
121120
"message": "Successfully unsubscribed from the newsletter.",
122-
"email": subscriber.email,
121+
"email": subscriber.get_email_display(),
123122
"unsubscribed_at": subscriber.unsubscribed_at,
124123
}
125124
)

0 commit comments

Comments
 (0)