feat(backend): migrate auth and notifications off email
Some checks failed
Backend CI/CD / test (push) Has been cancelled
Backend CI/CD / deploy (push) Has been cancelled

This commit is contained in:
2026-05-21 10:28:04 +03:30
parent b4903f7cb1
commit b7b21a6cc6
35 changed files with 2784 additions and 1390 deletions

View File

@@ -11,6 +11,7 @@ from core.media import (
safe_process_public_image,
)
from core.models import BaseModel
from apps.users.email_identity import normalize_email_identity, normalize_mobile_number
class University(BaseModel):
@@ -38,7 +39,8 @@ class Major(BaseModel):
class User(AbstractUser, BaseModel):
email = models.EmailField(unique=True)
email = models.EmailField(unique=True, null=True, blank=True, default=None)
mobile = models.CharField(max_length=11, unique=True, null=True, blank=True, default=None)
bio = models.TextField(null=True, blank=True)
profile_picture = models.ImageField(upload_to='profile_pictures/', null=True, blank=True)
@@ -59,14 +61,15 @@ class User(AbstractUser, BaseModel):
related_name='users',
)
is_email_verified = models.BooleanField(default=False)
is_mobile_verified = models.BooleanField(default=False)
email_verification_token = models.UUIDField(default=uuid.uuid4, unique=True)
email_verification_sent_at = models.DateTimeField(null=True, blank=True)
password_reset_token = models.UUIDField(null=True, blank=True, unique=True)
password_reset_token_expires_at = models.DateTimeField(null=True, blank=True)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']
USERNAME_FIELD = 'username'
REQUIRED_FIELDS = []
class Meta:
db_table = 'users'
@@ -74,7 +77,8 @@ class User(AbstractUser, BaseModel):
verbose_name_plural = 'Users'
def __str__(self):
return f"{self.get_full_name()} ({self.email})"
identity = self.mobile or self.email or self.username
return f"{self.get_full_name() or self.username} ({identity})"
def get_full_name(self):
return f"{self.first_name} {self.last_name}".strip()
@@ -89,6 +93,17 @@ class User(AbstractUser, BaseModel):
return self.university.name
return None
@property
def requires_mobile_verification(self):
return not self.is_mobile_verified
@property
def has_google_link(self):
return self.social_accounts.filter(
provider=UserSocialAccount.ProviderType.GOOGLE,
is_active=True,
).exists()
def regenerate_verification_token(self):
self.email_verification_token = uuid.uuid4()
self.save(update_fields=['email_verification_token'])
@@ -102,12 +117,8 @@ class User(AbstractUser, BaseModel):
def save(self, *args, **kwargs):
previous_image_name = get_image_previous_name(self, "profile_picture")
current_image_name = self.profile_picture.name if self.profile_picture else None
send_verified_success = False
if self.pk is not None:
prev = type(self).objects.filter(pk=self.pk).values_list('is_email_verified', flat=True).first()
if prev is not None and prev is False and self.is_email_verified is True:
send_verified_success = True
self.email = normalize_email_identity(self.email)
self.mobile = normalize_mobile_number(self.mobile)
super().save(*args, **kwargs)
@@ -122,9 +133,42 @@ class User(AbstractUser, BaseModel):
if previous_image_name != current_image_name and self.profile_picture:
safe_process_public_image(self.profile_picture, "profile_picture")
if send_verified_success:
try:
from apps.users.tasks import send_email_verified_success
send_email_verified_success.delay(self.id)
except Exception:
pass
class UserSocialAccount(BaseModel):
class ProviderType(models.TextChoices):
GOOGLE = "google", "google"
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name="social_accounts",
)
provider = models.CharField(max_length=32, choices=ProviderType.choices)
provider_user_id = models.CharField(max_length=255)
email = models.EmailField(blank=True, null=True, default=None)
email_verified = models.BooleanField(default=False)
avatar_url = models.URLField(blank=True, default="")
is_active = models.BooleanField(default=True)
class Meta:
verbose_name = "User Social Account"
verbose_name_plural = "User Social Accounts"
db_table = "user_social_accounts"
ordering = ["-updated_at", "-created_at"]
constraints = [
models.UniqueConstraint(
fields=("provider", "provider_user_id"),
name="user_social_account_provider_uid_uniq",
)
]
indexes = [
models.Index(fields=["provider", "provider_user_id"], name="user_social_provider_uid_idx"),
models.Index(fields=["provider", "email"], name="user_social_provider_email_idx"),
]
def __str__(self):
return f"{self.provider}:{self.provider_user_id}"
def save(self, *args, **kwargs):
self.email = normalize_email_identity(self.email)
super().save(*args, **kwargs)