## Summary - Closes #1 - Replace broken `csrf_exempt` `reset_password` FBV (responses never returned; missing `requests` import) with working DRF `ResetUserPassword` - Deduplicate reset email helper; build set-password links from `FRONTEND_BASE_URL` - Harden `SetUserPassword`: require unusable password, min 8 chars, handle missing slug - Accept reCAPTCHA v2 (success only) and v3 (score ≥ 0.5); avoid email enumeration (200 after valid captcha) - Add unit tests for reset + set-password edge cases ## Test plan - [ ] `manage.py test chat_backend.tests.test_views_users.ResetPasswordTestCase chat_backend.tests.test_views_users.SetPasswordTestCase` - [ ] With SMTP configured: request reset for known email → receive link → set password → sign in - [ ] Unknown email still returns 200 and sends no mail - [ ] Failed captcha returns 400 - [ ] Pair with chat_web_app `feature/password-reset-1` PRReviewed-on: #27
This commit was merged in pull request #27.
This commit is contained in:
@@ -3,6 +3,7 @@ from django.contrib.auth.models import AbstractUser
|
||||
from django.utils import timezone
|
||||
from autoslug import AutoSlugField
|
||||
from chat_backend.storage import DatabaseStorage
|
||||
import uuid
|
||||
|
||||
# Create your models here.
|
||||
|
||||
@@ -74,7 +75,91 @@ class CustomUser(AbstractUser):
|
||||
)
|
||||
|
||||
def get_set_password_url(self):
|
||||
return f"https://chat.aimloperations.com/set_password?slug={self.slug}"
|
||||
from django.conf import settings
|
||||
|
||||
base = settings.FRONTEND_BASE_URL.rstrip("/")
|
||||
return f"{base}/set_password/?slug={self.slug}"
|
||||
|
||||
|
||||
class UserAuthEvent(models.Model):
|
||||
"""Audit trail for password reset / set actions, shown on user admin."""
|
||||
|
||||
class EventType(models.TextChoices):
|
||||
PASSWORD_RESET_REQUESTED = (
|
||||
"password_reset_requested",
|
||||
"Password reset requested",
|
||||
)
|
||||
PASSWORD_SET = ("password_set", "Password set")
|
||||
INVITE_SENT = ("invite_sent", "Invite sent")
|
||||
|
||||
user = models.ForeignKey(
|
||||
CustomUser,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="auth_events",
|
||||
)
|
||||
event_type = models.CharField(max_length=64, choices=EventType.choices)
|
||||
created = models.DateTimeField(default=timezone.now, db_index=True)
|
||||
detail = models.CharField(max_length=512, blank=True, default="")
|
||||
ip_address = models.GenericIPAddressField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-created"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_event_type_display()} @ {self.created.isoformat()}"
|
||||
|
||||
@classmethod
|
||||
def log(cls, user, event_type, *, detail="", ip_address=None):
|
||||
return cls.objects.create(
|
||||
user=user,
|
||||
event_type=event_type,
|
||||
detail=detail or "",
|
||||
ip_address=ip_address,
|
||||
)
|
||||
|
||||
|
||||
class OutboundEmail(models.Model):
|
||||
"""Record of emails queued/sent by the app (visible in admin)."""
|
||||
|
||||
class Kind(models.TextChoices):
|
||||
PASSWORD_RESET = "password_reset", "Password reset"
|
||||
INVITE = "invite", "Invite"
|
||||
FEEDBACK = "feedback", "Feedback"
|
||||
|
||||
class Status(models.TextChoices):
|
||||
QUEUED = "queued", "Queued"
|
||||
SENDING = "sending", "Sending"
|
||||
SENT = "sent", "Sent to SMTP"
|
||||
FAILED = "failed", "Failed"
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
kind = models.CharField(max_length=32, choices=Kind.choices)
|
||||
status = models.CharField(
|
||||
max_length=16, choices=Status.choices, default=Status.QUEUED, db_index=True
|
||||
)
|
||||
to_email = models.EmailField()
|
||||
from_email = models.EmailField()
|
||||
subject = models.CharField(max_length=255)
|
||||
html_template = models.CharField(max_length=255)
|
||||
text_template = models.CharField(max_length=255)
|
||||
context = models.JSONField(default=dict, blank=True)
|
||||
error_message = models.TextField(blank=True, default="")
|
||||
user = models.ForeignKey(
|
||||
CustomUser,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="outbound_emails",
|
||||
)
|
||||
created = models.DateTimeField(default=timezone.now, db_index=True)
|
||||
updated = models.DateTimeField(auto_now=True)
|
||||
sent_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-created"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.subject} → {self.to_email} ({self.status})"
|
||||
|
||||
|
||||
FEEDBACK_CHOICE = (
|
||||
|
||||
Reference in New Issue
Block a user