You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

231 lines
9.6 KiB

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 apps.account.models.verification import VerificationDocument
from apps.account.serializers.auth_serializers import (
RegisterRequestSerializer,
RegisterResponseSerializer,
UserMeSerializer,
UserMeUpdateSerializer,
VerificationDocumentSerializer,
VerificationDocumentUploadSerializer,
PasswordRecoverRequestSerializer,
PasswordResetRequestSerializer,
CustomTokenObtainPairSerializer,
)
logger = logging.getLogger(__name__)
User = get_user_model()
class CustomTokenObtainPairView(TokenObtainPairView):
"""
Takes a set of user credentials (either 'username' or 'email' + password) and returns access & refresh token pair.
"""
serializer_class = CustomTokenObtainPairSerializer
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)