mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-29 23:27:03 -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:
@@ -1,5 +1,6 @@
|
||||
from rest_framework import serializers
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import serializers
|
||||
|
||||
from apps.accounts.models import UserProfile
|
||||
from apps.accounts.serializers import UserSerializer # existing shared user serializer
|
||||
|
||||
@@ -24,7 +25,7 @@ class UserProfileUpdateInputSerializer(serializers.ModelSerializer):
|
||||
from django_cloudflareimages_toolkit.models import CloudflareImage
|
||||
image, _ = CloudflareImage.objects.get_or_create(cloudflare_id=cloudflare_id)
|
||||
instance.avatar = image
|
||||
|
||||
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
|
||||
@@ -2,8 +2,14 @@
|
||||
URL configuration for user account management API endpoints.
|
||||
"""
|
||||
|
||||
from django.urls import path
|
||||
from . import views
|
||||
from django.urls import include, path
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from . import views, views_credits, views_magic_link
|
||||
|
||||
# Register ViewSets
|
||||
router = DefaultRouter()
|
||||
router.register(r"credits", views_credits.RideCreditViewSet, basename="ride-credit")
|
||||
|
||||
urlpatterns = [
|
||||
# Admin endpoints for user management
|
||||
@@ -108,19 +114,18 @@ urlpatterns = [
|
||||
path("profile/avatar/upload/", views.upload_avatar, name="upload_avatar"),
|
||||
path("profile/avatar/save/", views.save_avatar_image, name="save_avatar_image"),
|
||||
path("profile/avatar/delete/", views.delete_avatar, name="delete_avatar"),
|
||||
|
||||
|
||||
# Login history endpoint
|
||||
path("login-history/", views.get_login_history, name="get_login_history"),
|
||||
|
||||
# Magic Link (Login by Code) endpoints
|
||||
path("magic-link/request/", views_magic_link.request_magic_link, name="request_magic_link"),
|
||||
path("magic-link/verify/", views_magic_link.verify_magic_link, name="verify_magic_link"),
|
||||
|
||||
# Public Profile
|
||||
path("profiles/<str:username>/", views.get_public_user_profile, name="get_public_user_profile"),
|
||||
]
|
||||
|
||||
# Register ViewSets
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from . import views_credits
|
||||
from django.urls import include
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r"credits", views_credits.RideCreditViewSet, basename="ride-credit")
|
||||
|
||||
urlpatterns += [
|
||||
# ViewSet routes
|
||||
path("", include(router.urls)),
|
||||
]
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
from rest_framework import viewsets, permissions, filters
|
||||
from django.db import transaction
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from apps.rides.models.credits import RideCredit
|
||||
from apps.api.v1.serializers.ride_credits import RideCreditSerializer
|
||||
from drf_spectacular.utils import extend_schema, OpenApiParameter
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiParameter, extend_schema
|
||||
from rest_framework import filters, permissions, status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from apps.api.v1.serializers.ride_credits import RideCreditSerializer
|
||||
from apps.rides.models.credits import RideCredit
|
||||
|
||||
|
||||
class RideCreditViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
@@ -14,8 +19,8 @@ class RideCreditViewSet(viewsets.ModelViewSet):
|
||||
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
|
||||
filter_backends = [DjangoFilterBackend, filters.OrderingFilter]
|
||||
filterset_fields = ['user__username', 'ride__park__slug', 'ride__manufacturer__slug']
|
||||
ordering_fields = ['first_ridden_at', 'last_ridden_at', 'created_at', 'count', 'rating']
|
||||
ordering = ['-last_ridden_at']
|
||||
ordering_fields = ['first_ridden_at', 'last_ridden_at', 'created_at', 'count', 'rating', 'display_order']
|
||||
ordering = ['display_order', '-last_ridden_at']
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
@@ -23,18 +28,77 @@ class RideCreditViewSet(viewsets.ModelViewSet):
|
||||
Optionally filter by user via query param ?user=username
|
||||
"""
|
||||
queryset = RideCredit.objects.all().select_related('ride', 'ride__park', 'user')
|
||||
|
||||
|
||||
# Filter by user if provided
|
||||
username = self.request.query_params.get('user')
|
||||
if username:
|
||||
queryset = queryset.filter(user__username=username)
|
||||
|
||||
|
||||
return queryset
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""Associate the current user with the ride credit."""
|
||||
serializer.save(user=self.request.user)
|
||||
|
||||
@action(detail=False, methods=['post'], permission_classes=[permissions.IsAuthenticated])
|
||||
@extend_schema(
|
||||
summary="Reorder ride credits",
|
||||
description="Bulk update the display order of ride credits. Send a list of {id, order} objects.",
|
||||
request={
|
||||
'application/json': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'order': {
|
||||
'type': 'array',
|
||||
'items': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'id': {'type': 'integer'},
|
||||
'order': {'type': 'integer'}
|
||||
},
|
||||
'required': ['id', 'order']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
def reorder(self, request):
|
||||
"""
|
||||
Bulk update display_order for multiple credits.
|
||||
Expects: {"order": [{"id": 1, "order": 0}, {"id": 2, "order": 1}, ...]}
|
||||
"""
|
||||
order_data = request.data.get('order', [])
|
||||
|
||||
if not order_data:
|
||||
return Response(
|
||||
{'error': 'No order data provided'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Validate that all credits belong to the current user
|
||||
credit_ids = [item['id'] for item in order_data]
|
||||
user_credits = RideCredit.objects.filter(
|
||||
id__in=credit_ids,
|
||||
user=request.user
|
||||
).values_list('id', flat=True)
|
||||
|
||||
if set(credit_ids) != set(user_credits):
|
||||
return Response(
|
||||
{'error': 'You can only reorder your own credits'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
# Bulk update in a transaction
|
||||
with transaction.atomic():
|
||||
for item in order_data:
|
||||
RideCredit.objects.filter(
|
||||
id=item['id'],
|
||||
user=request.user
|
||||
).update(display_order=item['order'])
|
||||
|
||||
return Response({'status': 'reordered', 'count': len(order_data)})
|
||||
|
||||
@extend_schema(
|
||||
summary="List ride credits",
|
||||
description="List ride credits. filter by user username.",
|
||||
@@ -49,3 +113,4 @@ class RideCreditViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
def list(self, request, *args, **kwargs):
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
|
||||
180
backend/apps/api/v1/accounts/views_magic_link.py
Normal file
180
backend/apps/api/v1/accounts/views_magic_link.py
Normal file
@@ -0,0 +1,180 @@
|
||||
"""
|
||||
Magic Link (Login by Code) API views.
|
||||
|
||||
Provides API endpoints for passwordless login via email code.
|
||||
Uses django-allauth's built-in login-by-code functionality.
|
||||
"""
|
||||
from django.conf import settings
|
||||
from drf_spectacular.utils import OpenApiExample, extend_schema
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.response import Response
|
||||
|
||||
try:
|
||||
from allauth.account.internal.flows.login_by_code import perform_login_by_code, request_login_code
|
||||
from allauth.account.models import EmailAddress
|
||||
from allauth.account.utils import user_email # noqa: F401 - imported to verify availability
|
||||
HAS_LOGIN_BY_CODE = True
|
||||
except ImportError:
|
||||
HAS_LOGIN_BY_CODE = False
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary="Request magic link login code",
|
||||
description="Send a one-time login code to the user's email address.",
|
||||
request={
|
||||
'application/json': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'email': {'type': 'string', 'format': 'email'}
|
||||
},
|
||||
'required': ['email']
|
||||
}
|
||||
},
|
||||
responses={
|
||||
200: {'description': 'Login code sent successfully'},
|
||||
400: {'description': 'Invalid email or feature disabled'},
|
||||
},
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
'Request login code',
|
||||
value={'email': 'user@example.com'},
|
||||
request_only=True
|
||||
)
|
||||
]
|
||||
)
|
||||
@api_view(['POST'])
|
||||
@permission_classes([AllowAny])
|
||||
def request_magic_link(request):
|
||||
"""
|
||||
Request a login code to be sent to the user's email.
|
||||
|
||||
This is the first step of the magic link flow:
|
||||
1. User enters their email
|
||||
2. If the email exists, a code is sent
|
||||
3. User enters the code to complete login
|
||||
"""
|
||||
if not getattr(settings, 'ACCOUNT_LOGIN_BY_CODE_ENABLED', False):
|
||||
return Response(
|
||||
{'error': 'Magic link login is not enabled'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
if not HAS_LOGIN_BY_CODE:
|
||||
return Response(
|
||||
{'error': 'Login by code is not available in this version of allauth'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
email = request.data.get('email', '').lower().strip()
|
||||
|
||||
if not email:
|
||||
return Response(
|
||||
{'error': 'Email is required'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Check if email exists (don't reveal if it doesn't for security)
|
||||
try:
|
||||
email_address = EmailAddress.objects.get(email__iexact=email, verified=True)
|
||||
user = email_address.user
|
||||
|
||||
# Request the login code
|
||||
request_login_code(request._request, user)
|
||||
|
||||
return Response({
|
||||
'success': True,
|
||||
'message': 'If an account exists with this email, a login code has been sent.',
|
||||
'timeout': getattr(settings, 'ACCOUNT_LOGIN_BY_CODE_TIMEOUT', 300)
|
||||
})
|
||||
|
||||
except EmailAddress.DoesNotExist:
|
||||
# Don't reveal that the email doesn't exist
|
||||
return Response({
|
||||
'success': True,
|
||||
'message': 'If an account exists with this email, a login code has been sent.',
|
||||
'timeout': getattr(settings, 'ACCOUNT_LOGIN_BY_CODE_TIMEOUT', 300)
|
||||
})
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary="Verify magic link code",
|
||||
description="Verify the login code and complete the login process.",
|
||||
request={
|
||||
'application/json': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'email': {'type': 'string', 'format': 'email'},
|
||||
'code': {'type': 'string'}
|
||||
},
|
||||
'required': ['email', 'code']
|
||||
}
|
||||
},
|
||||
responses={
|
||||
200: {'description': 'Login successful'},
|
||||
400: {'description': 'Invalid or expired code'},
|
||||
}
|
||||
)
|
||||
@api_view(['POST'])
|
||||
@permission_classes([AllowAny])
|
||||
def verify_magic_link(request):
|
||||
"""
|
||||
Verify the login code and complete the login.
|
||||
|
||||
This is the second step of the magic link flow.
|
||||
"""
|
||||
if not getattr(settings, 'ACCOUNT_LOGIN_BY_CODE_ENABLED', False):
|
||||
return Response(
|
||||
{'error': 'Magic link login is not enabled'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
if not HAS_LOGIN_BY_CODE:
|
||||
return Response(
|
||||
{'error': 'Login by code is not available'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
email = request.data.get('email', '').lower().strip()
|
||||
code = request.data.get('code', '').strip()
|
||||
|
||||
if not email or not code:
|
||||
return Response(
|
||||
{'error': 'Email and code are required'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
try:
|
||||
email_address = EmailAddress.objects.get(email__iexact=email, verified=True)
|
||||
user = email_address.user
|
||||
|
||||
# Attempt to verify the code and log in
|
||||
success = perform_login_by_code(request._request, user, code)
|
||||
|
||||
if success:
|
||||
return Response({
|
||||
'success': True,
|
||||
'message': 'Login successful',
|
||||
'user': {
|
||||
'id': user.id,
|
||||
'username': user.username,
|
||||
'email': user.email
|
||||
}
|
||||
})
|
||||
else:
|
||||
return Response(
|
||||
{'error': 'Invalid or expired code. Please request a new one.'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
except EmailAddress.DoesNotExist:
|
||||
return Response(
|
||||
{'error': 'Invalid email or code'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
except Exception:
|
||||
return Response(
|
||||
{'error': 'Invalid or expired code. Please request a new one.'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
Reference in New Issue
Block a user