mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 08:31:15 -05:00
Refactor code structure and remove redundant changes
This commit is contained in:
592
django-backend/apps/users/services.py
Normal file
592
django-backend/apps/users/services.py
Normal file
@@ -0,0 +1,592 @@
|
||||
"""
|
||||
User authentication and management services.
|
||||
|
||||
Provides business logic for:
|
||||
- User registration and authentication
|
||||
- OAuth integration
|
||||
- MFA/2FA management
|
||||
- Permission and role management
|
||||
"""
|
||||
|
||||
from typing import Optional, Dict, Any
|
||||
from django.contrib.auth import authenticate
|
||||
from django.contrib.auth.password_validation import validate_password
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
from allauth.socialaccount.models import SocialAccount
|
||||
import logging
|
||||
|
||||
from .models import User, UserRole, UserProfile
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AuthenticationService:
|
||||
"""Service for handling user authentication operations"""
|
||||
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def register_user(
|
||||
email: str,
|
||||
password: str,
|
||||
username: Optional[str] = None,
|
||||
first_name: str = '',
|
||||
last_name: str = ''
|
||||
) -> User:
|
||||
"""
|
||||
Register a new user with email and password.
|
||||
|
||||
Args:
|
||||
email: User's email address
|
||||
password: User's password (will be validated and hashed)
|
||||
username: Optional username (defaults to email prefix)
|
||||
first_name: User's first name
|
||||
last_name: User's last name
|
||||
|
||||
Returns:
|
||||
Created User instance
|
||||
|
||||
Raises:
|
||||
ValidationError: If email exists or password is invalid
|
||||
"""
|
||||
# Normalize email
|
||||
email = email.lower().strip()
|
||||
|
||||
# Check if user exists
|
||||
if User.objects.filter(email=email).exists():
|
||||
raise ValidationError({'email': 'A user with this email already exists.'})
|
||||
|
||||
# Set username if not provided
|
||||
if not username:
|
||||
username = email.split('@')[0]
|
||||
# Make unique if needed
|
||||
base_username = username
|
||||
counter = 1
|
||||
while User.objects.filter(username=username).exists():
|
||||
username = f"{base_username}{counter}"
|
||||
counter += 1
|
||||
|
||||
# Validate password
|
||||
try:
|
||||
validate_password(password)
|
||||
except ValidationError as e:
|
||||
raise ValidationError({'password': e.messages})
|
||||
|
||||
# Create user
|
||||
user = User.objects.create_user(
|
||||
email=email,
|
||||
username=username,
|
||||
password=password,
|
||||
first_name=first_name,
|
||||
last_name=last_name
|
||||
)
|
||||
|
||||
# Create role (default: user)
|
||||
UserRole.objects.create(user=user, role='user')
|
||||
|
||||
# Create profile
|
||||
UserProfile.objects.create(user=user)
|
||||
|
||||
logger.info(f"New user registered: {user.email}")
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
def authenticate_user(email: str, password: str) -> Optional[User]:
|
||||
"""
|
||||
Authenticate user with email and password.
|
||||
|
||||
Args:
|
||||
email: User's email address
|
||||
password: User's password
|
||||
|
||||
Returns:
|
||||
User instance if authentication successful, None otherwise
|
||||
"""
|
||||
email = email.lower().strip()
|
||||
user = authenticate(username=email, password=password)
|
||||
|
||||
if user and user.banned:
|
||||
logger.warning(f"Banned user attempted login: {email}")
|
||||
raise ValidationError("This account has been banned.")
|
||||
|
||||
if user:
|
||||
user.last_login = timezone.now()
|
||||
user.save(update_fields=['last_login'])
|
||||
logger.info(f"User authenticated: {email}")
|
||||
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def create_oauth_user(
|
||||
email: str,
|
||||
provider: str,
|
||||
oauth_sub: str,
|
||||
username: Optional[str] = None,
|
||||
first_name: str = '',
|
||||
last_name: str = '',
|
||||
avatar_url: str = ''
|
||||
) -> User:
|
||||
"""
|
||||
Create or get user from OAuth provider.
|
||||
|
||||
Args:
|
||||
email: User's email from OAuth provider
|
||||
provider: OAuth provider name (google, discord)
|
||||
oauth_sub: OAuth subject identifier
|
||||
username: Optional username
|
||||
first_name: User's first name
|
||||
last_name: User's last name
|
||||
avatar_url: URL to user's avatar
|
||||
|
||||
Returns:
|
||||
User instance
|
||||
"""
|
||||
email = email.lower().strip()
|
||||
|
||||
# Check if user exists with this email
|
||||
try:
|
||||
user = User.objects.get(email=email)
|
||||
# Update OAuth info if not set
|
||||
if not user.oauth_provider:
|
||||
user.oauth_provider = provider
|
||||
user.oauth_sub = oauth_sub
|
||||
user.save(update_fields=['oauth_provider', 'oauth_sub'])
|
||||
return user
|
||||
except User.DoesNotExist:
|
||||
pass
|
||||
|
||||
# Create new user
|
||||
if not username:
|
||||
username = email.split('@')[0]
|
||||
base_username = username
|
||||
counter = 1
|
||||
while User.objects.filter(username=username).exists():
|
||||
username = f"{base_username}{counter}"
|
||||
counter += 1
|
||||
|
||||
user = User.objects.create(
|
||||
email=email,
|
||||
username=username,
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
avatar_url=avatar_url,
|
||||
oauth_provider=provider,
|
||||
oauth_sub=oauth_sub
|
||||
)
|
||||
|
||||
# No password needed for OAuth users
|
||||
user.set_unusable_password()
|
||||
user.save()
|
||||
|
||||
# Create role and profile
|
||||
UserRole.objects.create(user=user, role='user')
|
||||
UserProfile.objects.create(user=user)
|
||||
|
||||
logger.info(f"OAuth user created: {email} via {provider}")
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
def change_password(user: User, old_password: str, new_password: str) -> bool:
|
||||
"""
|
||||
Change user's password.
|
||||
|
||||
Args:
|
||||
user: User instance
|
||||
old_password: Current password
|
||||
new_password: New password
|
||||
|
||||
Returns:
|
||||
True if successful
|
||||
|
||||
Raises:
|
||||
ValidationError: If old password is incorrect or new password is invalid
|
||||
"""
|
||||
# Check old password
|
||||
if not user.check_password(old_password):
|
||||
raise ValidationError({'old_password': 'Incorrect password.'})
|
||||
|
||||
# Validate new password
|
||||
try:
|
||||
validate_password(new_password, user=user)
|
||||
except ValidationError as e:
|
||||
raise ValidationError({'new_password': e.messages})
|
||||
|
||||
# Set new password
|
||||
user.set_password(new_password)
|
||||
user.save()
|
||||
|
||||
logger.info(f"Password changed for user: {user.email}")
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def reset_password(user: User, new_password: str) -> bool:
|
||||
"""
|
||||
Reset user's password (admin/forgot password flow).
|
||||
|
||||
Args:
|
||||
user: User instance
|
||||
new_password: New password
|
||||
|
||||
Returns:
|
||||
True if successful
|
||||
|
||||
Raises:
|
||||
ValidationError: If new password is invalid
|
||||
"""
|
||||
# Validate new password
|
||||
try:
|
||||
validate_password(new_password, user=user)
|
||||
except ValidationError as e:
|
||||
raise ValidationError({'password': e.messages})
|
||||
|
||||
# Set new password
|
||||
user.set_password(new_password)
|
||||
user.save()
|
||||
|
||||
logger.info(f"Password reset for user: {user.email}")
|
||||
return True
|
||||
|
||||
|
||||
class MFAService:
|
||||
"""Service for handling multi-factor authentication"""
|
||||
|
||||
@staticmethod
|
||||
def enable_totp(user: User, device_name: str = 'default') -> TOTPDevice:
|
||||
"""
|
||||
Enable TOTP-based MFA for user.
|
||||
|
||||
Args:
|
||||
user: User instance
|
||||
device_name: Name for the TOTP device
|
||||
|
||||
Returns:
|
||||
TOTPDevice instance with QR code data
|
||||
"""
|
||||
# Check if device already exists
|
||||
device = TOTPDevice.objects.filter(
|
||||
user=user,
|
||||
name=device_name
|
||||
).first()
|
||||
|
||||
if not device:
|
||||
device = TOTPDevice.objects.create(
|
||||
user=user,
|
||||
name=device_name,
|
||||
confirmed=False
|
||||
)
|
||||
|
||||
return device
|
||||
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def confirm_totp(user: User, token: str, device_name: str = 'default') -> bool:
|
||||
"""
|
||||
Confirm TOTP device with verification token.
|
||||
|
||||
Args:
|
||||
user: User instance
|
||||
token: 6-digit TOTP token
|
||||
device_name: Name of the TOTP device
|
||||
|
||||
Returns:
|
||||
True if successful
|
||||
|
||||
Raises:
|
||||
ValidationError: If token is invalid
|
||||
"""
|
||||
device = TOTPDevice.objects.filter(
|
||||
user=user,
|
||||
name=device_name
|
||||
).first()
|
||||
|
||||
if not device:
|
||||
raise ValidationError("TOTP device not found.")
|
||||
|
||||
# Verify token
|
||||
if not device.verify_token(token):
|
||||
raise ValidationError("Invalid verification code.")
|
||||
|
||||
# Confirm device
|
||||
device.confirmed = True
|
||||
device.save()
|
||||
|
||||
# Enable MFA on user
|
||||
user.mfa_enabled = True
|
||||
user.save(update_fields=['mfa_enabled'])
|
||||
|
||||
logger.info(f"MFA enabled for user: {user.email}")
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def verify_totp(user: User, token: str) -> bool:
|
||||
"""
|
||||
Verify TOTP token for authentication.
|
||||
|
||||
Args:
|
||||
user: User instance
|
||||
token: 6-digit TOTP token
|
||||
|
||||
Returns:
|
||||
True if valid
|
||||
"""
|
||||
device = TOTPDevice.objects.filter(
|
||||
user=user,
|
||||
confirmed=True
|
||||
).first()
|
||||
|
||||
if not device:
|
||||
return False
|
||||
|
||||
return device.verify_token(token)
|
||||
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def disable_totp(user: User) -> bool:
|
||||
"""
|
||||
Disable TOTP-based MFA for user.
|
||||
|
||||
Args:
|
||||
user: User instance
|
||||
|
||||
Returns:
|
||||
True if successful
|
||||
"""
|
||||
# Delete all TOTP devices
|
||||
TOTPDevice.objects.filter(user=user).delete()
|
||||
|
||||
# Disable MFA on user
|
||||
user.mfa_enabled = False
|
||||
user.save(update_fields=['mfa_enabled'])
|
||||
|
||||
logger.info(f"MFA disabled for user: {user.email}")
|
||||
return True
|
||||
|
||||
|
||||
class RoleService:
|
||||
"""Service for managing user roles and permissions"""
|
||||
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def assign_role(
|
||||
user: User,
|
||||
role: str,
|
||||
granted_by: Optional[User] = None
|
||||
) -> UserRole:
|
||||
"""
|
||||
Assign role to user.
|
||||
|
||||
Args:
|
||||
user: User to assign role to
|
||||
role: Role name (user, moderator, admin)
|
||||
granted_by: User granting the role
|
||||
|
||||
Returns:
|
||||
UserRole instance
|
||||
|
||||
Raises:
|
||||
ValidationError: If role is invalid
|
||||
"""
|
||||
valid_roles = ['user', 'moderator', 'admin']
|
||||
if role not in valid_roles:
|
||||
raise ValidationError(f"Invalid role. Must be one of: {', '.join(valid_roles)}")
|
||||
|
||||
# Get or create role
|
||||
user_role, created = UserRole.objects.get_or_create(
|
||||
user=user,
|
||||
defaults={'role': role, 'granted_by': granted_by}
|
||||
)
|
||||
|
||||
if not created and user_role.role != role:
|
||||
user_role.role = role
|
||||
user_role.granted_by = granted_by
|
||||
user_role.granted_at = timezone.now()
|
||||
user_role.save()
|
||||
|
||||
logger.info(f"Role '{role}' assigned to user: {user.email}")
|
||||
return user_role
|
||||
|
||||
@staticmethod
|
||||
def has_role(user: User, role: str) -> bool:
|
||||
"""
|
||||
Check if user has specific role.
|
||||
|
||||
Args:
|
||||
user: User instance
|
||||
role: Role name to check
|
||||
|
||||
Returns:
|
||||
True if user has the role
|
||||
"""
|
||||
try:
|
||||
user_role = user.role
|
||||
if role == 'moderator':
|
||||
return user_role.is_moderator
|
||||
elif role == 'admin':
|
||||
return user_role.is_admin
|
||||
return user_role.role == role
|
||||
except UserRole.DoesNotExist:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_user_permissions(user: User) -> Dict[str, bool]:
|
||||
"""
|
||||
Get user's permission summary.
|
||||
|
||||
Args:
|
||||
user: User instance
|
||||
|
||||
Returns:
|
||||
Dictionary of permissions
|
||||
"""
|
||||
try:
|
||||
user_role = user.role
|
||||
is_moderator = user_role.is_moderator
|
||||
is_admin = user_role.is_admin
|
||||
except UserRole.DoesNotExist:
|
||||
is_moderator = False
|
||||
is_admin = False
|
||||
|
||||
return {
|
||||
'can_submit': not user.banned,
|
||||
'can_moderate': is_moderator and not user.banned,
|
||||
'can_admin': is_admin and not user.banned,
|
||||
'can_edit_own': not user.banned,
|
||||
'can_delete_own': not user.banned,
|
||||
}
|
||||
|
||||
|
||||
class UserManagementService:
|
||||
"""Service for user profile and account management"""
|
||||
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def update_profile(
|
||||
user: User,
|
||||
**kwargs
|
||||
) -> User:
|
||||
"""
|
||||
Update user profile information.
|
||||
|
||||
Args:
|
||||
user: User instance
|
||||
**kwargs: Fields to update
|
||||
|
||||
Returns:
|
||||
Updated User instance
|
||||
"""
|
||||
allowed_fields = [
|
||||
'first_name', 'last_name', 'username',
|
||||
'avatar_url', 'bio'
|
||||
]
|
||||
|
||||
updated_fields = []
|
||||
for field, value in kwargs.items():
|
||||
if field in allowed_fields and hasattr(user, field):
|
||||
setattr(user, field, value)
|
||||
updated_fields.append(field)
|
||||
|
||||
if updated_fields:
|
||||
user.save(update_fields=updated_fields)
|
||||
logger.info(f"Profile updated for user: {user.email}")
|
||||
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def update_preferences(
|
||||
user: User,
|
||||
**kwargs
|
||||
) -> UserProfile:
|
||||
"""
|
||||
Update user preferences.
|
||||
|
||||
Args:
|
||||
user: User instance
|
||||
**kwargs: Preference fields to update
|
||||
|
||||
Returns:
|
||||
Updated UserProfile instance
|
||||
"""
|
||||
profile = user.profile
|
||||
|
||||
allowed_fields = [
|
||||
'email_notifications',
|
||||
'email_on_submission_approved',
|
||||
'email_on_submission_rejected',
|
||||
'profile_public',
|
||||
'show_email'
|
||||
]
|
||||
|
||||
updated_fields = []
|
||||
for field, value in kwargs.items():
|
||||
if field in allowed_fields and hasattr(profile, field):
|
||||
setattr(profile, field, value)
|
||||
updated_fields.append(field)
|
||||
|
||||
if updated_fields:
|
||||
profile.save(update_fields=updated_fields)
|
||||
logger.info(f"Preferences updated for user: {user.email}")
|
||||
|
||||
return profile
|
||||
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def ban_user(
|
||||
user: User,
|
||||
reason: str,
|
||||
banned_by: User
|
||||
) -> User:
|
||||
"""
|
||||
Ban a user.
|
||||
|
||||
Args:
|
||||
user: User to ban
|
||||
reason: Reason for ban
|
||||
banned_by: User performing the ban
|
||||
|
||||
Returns:
|
||||
Updated User instance
|
||||
"""
|
||||
user.ban(reason=reason, banned_by=banned_by)
|
||||
logger.warning(f"User banned: {user.email} by {banned_by.email}. Reason: {reason}")
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def unban_user(user: User) -> User:
|
||||
"""
|
||||
Unban a user.
|
||||
|
||||
Args:
|
||||
user: User to unban
|
||||
|
||||
Returns:
|
||||
Updated User instance
|
||||
"""
|
||||
user.unban()
|
||||
logger.info(f"User unbanned: {user.email}")
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
def get_user_stats(user: User) -> Dict[str, Any]:
|
||||
"""
|
||||
Get user statistics.
|
||||
|
||||
Args:
|
||||
user: User instance
|
||||
|
||||
Returns:
|
||||
Dictionary of user stats
|
||||
"""
|
||||
profile = user.profile
|
||||
|
||||
return {
|
||||
'total_submissions': profile.total_submissions,
|
||||
'approved_submissions': profile.approved_submissions,
|
||||
'reputation_score': user.reputation_score,
|
||||
'member_since': user.date_joined,
|
||||
'last_active': user.last_login,
|
||||
}
|
||||
Reference in New Issue
Block a user