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,44 +6,46 @@ login, signup, logout, password management, social authentication,
|
||||
user profiles, and top lists.
|
||||
"""
|
||||
|
||||
from .serializers_package.social import (
|
||||
ConnectedProviderSerializer,
|
||||
AvailableProviderSerializer,
|
||||
SocialAuthStatusSerializer,
|
||||
ConnectProviderInputSerializer,
|
||||
ConnectProviderOutputSerializer,
|
||||
DisconnectProviderOutputSerializer,
|
||||
SocialProviderErrorSerializer,
|
||||
)
|
||||
from apps.accounts.services.social_provider_service import SocialProviderService
|
||||
from django.contrib.auth import authenticate, login, logout, get_user_model
|
||||
from typing import cast # added 'cast'
|
||||
|
||||
from django.contrib.auth import authenticate, get_user_model, login, logout
|
||||
from django.contrib.sites.shortcuts import get_current_site
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Q
|
||||
from typing import Optional, cast # added 'cast'
|
||||
from django.http import HttpRequest # new import
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view
|
||||
from rest_framework import status
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from apps.accounts.services.social_provider_service import SocialProviderService
|
||||
|
||||
# Import directly from the auth serializers.py file (not the serializers package)
|
||||
from .serializers import (
|
||||
AuthStatusOutputSerializer,
|
||||
# Authentication serializers
|
||||
LoginInputSerializer,
|
||||
LoginOutputSerializer,
|
||||
SignupInputSerializer,
|
||||
SignupOutputSerializer,
|
||||
LogoutOutputSerializer,
|
||||
UserOutputSerializer,
|
||||
PasswordResetInputSerializer,
|
||||
PasswordResetOutputSerializer,
|
||||
PasswordChangeInputSerializer,
|
||||
PasswordChangeOutputSerializer,
|
||||
PasswordResetInputSerializer,
|
||||
PasswordResetOutputSerializer,
|
||||
SignupInputSerializer,
|
||||
SignupOutputSerializer,
|
||||
SocialProviderOutputSerializer,
|
||||
AuthStatusOutputSerializer,
|
||||
UserOutputSerializer,
|
||||
)
|
||||
from .serializers_package.social import (
|
||||
AvailableProviderSerializer,
|
||||
ConnectedProviderSerializer,
|
||||
ConnectProviderInputSerializer,
|
||||
ConnectProviderOutputSerializer,
|
||||
DisconnectProviderOutputSerializer,
|
||||
SocialAuthStatusSerializer,
|
||||
SocialProviderErrorSerializer,
|
||||
)
|
||||
|
||||
# Handle optional dependencies with fallback classes
|
||||
@@ -62,10 +64,7 @@ try:
|
||||
|
||||
# Ensure the imported object is a class/type that can be used as a base class.
|
||||
# If it's not a type for any reason, fall back to the safe mixin.
|
||||
if isinstance(_ImportedTurnstileMixin, type):
|
||||
TurnstileMixin = _ImportedTurnstileMixin
|
||||
else:
|
||||
TurnstileMixin = FallbackTurnstileMixin
|
||||
TurnstileMixin = _ImportedTurnstileMixin if isinstance(_ImportedTurnstileMixin, type) else FallbackTurnstileMixin
|
||||
except Exception:
|
||||
# Catch any import errors or unexpected exceptions and use the fallback mixin.
|
||||
TurnstileMixin = FallbackTurnstileMixin
|
||||
@@ -88,7 +87,7 @@ def _get_underlying_request(request: Request) -> HttpRequest:
|
||||
# Helper: encapsulate user lookup + authenticate to reduce complexity in view
|
||||
def _authenticate_user_by_lookup(
|
||||
email_or_username: str, password: str, request: Request
|
||||
) -> Optional[UserModel]:
|
||||
) -> UserModel | None:
|
||||
"""
|
||||
Try a single optimized query to find a user by email OR username then authenticate.
|
||||
Returns authenticated user or None.
|
||||
@@ -199,7 +198,7 @@ class LoginAPIView(APIView):
|
||||
else:
|
||||
return Response(
|
||||
{
|
||||
"error": "Email verification required",
|
||||
"error": "Email verification required",
|
||||
"message": "Please verify your email address before logging in. Check your email for a verification link.",
|
||||
"email_verification_required": True
|
||||
},
|
||||
@@ -246,7 +245,7 @@ class SignupAPIView(APIView):
|
||||
serializer = SignupInputSerializer(data=request.data, context={"request": request})
|
||||
if serializer.is_valid():
|
||||
user = serializer.save()
|
||||
|
||||
|
||||
# Don't log in the user immediately - they need to verify their email first
|
||||
response_serializer = SignupOutputSerializer(
|
||||
{
|
||||
@@ -754,23 +753,23 @@ class EmailVerificationAPIView(APIView):
|
||||
|
||||
def get(self, request: Request, token: str) -> Response:
|
||||
from apps.accounts.models import EmailVerification
|
||||
|
||||
|
||||
try:
|
||||
verification = EmailVerification.objects.select_related('user').get(token=token)
|
||||
user = verification.user
|
||||
|
||||
|
||||
# Activate the user
|
||||
user.is_active = True
|
||||
user.save()
|
||||
|
||||
|
||||
# Delete the verification record
|
||||
verification.delete()
|
||||
|
||||
|
||||
return Response({
|
||||
"message": "Email verified successfully. You can now log in.",
|
||||
"success": True
|
||||
})
|
||||
|
||||
|
||||
except EmailVerification.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Invalid or expired verification token"},
|
||||
@@ -798,45 +797,46 @@ class ResendVerificationAPIView(APIView):
|
||||
authentication_classes = []
|
||||
|
||||
def post(self, request: Request) -> Response:
|
||||
from apps.accounts.models import EmailVerification
|
||||
from django.contrib.sites.shortcuts import get_current_site
|
||||
from django.utils.crypto import get_random_string
|
||||
from django_forwardemail.services import EmailService
|
||||
from django.contrib.sites.shortcuts import get_current_site
|
||||
|
||||
|
||||
from apps.accounts.models import EmailVerification
|
||||
|
||||
email = request.data.get('email')
|
||||
if not email:
|
||||
return Response(
|
||||
{"error": "Email address is required"},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
user = UserModel.objects.get(email__iexact=email.strip().lower())
|
||||
|
||||
|
||||
# Don't resend if user is already active
|
||||
if user.is_active:
|
||||
return Response(
|
||||
{"error": "Email is already verified"},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
# Create or update verification record
|
||||
verification, created = EmailVerification.objects.get_or_create(
|
||||
user=user,
|
||||
defaults={'token': get_random_string(64)}
|
||||
)
|
||||
|
||||
|
||||
if not created:
|
||||
# Update existing token and timestamp
|
||||
verification.token = get_random_string(64)
|
||||
verification.save()
|
||||
|
||||
|
||||
# Send verification email
|
||||
site = get_current_site(_get_underlying_request(request))
|
||||
verification_url = request.build_absolute_uri(
|
||||
f"/api/v1/auth/verify-email/{verification.token}/"
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
EmailService.send_email(
|
||||
to=user.email,
|
||||
@@ -854,22 +854,22 @@ The ThrillWiki Team
|
||||
""".strip(),
|
||||
site=site,
|
||||
)
|
||||
|
||||
|
||||
return Response({
|
||||
"message": "Verification email sent successfully",
|
||||
"success": True
|
||||
})
|
||||
|
||||
|
||||
except Exception as e:
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.error(f"Failed to send verification email to {user.email}: {e}")
|
||||
|
||||
|
||||
return Response(
|
||||
{"error": "Failed to send verification email"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
|
||||
except UserModel.DoesNotExist:
|
||||
# Don't reveal whether email exists
|
||||
return Response({
|
||||
|
||||
Reference in New Issue
Block a user