Initial commit - clean project structure
This commit is contained in:
194
accounts/views.py
Normal file
194
accounts/views.py
Normal file
@@ -0,0 +1,194 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user