mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 11:31:07 -05:00
310 lines
10 KiB
Python
310 lines
10 KiB
Python
"""
|
|
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
|