mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-29 21:47:01 -05:00
feat: Implement MFA authentication, add ride statistics model, and update various services, APIs, and tests across the application.
This commit is contained in:
@@ -6,43 +6,44 @@ user deletion while preserving submissions, profile management, settings,
|
||||
preferences, privacy, notifications, and security.
|
||||
"""
|
||||
|
||||
from apps.api.v1.serializers.accounts import (
|
||||
CompleteUserSerializer,
|
||||
PublicUserSerializer,
|
||||
UserPreferencesSerializer,
|
||||
NotificationSettingsSerializer,
|
||||
PrivacySettingsSerializer,
|
||||
SecuritySettingsSerializer,
|
||||
UserStatisticsSerializer,
|
||||
UserListSerializer,
|
||||
AccountUpdateSerializer,
|
||||
ProfileUpdateSerializer,
|
||||
ThemePreferenceSerializer,
|
||||
UserNotificationSerializer,
|
||||
NotificationPreferenceSerializer,
|
||||
MarkNotificationsReadSerializer,
|
||||
AvatarUploadSerializer,
|
||||
)
|
||||
from apps.accounts.services import UserDeletionService
|
||||
from apps.accounts.export_service import UserExportService
|
||||
from apps.accounts.models import (
|
||||
User,
|
||||
UserProfile,
|
||||
UserNotification,
|
||||
NotificationPreference,
|
||||
)
|
||||
from apps.lists.models import UserList
|
||||
import logging
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.permissions import IsAuthenticated, IsAdminUser
|
||||
from rest_framework.response import Response
|
||||
from drf_spectacular.utils import extend_schema, OpenApiParameter
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.permissions import AllowAny
|
||||
from django.utils import timezone
|
||||
from django_cloudflareimages_toolkit.models import CloudflareImage
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiParameter, extend_schema
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.permissions import AllowAny, IsAdminUser, IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from apps.accounts.export_service import UserExportService
|
||||
from apps.accounts.models import (
|
||||
NotificationPreference,
|
||||
User,
|
||||
UserNotification,
|
||||
UserProfile,
|
||||
)
|
||||
from apps.accounts.services import UserDeletionService
|
||||
from apps.api.v1.serializers.accounts import (
|
||||
AccountUpdateSerializer,
|
||||
AvatarUploadSerializer,
|
||||
CompleteUserSerializer,
|
||||
MarkNotificationsReadSerializer,
|
||||
NotificationPreferenceSerializer,
|
||||
NotificationSettingsSerializer,
|
||||
PrivacySettingsSerializer,
|
||||
ProfileUpdateSerializer,
|
||||
PublicUserSerializer,
|
||||
SecuritySettingsSerializer,
|
||||
ThemePreferenceSerializer,
|
||||
UserListSerializer,
|
||||
UserNotificationSerializer,
|
||||
UserPreferencesSerializer,
|
||||
UserStatisticsSerializer,
|
||||
)
|
||||
from apps.lists.models import UserList
|
||||
|
||||
# Set up logging
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -307,7 +308,7 @@ def save_avatar_image(request):
|
||||
try:
|
||||
cloudflare_image = CloudflareImage.objects.get(
|
||||
cloudflare_id=cloudflare_image_id)
|
||||
|
||||
|
||||
# Update existing record with latest data from Cloudflare
|
||||
cloudflare_image.status = 'uploaded'
|
||||
cloudflare_image.uploaded_at = timezone.now()
|
||||
@@ -319,7 +320,7 @@ def save_avatar_image(request):
|
||||
cloudflare_image.height = image_data.get('height')
|
||||
cloudflare_image.format = image_data.get('format', '')
|
||||
cloudflare_image.save()
|
||||
|
||||
|
||||
except CloudflareImage.DoesNotExist:
|
||||
# Create new CloudflareImage record from API response
|
||||
cloudflare_image = CloudflareImage.objects.create(
|
||||
@@ -367,7 +368,7 @@ def save_avatar_image(request):
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete old avatar from Cloudflare: {str(e)}")
|
||||
# Continue with database deletion even if Cloudflare deletion fails
|
||||
|
||||
|
||||
old_avatar.delete()
|
||||
|
||||
# Debug logging to see what's happening with the CloudflareImage
|
||||
@@ -442,7 +443,7 @@ def delete_avatar(request):
|
||||
avatar_to_delete = profile.avatar
|
||||
profile.avatar = None
|
||||
profile.save()
|
||||
|
||||
|
||||
# Delete from Cloudflare first, then from database
|
||||
try:
|
||||
from django_cloudflareimages_toolkit.services import CloudflareImagesService
|
||||
@@ -452,7 +453,7 @@ def delete_avatar(request):
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete avatar from Cloudflare: {str(e)}")
|
||||
# Continue with database deletion even if Cloudflare deletion fails
|
||||
|
||||
|
||||
avatar_to_delete.delete()
|
||||
|
||||
# Get the default avatar URL
|
||||
@@ -1273,10 +1274,10 @@ def update_security_settings(request):
|
||||
|
||||
# Handle security settings updates
|
||||
if "two_factor_enabled" in request.data:
|
||||
setattr(user, "two_factor_enabled", request.data["two_factor_enabled"])
|
||||
user.two_factor_enabled = request.data["two_factor_enabled"]
|
||||
|
||||
if "login_notifications" in request.data:
|
||||
setattr(user, "login_notifications", request.data["login_notifications"])
|
||||
user.login_notifications = request.data["login_notifications"]
|
||||
|
||||
user.save()
|
||||
|
||||
@@ -1612,7 +1613,7 @@ def export_user_data(request):
|
||||
except Exception as e:
|
||||
logger.error(f"Error exporting data for user {request.user.id}: {e}", exc_info=True)
|
||||
return Response(
|
||||
{"error": "Failed to generate data export"},
|
||||
{"error": "Failed to generate data export"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
@@ -1636,54 +1637,73 @@ def get_public_user_profile(request, username):
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
# === MISSING FUNCTION IMPLEMENTATIONS ===
|
||||
|
||||
|
||||
@extend_schema(
|
||||
operation_id="request_account_deletion",
|
||||
summary="Request account deletion",
|
||||
description="Request deletion of the authenticated user's account.",
|
||||
operation_id="get_login_history",
|
||||
summary="Get user login history",
|
||||
description=(
|
||||
"Returns the authenticated user's recent login history including "
|
||||
"IP addresses, devices, and timestamps for security auditing."
|
||||
),
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="limit",
|
||||
type=OpenApiTypes.INT,
|
||||
location=OpenApiParameter.QUERY,
|
||||
description="Maximum number of entries to return (default: 20, max: 100)",
|
||||
),
|
||||
],
|
||||
responses={
|
||||
200: {"description": "Deletion request created"},
|
||||
400: {"description": "Cannot delete account"},
|
||||
},
|
||||
tags=["Self-Service Account Management"],
|
||||
)
|
||||
@api_view(["POST"])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def request_account_deletion(request):
|
||||
"""Request account deletion."""
|
||||
try:
|
||||
user = request.user
|
||||
|
||||
# Check if user can be deleted
|
||||
can_delete, reason = UserDeletionService.can_delete_user(user)
|
||||
if not can_delete:
|
||||
return Response(
|
||||
{"success": False, "error": reason},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Create deletion request
|
||||
deletion_request = UserDeletionService.create_deletion_request(user)
|
||||
|
||||
return Response(
|
||||
{
|
||||
"success": True,
|
||||
"message": "Verification code sent to your email",
|
||||
"expires_at": deletion_request.expires_at,
|
||||
"email": user.email,
|
||||
200: {
|
||||
"description": "Login history entries",
|
||||
"example": {
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"ip_address": "192.168.1.1",
|
||||
"user_agent": "Mozilla/5.0...",
|
||||
"login_method": "PASSWORD",
|
||||
"login_method_display": "Password",
|
||||
"login_timestamp": "2024-12-27T10:30:00Z",
|
||||
"country": "United States",
|
||||
"city": "New York",
|
||||
}
|
||||
],
|
||||
"count": 1,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
},
|
||||
401: {"description": "Authentication required"},
|
||||
},
|
||||
tags=["User Security"],
|
||||
)
|
||||
@api_view(["GET"])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def get_login_history(request):
|
||||
"""Get user login history for security auditing."""
|
||||
from apps.accounts.login_history import LoginHistory
|
||||
|
||||
user = request.user
|
||||
limit = min(int(request.query_params.get("limit", 20)), 100)
|
||||
|
||||
# Get login history for user
|
||||
entries = LoginHistory.objects.filter(user=user).order_by("-login_timestamp")[:limit]
|
||||
|
||||
# Serialize
|
||||
results = []
|
||||
for entry in entries:
|
||||
results.append({
|
||||
"id": entry.id,
|
||||
"ip_address": entry.ip_address,
|
||||
"user_agent": entry.user_agent[:100] if entry.user_agent else None, # Truncate long user agents
|
||||
"login_method": entry.login_method,
|
||||
"login_method_display": dict(LoginHistory._meta.get_field('login_method').choices).get(entry.login_method, entry.login_method),
|
||||
"login_timestamp": entry.login_timestamp.isoformat(),
|
||||
"country": entry.country,
|
||||
"city": entry.city,
|
||||
"success": entry.success,
|
||||
})
|
||||
|
||||
return Response({
|
||||
"results": results,
|
||||
"count": len(results),
|
||||
})
|
||||
|
||||
except ValueError as e:
|
||||
return Response(
|
||||
{"success": False, "error": str(e)},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
except Exception as e:
|
||||
return Response(
|
||||
{"success": False, "error": f"Error creating deletion request: {str(e)}"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user