""" User Deletion Service This service handles user account deletion while preserving submissions and maintaining data integrity across the platform. """ from django.utils import timezone from django.db import transaction from django.contrib.auth import get_user_model from django.core.mail import send_mail from django.conf import settings from django.template.loader import render_to_string from typing import Dict, Any, Tuple, Optional import logging import secrets import string from datetime import datetime from apps.accounts.models import User logger = logging.getLogger(__name__) User = get_user_model() class UserDeletionRequest: """Model for tracking user deletion requests.""" def __init__(self, user: User, verification_code: str, expires_at: datetime): self.user = user self.verification_code = verification_code self.expires_at = expires_at self.created_at = timezone.now() class UserDeletionService: """Service for handling user account deletion with submission preservation.""" # In-memory storage for deletion requests (in production, use Redis or database) _deletion_requests = {} @staticmethod def can_delete_user(user: User) -> Tuple[bool, Optional[str]]: """ Check if a user can be safely deleted. Args: user: User to check for deletion eligibility Returns: Tuple[bool, Optional[str]]: (can_delete, reason_if_not) """ # Prevent deletion of superusers if user.is_superuser: return False, "Cannot delete superuser accounts" # Prevent deletion of staff/admin users if user.is_staff: return False, "Cannot delete staff accounts" # Check for system users (if you have any special system accounts) if hasattr(user, 'role') and user.role in ['ADMIN', 'MODERATOR']: return False, "Cannot delete admin or moderator accounts" return True, None @staticmethod def request_user_deletion(user: User) -> UserDeletionRequest: """ Create a deletion request for a user and send verification email. Args: user: User requesting deletion Returns: UserDeletionRequest: The deletion request object Raises: ValueError: If user cannot be deleted """ # Check if user can be deleted can_delete, reason = UserDeletionService.can_delete_user(user) if not can_delete: raise ValueError(reason) # Generate verification code verification_code = ''.join(secrets.choice( string.ascii_uppercase + string.digits) for _ in range(8)) # Set expiration (24 hours from now) expires_at = timezone.now() + timezone.timedelta(hours=24) # Create deletion request deletion_request = UserDeletionRequest(user, verification_code, expires_at) # Store request (in production, use Redis or database) UserDeletionService._deletion_requests[verification_code] = deletion_request # Send verification email UserDeletionService._send_deletion_verification_email( user, verification_code, expires_at) return deletion_request @staticmethod def verify_and_delete_user(verification_code: str) -> Dict[str, Any]: """ Verify deletion code and delete user account. Args: verification_code: Verification code from email Returns: Dict[str, Any]: Deletion result information Raises: ValueError: If verification code is invalid or expired """ # Find deletion request deletion_request = UserDeletionService._deletion_requests.get(verification_code) if not deletion_request: raise ValueError("Invalid verification code") # Check if expired if timezone.now() > deletion_request.expires_at: # Clean up expired request del UserDeletionService._deletion_requests[verification_code] raise ValueError("Verification code has expired") user = deletion_request.user # Perform deletion result = UserDeletionService.delete_user_preserve_submissions(user) # Clean up deletion request del UserDeletionService._deletion_requests[verification_code] # Add verification info to result result['deletion_request'] = { 'verification_code': verification_code, 'created_at': deletion_request.created_at, 'verified_at': timezone.now(), } return result @staticmethod def cancel_deletion_request(user: User) -> bool: """ Cancel a pending deletion request for a user. Args: user: User whose deletion request to cancel Returns: bool: True if request was found and cancelled, False if no request found """ # Find and remove any deletion requests for this user to_remove = [] for code, request in UserDeletionService._deletion_requests.items(): if request.user.id == user.id: to_remove.append(code) for code in to_remove: del UserDeletionService._deletion_requests[code] return len(to_remove) > 0 @staticmethod @transaction.atomic def delete_user_preserve_submissions(user: User) -> Dict[str, Any]: """ Delete a user account while preserving all their submissions. Args: user: User to delete Returns: Dict[str, Any]: Information about the deletion and preserved submissions """ # Get or create the "deleted_user" placeholder deleted_user_placeholder, created = User.objects.get_or_create( username='deleted_user', defaults={ 'email': 'deleted@thrillwiki.com', 'first_name': 'Deleted', 'last_name': 'User', 'is_active': False, } ) # Count submissions before transfer submission_counts = UserDeletionService._count_user_submissions(user) # Transfer submissions to placeholder user UserDeletionService._transfer_user_submissions(user, deleted_user_placeholder) # Store user info before deletion deleted_user_info = { 'username': user.username, 'user_id': getattr(user, 'user_id', user.id), 'email': user.email, 'date_joined': user.date_joined, } # Delete the user account user.delete() return { 'deleted_user': deleted_user_info, 'preserved_submissions': submission_counts, 'transferred_to': { 'username': deleted_user_placeholder.username, 'user_id': getattr(deleted_user_placeholder, 'user_id', deleted_user_placeholder.id), } } @staticmethod def _count_user_submissions(user: User) -> Dict[str, int]: """Count all submissions for a user.""" counts = {} # Count different types of submissions # Note: These are placeholder counts - adjust based on your actual models counts['park_reviews'] = getattr( user, 'park_reviews', user.__class__.objects.none()).count() counts['ride_reviews'] = getattr( user, 'ride_reviews', user.__class__.objects.none()).count() counts['uploaded_park_photos'] = getattr( user, 'uploaded_park_photos', user.__class__.objects.none()).count() counts['uploaded_ride_photos'] = getattr( user, 'uploaded_ride_photos', user.__class__.objects.none()).count() counts['top_lists'] = getattr( user, 'top_lists', user.__class__.objects.none()).count() counts['edit_submissions'] = getattr( user, 'edit_submissions', user.__class__.objects.none()).count() counts['photo_submissions'] = getattr( user, 'photo_submissions', user.__class__.objects.none()).count() return counts @staticmethod def _transfer_user_submissions(user: User, placeholder_user: User) -> None: """Transfer all user submissions to placeholder user.""" # Transfer different types of submissions # Note: Adjust these based on your actual model relationships # Park reviews if hasattr(user, 'park_reviews'): user.park_reviews.all().update(user=placeholder_user) # Ride reviews if hasattr(user, 'ride_reviews'): user.ride_reviews.all().update(user=placeholder_user) # Uploaded photos if hasattr(user, 'uploaded_park_photos'): user.uploaded_park_photos.all().update(user=placeholder_user) if hasattr(user, 'uploaded_ride_photos'): user.uploaded_ride_photos.all().update(user=placeholder_user) # Top lists if hasattr(user, 'top_lists'): user.top_lists.all().update(user=placeholder_user) # Edit submissions if hasattr(user, 'edit_submissions'): user.edit_submissions.all().update(user=placeholder_user) # Photo submissions if hasattr(user, 'photo_submissions'): user.photo_submissions.all().update(user=placeholder_user) @staticmethod def _send_deletion_verification_email(user: User, verification_code: str, expires_at: timezone.datetime) -> None: """Send verification email for account deletion.""" try: context = { 'user': user, 'verification_code': verification_code, 'expires_at': expires_at, 'site_name': 'ThrillWiki', 'site_url': getattr(settings, 'SITE_URL', 'https://thrillwiki.com'), } subject = 'ThrillWiki: Confirm Account Deletion' html_message = render_to_string( 'emails/account_deletion_verification.html', context) plain_message = render_to_string( 'emails/account_deletion_verification.txt', context) send_mail( subject=subject, message=plain_message, html_message=html_message, from_email=settings.DEFAULT_FROM_EMAIL, recipient_list=[user.email], fail_silently=False, ) logger.info(f"Deletion verification email sent to {user.email}") except Exception as e: logger.error( f"Failed to send deletion verification email to {user.email}: {str(e)}") raise