188 lines
6.4 KiB
Python
188 lines
6.4 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, _ = 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'])
|
|
|
|
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)
|