import random import uuid from django.conf import settings from django.utils import timezone from drf_spectacular.utils import extend_schema from rest_framework import status from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView from .authentication import issue_access_token from .models import OTPCode, Session, User from .serializers import ( GuestSessionRequestSerializer, GuestSessionResponseSerializer, LoginRequestSerializer, LoginResponseSerializer, RefreshRequestSerializer, RefreshResponseSerializer, RequestOTPRequestSerializer, RequestOTPResponseSerializer, SetGradeRequestSerializer, SetGradeResponseSerializer, ) class GuestSessionView(APIView): """POST /api/auth/session — ایجاد سشن مهمان""" @extend_schema( request=GuestSessionRequestSerializer, responses=GuestSessionResponseSerializer, tags=['Identity & Profile (accounts)'], ) def post(self, request): serializer = GuestSessionRequestSerializer(data=request.data) serializer.is_valid(raise_exception=True) device_id = serializer.validated_data['deviceId'] user, is_new_user = User.objects.get_or_create( device_id=device_id, is_guest=True, defaults={'name': ''}, ) token = issue_access_token(user.id) data = { 'userId': str(user.id), 'token': token, 'isNewUser': is_new_user, 'isGuest': True, } return Response(GuestSessionResponseSerializer(data).data) class RequestOTPView(APIView): """ POST /api/auth/otp/request — درخواست کد OTP (کمکی، برای تست از طریق Swagger) در محیط توسعه، کد ساخته‌شده در پاسخ برگردونده می‌شه تا نیازی به SMS واقعی نباشه. """ @extend_schema( request=RequestOTPRequestSerializer, responses=RequestOTPResponseSerializer, tags=['Identity & Profile (accounts)'], ) def post(self, request): serializer = RequestOTPRequestSerializer(data=request.data) serializer.is_valid(raise_exception=True) phone = serializer.validated_data['phone'] code = f'{random.randint(0, 9999):04d}' OTPCode.objects.create( phone=phone, code=code, expires_at=timezone.now() + timezone.timedelta(minutes=5), ) data = { 'success': True, 'devOtp': code, 'note': f'در پروداکشن این کد پیامک می‌شه؛ برای تست همیشه {settings.DEV_MASTER_OTP} هم قبول می‌شه.', } return Response(RequestOTPResponseSerializer(data).data) class LoginView(APIView): """POST /api/auth/login — ورود با OTP""" @extend_schema( request=LoginRequestSerializer, responses=LoginResponseSerializer, tags=['Identity & Profile (accounts)'], ) def post(self, request): serializer = LoginRequestSerializer(data=request.data) serializer.is_valid(raise_exception=True) phone = serializer.validated_data['phone'] otp = serializer.validated_data['otp'] valid = False if otp == settings.DEV_MASTER_OTP: valid = True else: otp_qs = OTPCode.objects.filter( phone=phone, code=otp, is_used=False, expires_at__gte=timezone.now() ).order_by('-id') otp_obj = otp_qs.first() if otp_obj: otp_obj.is_used = True otp_obj.save(update_fields=['is_used']) valid = True if not valid: return Response({'detail': 'کد OTP نامعتبر یا منقضی‌شده است.'}, status=status.HTTP_400_BAD_REQUEST) user, created = User.objects.get_or_create( phone=phone, defaults={'is_guest': False, 'name': ''}, ) if user.is_guest: user.is_guest = False user.save(update_fields=['is_guest']) if created: # ثبت‌نام واقعی (نه guest session): آفرهای auto_assign_on_signup # (مثل یک هفته رایگان) از طریق discount/receivers.py روی همین # سیگنال اعمال می‌شن؛ این ویو از وجود آفر/discount چیزی نمی‌دونه. from .signals import user_registered user_registered.send(sender=User, user_id=user.id) refresh_token = uuid.uuid4().hex Session.objects.create( user=user, refresh_token=refresh_token, device_id=request.data.get('deviceId', ''), expires_at=timezone.now() + timezone.timedelta(days=settings.REFRESH_TOKEN_TTL_DAYS), ) access_token = issue_access_token(user.id) data = { 'userId': str(user.id), 'token': access_token, 'refreshToken': refresh_token, } return Response(LoginResponseSerializer(data).data) class RefreshTokenView(APIView): """POST /api/auth/refresh — تمدید توکن""" @extend_schema( request=RefreshRequestSerializer, responses=RefreshResponseSerializer, tags=['Identity & Profile (accounts)'], ) def post(self, request): serializer = RefreshRequestSerializer(data=request.data) serializer.is_valid(raise_exception=True) refresh_token = serializer.validated_data['refreshToken'] session = Session.objects.filter( refresh_token=refresh_token, revoked=False, expires_at__gte=timezone.now() ).first() if not session: return Response({'detail': 'رفرش‌توکن نامعتبر یا منقضی‌شده است.'}, status=status.HTTP_401_UNAUTHORIZED) access_token = issue_access_token(session.user_id) data = {'token': access_token, 'expiresIn': settings.ACCESS_TOKEN_TTL_SECONDS} return Response(RefreshResponseSerializer(data).data) class SetUserGradeView(APIView): """POST /api/user/grade — ثبت پایه‌ی کاربر (نیاز به احراز هویت)""" permission_classes = [IsAuthenticated] @extend_schema( request=SetGradeRequestSerializer, responses=SetGradeResponseSerializer, tags=['Identity & Profile (accounts)'], ) def post(self, request): serializer = SetGradeRequestSerializer(data=request.data) serializer.is_valid(raise_exception=True) grade_id = serializer.validated_data['gradeId'] user = request.user user.grade_id = grade_id user.save(update_fields=['grade_id']) data = {'success': True, 'gradeId': grade_id} return Response(SetGradeResponseSerializer(data).data)