mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-02-05 13:35:19 -05:00
- 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.
200 lines
5.8 KiB
Python
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
|