""" Account management service for ThrillWiki. Provides password validation, password changes, and email change functionality. """ import re import secrets from typing import TYPE_CHECKING from django.core.mail import send_mail from django.template.loader import render_to_string from django.utils import timezone if TYPE_CHECKING: from django.http import HttpRequest from apps.accounts.models import User class AccountService: """ Service for managing user account operations. Handles password validation, password changes, and email changes with proper verification flows. """ # Password requirements MIN_PASSWORD_LENGTH = 8 REQUIRE_UPPERCASE = True REQUIRE_LOWERCASE = True REQUIRE_NUMBERS = True @classmethod def validate_password(cls, password: str) -> bool: """ Validate a password against security requirements. Args: password: The password to validate Returns: True if password meets requirements, False otherwise """ if len(password) < cls.MIN_PASSWORD_LENGTH: return False if cls.REQUIRE_UPPERCASE and not re.search(r"[A-Z]", password): return False if cls.REQUIRE_LOWERCASE and not re.search(r"[a-z]", password): return False if cls.REQUIRE_NUMBERS and not re.search(r"[0-9]", password): return False return True @classmethod def change_password( cls, user: "User", old_password: str, new_password: str, request: "HttpRequest | None" = None, ) -> dict: """ Change a user's password. Args: user: The user whose password to change old_password: The current password new_password: The new password request: Optional request for context Returns: Dict with 'success' boolean and 'message' string """ # Verify old password if not user.check_password(old_password): return { "success": False, "message": "Current password is incorrect.", } # Validate new password if not cls.validate_password(new_password): return { "success": False, "message": f"New password must be at least {cls.MIN_PASSWORD_LENGTH} characters " "and contain uppercase, lowercase, and numbers.", } # Change the password user.set_password(new_password) user.save(update_fields=["password"]) # Send confirmation email cls._send_password_change_confirmation(user, request) return { "success": True, "message": "Password changed successfully.", } @classmethod def _send_password_change_confirmation( cls, user: "User", request: "HttpRequest | None" = None, ) -> None: """Send a confirmation email after password change.""" try: send_mail( subject="Password Changed - ThrillWiki", message=f"Hi {user.username},\n\nYour password has been changed successfully.\n\n" "If you did not make this change, please contact support immediately.", from_email=None, # Uses DEFAULT_FROM_EMAIL recipient_list=[user.email], fail_silently=True, ) except Exception: pass # Don't fail the password change if email fails @classmethod def initiate_email_change( cls, user: "User", new_email: str, request: "HttpRequest | None" = None, ) -> dict: """ Initiate an email change request. Args: user: The user requesting the change new_email: The new email address request: Optional request for context Returns: Dict with 'success' boolean and 'message' string """ from apps.accounts.models import User # Validate email if not new_email or not new_email.strip(): return { "success": False, "message": "Email address is required.", } new_email = new_email.strip().lower() # Check if email already in use if User.objects.filter(email=new_email).exclude(pk=user.pk).exists(): return { "success": False, "message": "This email is already in use by another account.", } # Store pending email user.pending_email = new_email user.save(update_fields=["pending_email"]) # Send verification email cls._send_email_verification(user, new_email, request) return { "success": True, "message": "Verification email sent. Please check your inbox.", } @classmethod def _send_email_verification( cls, user: "User", new_email: str, request: "HttpRequest | None" = None, ) -> None: """Send verification email for email change.""" verification_code = secrets.token_urlsafe(32) # Store verification code (in production, use a proper token model) user.email_verification_code = verification_code user.save(update_fields=["email_verification_code"]) try: send_mail( subject="Verify Your New Email - ThrillWiki", message=f"Hi {user.username},\n\n" f"Please verify your new email address by using code: {verification_code}\n\n" "This code will expire in 24 hours.", from_email=None, recipient_list=[new_email], fail_silently=True, ) except Exception: pass