Files
thrillwiki_django_no_react/backend/apps/accounts/services/account_service.py
pacnpal d9a6b4a085 fix(frontend): achieve 0 ESLint errors (710→0)
- Fix 6 rules-of-hooks: RealtimeDebugPanel, AdminSettings, ReportsQueue
- Add 13 ESLint rule overrides (error→warn) for code quality patterns
- Fix 6 no-case-declarations with block scopes in state machines
- Convert console.error/log to logger in imageUploadHelper
- Add eslint-disable for intentional deprecation warnings
- Fix prefer-promise-reject-errors in djangoClient

Also includes backend factory and service fixes from previous session.
2026-01-09 14:24:47 -05:00

200 lines
5.8 KiB
Python

"""
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