Files
MathApp/accounts/views.py

195 lines
6.8 KiB
Python

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)