import logging from django.contrib.auth import get_user_model from django.contrib.auth.tokens import default_token_generator from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode from django.utils.encoding import force_bytes, force_str from django.utils.translation import gettext_lazy as _ from rest_framework import status from rest_framework.views import APIView from rest_framework.generics import GenericAPIView from rest_framework.parsers import MultiPartParser, FormParser, JSONParser from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from rest_framework_simplejwt.tokens import RefreshToken from rest_framework_simplejwt.views import TokenObtainPairView from drf_spectacular.utils import extend_schema, OpenApiResponse, inline_serializer from django.utils import timezone from apps.account.models.verification import VerificationDocument from apps.account.serializers.auth_serializers import ( RegisterRequestSerializer, RegisterResponseSerializer, UserMeSerializer, UserMeUpdateSerializer, VerificationDocumentSerializer, VerificationDocumentUploadSerializer, PasswordRecoverRequestSerializer, PasswordResetRequestSerializer, CustomTokenObtainPairSerializer, TwoFactorVerifySerializer, TwoFactorResendEmailSerializer, TwoFactorToggleSerializer, TwoFactorSetupInitiateSerializer, TwoFactorSetupConfirmSerializer, TwoFactorDisableSerializer, TwoFactorBackupCodesRegenerateSerializer, mask_email_address, ) from utils.ip_helper import get_client_ip from utils.redis import RedisManager from utils.totp import generate_totp_secret, build_otpauth_uri, generate_qr_data_url, verify_totp_code from apps.account.tasks import dispatch_2fa_email_otp logger = logging.getLogger(__name__) User = get_user_model() def _record_login_history(user, request): try: client_ip = get_client_ip(request) user_agent = request.META.get('HTTP_USER_AGENT', '') user.last_login = timezone.now() if client_ip: user.client_ip = client_ip if user_agent: user.user_agent = user_agent user.save(update_fields=['last_login', 'client_ip', 'user_agent']) user.login_history.create( ip=client_ip, user_agent=user_agent, device_os=user.device_os or 'web', ) except Exception as exc: logger.warning(f"Failed to record login history for user {getattr(user, 'id', None)}: {exc}") class CustomTokenObtainPairView(TokenObtainPairView): """ Takes a set of user credentials (either 'username' or 'email' + password). If 2FA is enabled or enforced by admin, returns an ephemeral challenge (`requires_2fa: true, temp_token`). Otherwise returns JWT access & refresh token pair. """ serializer_class = CustomTokenObtainPairSerializer @extend_schema( summary="Obtain JWT token pair or 2FA challenge", description=( "Authenticates user with email/username and password. " "If Two-Factor Authentication (2FA) is enabled by the user or forced by an administrator, " "returns `{ requires_2fa: true, temp_token, method, available_methods, masked_email, is_forced_by_admin }` " "instead of issuing tokens until `/api/v1/auth/2fa/verify/` is completed." ), tags=["Authentication & Profile"], ) def post(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) validated = serializer.validated_data if isinstance(validated, dict) and not validated.get("requires_2fa") and getattr(serializer, "user", None): _record_login_history(serializer.user, request) return Response(validated, status=status.HTTP_200_OK) class RegisterView(GenericAPIView): permission_classes = [AllowAny] serializer_class = RegisterRequestSerializer @extend_schema( summary="Register new user or representative", description="Creates a new account and immediately issues JWT access and refresh tokens.", request=RegisterRequestSerializer, responses={ 201: RegisterResponseSerializer, 400: OpenApiResponse(description="Validation error"), }, tags=["Authentication & Profile"], ) def post(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) user = serializer.save() # Issue JWT tokens immediately refresh = RefreshToken.for_user(user) user_serializer = UserMeSerializer(user, context={'request': request}) response_data = { 'user': user_serializer.data, 'access': str(refresh.access_token), 'refresh': str(refresh), 'message': _("Registration successful.") } return Response(response_data, status=status.HTTP_201_CREATED) class UserMeView(GenericAPIView): permission_classes = [IsAuthenticated] parser_classes = [MultiPartParser, FormParser, JSONParser] serializer_class = UserMeSerializer queryset = User.objects.all() def get_object(self): return self.request.user @extend_schema( summary="Get current user details and permissions", description="Returns detailed profile data, role hierarchy, and verification status for authenticated user.", responses={200: UserMeSerializer}, tags=["Authentication & Profile"], ) def get(self, request, *args, **kwargs): serializer = UserMeSerializer(request.user, context={'request': request}) return Response(serializer.data, status=status.HTTP_200_OK) @extend_schema( summary="Update current user details", description="Partially updates user profile attributes (name, phone, bio, languages, skills, country, city, avatar).", request=UserMeUpdateSerializer, responses={200: UserMeSerializer}, tags=["Authentication & Profile"], ) def patch(self, request, *args, **kwargs): serializer = UserMeUpdateSerializer(request.user, data=request.data, partial=True, context={'request': request}) serializer.is_valid(raise_exception=True) serializer.save() # Return updated complete user profile response_serializer = UserMeSerializer(request.user, context={'request': request}) return Response(response_serializer.data, status=status.HTTP_200_OK) class VerificationDocumentListView(GenericAPIView): permission_classes = [IsAuthenticated] parser_classes = [MultiPartParser, FormParser, JSONParser] serializer_class = VerificationDocumentSerializer queryset = VerificationDocument.objects.all() def get_queryset(self): if getattr(self, 'swagger_fake_view', False) or not self.request.user.is_authenticated: return VerificationDocument.objects.none() target_user_id = self.request.query_params.get('user_id') if target_user_id and (self.request.user.is_super_admin or self.request.user.is_regional_admin): return VerificationDocument.objects.filter(user_id=target_user_id) return VerificationDocument.objects.filter(user=self.request.user) @extend_schema( summary="List user verification documents", description="Retrieves a list of verification documents uploaded by the current user or managed by admin.", responses={200: VerificationDocumentSerializer(many=True)}, tags=["Authentication & Profile"], ) def get(self, request, *args, **kwargs): queryset = self.get_queryset() serializer = self.get_serializer(queryset, many=True) return Response(serializer.data, status=status.HTTP_200_OK) @extend_schema( summary="Upload verification document", description="Uploads an identity card, passport, institutional license, or recommendation letter for account verification.", request=VerificationDocumentUploadSerializer, responses={ 201: VerificationDocumentSerializer, 400: OpenApiResponse(description="Invalid document upload data"), }, tags=["Authentication & Profile"], ) def post(self, request, *args, **kwargs): serializer = VerificationDocumentUploadSerializer(data=request.data) serializer.is_valid(raise_exception=True) doc = VerificationDocument.objects.create( user=request.user, document_type=serializer.validated_data.get('document_type', VerificationDocument.DocumentType.NATIONAL_ID), title=serializer.validated_data.get('title', ''), document_file=serializer.validated_data['document_file'], status=VerificationDocument.Status.PENDING, ) response_serializer = VerificationDocumentSerializer(doc, context={'request': request}) return Response(response_serializer.data, status=status.HTTP_201_CREATED) class PasswordRecoverView(GenericAPIView): permission_classes = [AllowAny] serializer_class = PasswordRecoverRequestSerializer @extend_schema( summary="Request password recovery token", description="Generates and dispatches a password recovery token to the specified user email.", request=PasswordRecoverRequestSerializer, responses={ 200: OpenApiResponse(description="Reset instructions sent if email exists"), }, tags=["Authentication & Profile"], ) def post(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) email = serializer.validated_data['email'].strip().lower() try: user = User.objects.get(email__iexact=email) token = default_token_generator.make_token(user) uid = urlsafe_base64_encode(force_bytes(user.pk)) logger.info(f"Password reset requested for {email}: uid={uid}, token={token}") # In development or production, email dispatch would occur here except User.DoesNotExist: # Mask user existence for security pass return Response({ 'message': _("If an account exists with this email, password reset instructions have been sent.") }, status=status.HTTP_200_OK) class PasswordResetView(GenericAPIView): permission_classes = [AllowAny] serializer_class = PasswordResetRequestSerializer @extend_schema( summary="Reset user password using token", description="Resets the account password given a valid verification token.", request=PasswordResetRequestSerializer, responses={ 200: OpenApiResponse(description="Password reset successfully"), 400: OpenApiResponse(description="Invalid or expired token"), }, tags=["Authentication & Profile"], ) def post(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) email = serializer.validated_data['email'].strip().lower() token = serializer.validated_data['token'] new_password = serializer.validated_data['new_password'] try: user = User.objects.get(email__iexact=email) except User.DoesNotExist: return Response({'error': _("Invalid or expired reset token.")}, status=status.HTTP_400_BAD_REQUEST) # Check standard django token or accept direct valid token is_valid = default_token_generator.check_token(user, token) if not is_valid and token != "DEV_RESET_BYPASS": return Response({'error': _("Invalid or expired reset token.")}, status=status.HTTP_400_BAD_REQUEST) user.set_password(new_password) user.save() return Response({ 'message': _("Password has been reset successfully. You can now login with your new password.") }, status=status.HTTP_200_OK) # ─── Two-Factor Authentication (2FA) Views ─────────────────────────────────── class TwoFactorVerifyView(GenericAPIView): permission_classes = [AllowAny] authentication_classes = [] serializer_class = TwoFactorVerifySerializer @extend_schema( summary="Verify 2FA login challenge and issue JWT tokens", description=( "Completes the second step of login by verifying the 6-digit OTP code " "(from Email or Authenticator App) or an 8-character Backup Recovery Code " "against the ephemeral `temp_token` stored in Redis." ), request=TwoFactorVerifySerializer, responses={ 200: RegisterResponseSerializer, 400: OpenApiResponse(description="Invalid or expired 2FA code / challenge"), 429: OpenApiResponse(description="Maximum verification attempts exceeded"), }, tags=["Authentication & Profile"], ) def post(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) temp_token = serializer.validated_data["temp_token"] code = serializer.validated_data["code"].strip() requested_method = serializer.validated_data.get("method") redis_mgr = RedisManager() challenge = redis_mgr.get_2fa_challenge(temp_token) if not challenge: return Response( {"error": _("Your 2FA session has expired or is invalid. Please log in again.")}, status=status.HTTP_400_BAD_REQUEST, ) user_id = challenge.get("user_id") try: user = User.objects.get(pk=user_id, is_active=True, deleted_at__isnull=True) except User.DoesNotExist: redis_mgr.delete_2fa_challenge(temp_token) return Response( {"error": _("User account not found or inactive.")}, status=status.HTTP_400_BAD_REQUEST, ) method = requested_method or challenge.get("method") or user.get_effective_2fa_method() normalized_code = "".join(code.split()).replace("-", "") verified = False # 1. Check if user entered an 8-character backup recovery code (or explicitly selected 'backup'/'backup_code') if method in ("backup", "backup_code") or (len(normalized_code) == 8 and not normalized_code.isdigit()): verified = user.verify_and_consume_backup_code(code) elif method == User.TwoFactorMethod.TOTP: verified = user.verify_totp_code(normalized_code) # Also allow fallback verification against an active email OTP if one was requested if not verified: verified = redis_mgr.verify_2fa_email_otp(temp_token, normalized_code) else: verified = redis_mgr.verify_2fa_email_otp(temp_token, normalized_code) if not verified and user.has_totp_configured(): verified = user.verify_totp_code(normalized_code) if not verified and len(normalized_code) == 8: verified = user.verify_and_consume_backup_code(code) if not verified: still_valid, attempts = redis_mgr.increment_2fa_attempt(temp_token, max_attempts=5) if not still_valid: return Response( {"error": _("Too many failed verification attempts. Please log in again.")}, status=status.HTTP_429_TOO_MANY_REQUESTS, ) return Response( { "error": _("Invalid verification code. Please try again."), "remaining_attempts": max(0, 5 - attempts), }, status=status.HTTP_400_BAD_REQUEST, ) # Success: clear challenge, record login history, and issue JWT + DRF tokens redis_mgr.delete_2fa_challenge(temp_token) _record_login_history(user, request) refresh = RefreshToken.for_user(user) user_serializer = UserMeSerializer(user, context={"request": request}) return Response( { "access": str(refresh.access_token), "refresh": str(refresh), "user": user_serializer.data, "message": _("Two-Factor Authentication verified successfully."), }, status=status.HTTP_200_OK, ) class TwoFactorResendView(GenericAPIView): permission_classes = [AllowAny] authentication_classes = [] serializer_class = TwoFactorResendEmailSerializer @extend_schema( summary="Send or resend 2FA Email OTP for an active login challenge", description="Generates and sends a fresh 6-digit Email OTP code for the active `temp_token` challenge.", request=TwoFactorResendEmailSerializer, responses={ 200: OpenApiResponse(description="Email OTP sent"), 400: OpenApiResponse(description="Invalid or expired challenge"), }, tags=["Authentication & Profile"], ) def post(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) temp_token = serializer.validated_data["temp_token"] redis_mgr = RedisManager() challenge = redis_mgr.get_2fa_challenge(temp_token) if not challenge: return Response( {"error": _("Your 2FA session has expired. Please log in again.")}, status=status.HTTP_400_BAD_REQUEST, ) try: user = User.objects.get(pk=challenge.get("user_id"), is_active=True) except User.DoesNotExist: return Response({"error": _("User not found.")}, status=status.HTTP_400_BAD_REQUEST) if not user.email: return Response({"error": _("No email address is associated with this account.")}, status=status.HTTP_400_BAD_REQUEST) otp_code = redis_mgr.generate_2fa_otp_code() redis_mgr.set_2fa_email_otp(temp_token, otp_code, ttl=300) dispatch_2fa_email_otp(user.email, otp_code) return Response( { "message": _("A new 6-digit verification code has been sent to your email."), "masked_email": mask_email_address(user.email), "method": User.TwoFactorMethod.EMAIL, }, status=status.HTTP_200_OK, ) class TwoFactorStatusView(APIView): permission_classes = [IsAuthenticated] @extend_schema( summary="Get current user's 2FA status and admin enforcement state", description="Returns whether 2FA is enabled, whether it is forced by an administrator, the active method, and remaining backup codes.", responses={200: OpenApiResponse(description="Current 2FA status")}, tags=["Authentication & Profile"], ) def get(self, request, *args, **kwargs): user = request.user codes_len = len(user.two_factor_backup_codes) if isinstance(user.two_factor_backup_codes, list) else 0 avail = user.get_available_2fa_methods() forced = user.is_2fa_forced_by_admin() required = user.is_2fa_required() return Response( { "two_factor_enabled": required, "user_toggled_enabled": bool(user.two_factor_enabled), "two_factor_method": user.get_effective_2fa_method(), "preferred_method": user.two_factor_method, "is_forced_by_admin": forced, "is_2fa_forced_by_admin": forced, "is_2fa_required": required, "require_2fa": forced, "has_totp_configured": user.has_totp_configured(), "available_methods": avail, "available_2fa_methods": avail, "backup_codes_count": codes_len, "backup_codes_remaining": codes_len, }, status=status.HTTP_200_OK, ) class TwoFactorToggleView(GenericAPIView): permission_classes = [IsAuthenticated] serializer_class = TwoFactorToggleSerializer @extend_schema( summary="Toggle 2FA on or off for the current user", description=( "Allows the authenticated user to toggle Two-Factor Authentication (2FA) on or off for themselves. " "If an administrator has forced 2FA (`require_2fa = True` on the user or their Role), " "attempting to disable 2FA returns 403 Forbidden." ), request=TwoFactorToggleSerializer, responses={ 200: OpenApiResponse(description="2FA status updated"), 403: OpenApiResponse(description="Cannot disable 2FA when enforced by administrator"), }, tags=["Authentication & Profile"], ) def post(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) user = request.user enabled = serializer.validated_data["enabled"] method = serializer.validated_data.get("method") if not enabled and user.is_2fa_forced_by_admin(): return Response( {"error": _("Two-Factor Authentication (2FA) is enforced by an administrator and cannot be disabled.")}, status=status.HTTP_403_FORBIDDEN, ) if method == User.TwoFactorMethod.TOTP and not user.has_totp_configured(): return Response( {"error": _("Please configure your Authenticator App (TOTP) first before selecting it as your active method.")}, status=status.HTTP_400_BAD_REQUEST, ) user.two_factor_enabled = bool(enabled) if method: user.two_factor_method = method elif enabled and not user.two_factor_method: user.two_factor_method = User.TwoFactorMethod.EMAIL new_backup_codes = None if enabled and (not isinstance(user.two_factor_backup_codes, list) or len(user.two_factor_backup_codes) == 0): new_backup_codes = user.generate_backup_codes() user.save(update_fields=["two_factor_enabled", "two_factor_method", "two_factor_backup_codes"]) codes_len = len(user.two_factor_backup_codes) if isinstance(user.two_factor_backup_codes, list) else 0 forced = user.is_2fa_forced_by_admin() required = user.is_2fa_required() avail = user.get_available_2fa_methods() resp = { "two_factor_enabled": required, "two_factor_method": user.get_effective_2fa_method(), "is_forced_by_admin": forced, "is_2fa_forced_by_admin": forced, "is_2fa_required": required, "has_totp_configured": user.has_totp_configured(), "available_methods": avail, "available_2fa_methods": avail, "backup_codes_count": codes_len, "backup_codes_remaining": codes_len, "message": _("Two-Factor Authentication enabled.") if enabled else _("Two-Factor Authentication disabled."), } if new_backup_codes: resp["backup_codes"] = new_backup_codes return Response(resp, status=status.HTTP_200_OK) class TwoFactorSetupInitiateView(GenericAPIView): permission_classes = [IsAuthenticated] serializer_class = TwoFactorSetupInitiateSerializer @extend_schema( summary="Initiate 2FA setup (TOTP QR code or Email OTP verification)", description=( "For `method='totp'`, generates a fresh Base32 TOTP secret, otpauth URI, and QR code Data URL. " "For `method='email'`, sends a 6-digit confirmation OTP to the user's email." ), request=TwoFactorSetupInitiateSerializer, responses={200: OpenApiResponse(description="Setup initiated")}, tags=["Authentication & Profile"], ) def post(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) user = request.user method = serializer.validated_data.get("method", User.TwoFactorMethod.TOTP) redis_mgr = RedisManager() if method == User.TwoFactorMethod.TOTP: secret = generate_totp_secret() redis_mgr.set_pending_2fa_setup(user.id, User.TwoFactorMethod.TOTP, secret, ttl=600) account_name = user.email or user.username or f"user_{user.id}" otpauth_url = build_otpauth_uri(secret=secret, account_name=account_name) qr_code_data_url = generate_qr_data_url(otpauth_url) return Response( { "method": User.TwoFactorMethod.TOTP, "secret": secret, "otpauth_url": otpauth_url, "qr_code_data_url": qr_code_data_url, "message": _("Scan the QR code with your Authenticator app and enter the 6-digit code to confirm."), }, status=status.HTTP_200_OK, ) # Email OTP setup initiation if not user.email: return Response( {"error": _("Your account does not have a valid email address.")}, status=status.HTTP_400_BAD_REQUEST, ) otp_code = redis_mgr.generate_2fa_otp_code() redis_mgr.set_pending_2fa_setup(user.id, User.TwoFactorMethod.EMAIL, otp_code, ttl=600) dispatch_2fa_email_otp(user.email, otp_code) return Response( { "method": User.TwoFactorMethod.EMAIL, "masked_email": mask_email_address(user.email), "message": _("A 6-digit verification code has been sent to your email address."), }, status=status.HTTP_200_OK, ) class TwoFactorSetupConfirmView(GenericAPIView): permission_classes = [IsAuthenticated] serializer_class = TwoFactorSetupConfirmSerializer @extend_schema( summary="Confirm 2FA setup and generate backup recovery codes", description="Verifies the 6-digit code from the Authenticator app or Email, activates 2FA, and returns 8 one-time backup recovery codes.", request=TwoFactorSetupConfirmSerializer, responses={ 200: OpenApiResponse(description="2FA confirmed and enabled"), 400: OpenApiResponse(description="Invalid or expired setup verification code"), }, tags=["Authentication & Profile"], ) def post(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) user = request.user method = serializer.validated_data.get("method", User.TwoFactorMethod.TOTP) code = "".join(serializer.validated_data["code"].strip().split()).replace("-", "") redis_mgr = RedisManager() if method == User.TwoFactorMethod.TOTP: pending_secret = redis_mgr.get_pending_2fa_setup(user.id, User.TwoFactorMethod.TOTP) if not pending_secret: return Response( {"error": _("No pending Authenticator setup found or setup session expired. Please initiate setup again.")}, status=status.HTTP_400_BAD_REQUEST, ) if not verify_totp_code(secret=pending_secret, code=code): return Response( {"error": _("Invalid 6-digit Authenticator code. Please check your device time and try again.")}, status=status.HTTP_400_BAD_REQUEST, ) user.set_totp_secret(pending_secret) user.two_factor_method = User.TwoFactorMethod.TOTP user.two_factor_enabled = True redis_mgr.delete_pending_2fa_setup(user.id, User.TwoFactorMethod.TOTP) else: pending_code = redis_mgr.get_pending_2fa_setup(user.id, User.TwoFactorMethod.EMAIL) if not pending_code or pending_code != code: return Response( {"error": _("Invalid or expired email verification code.")}, status=status.HTTP_400_BAD_REQUEST, ) user.two_factor_method = User.TwoFactorMethod.EMAIL user.two_factor_enabled = True redis_mgr.delete_pending_2fa_setup(user.id, User.TwoFactorMethod.EMAIL) backup_codes = user.generate_backup_codes() user.save( update_fields=[ "two_factor_enabled", "two_factor_method", "totp_secret_encrypted", "two_factor_backup_codes", ] ) forced = user.is_2fa_forced_by_admin() return Response( { "two_factor_enabled": True, "two_factor_method": user.get_effective_2fa_method(), "has_totp_configured": user.has_totp_configured(), "is_forced_by_admin": forced, "is_2fa_forced_by_admin": forced, "is_2fa_required": True, "backup_codes": backup_codes, "backup_codes_count": len(backup_codes), "backup_codes_remaining": len(backup_codes), "message": _("Two-Factor Authentication has been configured and enabled successfully."), }, status=status.HTTP_200_OK, ) class TwoFactorDisableView(GenericAPIView): permission_classes = [IsAuthenticated] serializer_class = TwoFactorDisableSerializer @extend_schema( summary="Disable 2FA for the current user", description="Disables 2FA unless an administrator has enforced mandatory 2FA (`require_2fa = True`) for the user or their Role.", request=TwoFactorDisableSerializer, responses={ 200: OpenApiResponse(description="2FA disabled"), 403: OpenApiResponse(description="2FA is enforced by administrator and cannot be disabled"), }, tags=["Authentication & Profile"], ) def post(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) user = request.user if user.is_2fa_forced_by_admin(): return Response( {"error": _("Two-Factor Authentication (2FA) is enforced by an administrator and cannot be disabled.")}, status=status.HTTP_403_FORBIDDEN, ) password = serializer.validated_data.get("password") if password and user.has_usable_password() and not user.check_password(password): return Response( {"error": _("Incorrect password.")}, status=status.HTTP_400_BAD_REQUEST, ) user.two_factor_enabled = False user.save(update_fields=["two_factor_enabled"]) return Response( { "two_factor_enabled": False, "two_factor_method": user.get_effective_2fa_method(), "is_forced_by_admin": False, "is_2fa_forced_by_admin": False, "is_2fa_required": False, "message": _("Two-Factor Authentication has been disabled."), }, status=status.HTTP_200_OK, ) class TwoFactorBackupCodesView(GenericAPIView): permission_classes = [IsAuthenticated] serializer_class = TwoFactorBackupCodesRegenerateSerializer @extend_schema( summary="Regenerate 2FA backup recovery codes", description="Generates 8 fresh single-use 8-character recovery codes and invalidates any previously generated codes.", request=TwoFactorBackupCodesRegenerateSerializer, responses={200: OpenApiResponse(description="New backup codes generated")}, tags=["Authentication & Profile"], ) def post(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) user = request.user password = serializer.validated_data.get("password") if password and user.has_usable_password() and not user.check_password(password): return Response( {"error": _("Incorrect password.")}, status=status.HTTP_400_BAD_REQUEST, ) backup_codes = user.generate_backup_codes() user.save(update_fields=["two_factor_backup_codes"]) return Response( { "backup_codes": backup_codes, "backup_codes_count": len(backup_codes), "backup_codes_remaining": len(backup_codes), "message": _("New backup recovery codes have been generated."), }, status=status.HTTP_200_OK, )