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
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)
|