import secrets from datetime import timedelta from django.contrib.auth import get_user_model from django.core.exceptions import ValidationError as DjangoValidationError from django.db import transaction from django.utils import timezone from rest_framework import exceptions from apps.cuser.models import CuserRole, SmsOpt from apps.jwt_auth.models import CUserRoleLoginOpt from utils.services.sms import DefaultSmsProvider from utils.validations.mobile import normalize_mobile CUser = get_user_model() SMS_RESEND_INTERVAL = 60 class SmsRequestThrottled(exceptions.Throttled): def __init__(self, wait): super().__init__(wait=wait) self.detail = {'detail': 'کد ورود قبلاً ارسال شده است. کمی صبر کنید و کد قبلی را وارد کنید.', 'retry_after': wait} class SmsDeliveryUnavailable(exceptions.APIException): status_code = 503 default_detail = 'ارسال پیامک انجام نشد. لطفاً کمی بعد دوباره تلاش کنید.' default_code = 'sms_failed' def eligible_roles(user, customer_only=False): roles = CuserRole.objects.select_related('role_type', 'sys_ws', 'cuser').filter( cuser=user, is_active=True, mobile_verify=True, sys_ws__is_active=True, ) if customer_only: roles = roles.filter(role_type__role_name_en='workspace_customer', customer_panel=True) return [role for role in roles if (not role.sys_ws.expire or role.sys_ws.expire > timezone.now()) and (role.role_type.role_name_en != 'workspace_customer' or role.customer_panel is True)] def create_login_options(user, roles): CUserRoleLoginOpt.objects.filter(cuser=user).delete() options = [] for role in roles: options.append(CUserRoleLoginOpt.objects.create( cuser=user, cuser_role=role, expire=timezone.now() + timedelta(minutes=5), )) return options @transaction.atomic def login_by_username(username: str, password: str): user = CUser.objects.select_for_update().filter(username=username).first() if not user or not user.check_password(password): raise exceptions.ValidationError({'error': 'نام کاربری یا رمز عبور اشتباه است.'}) if not user.is_active: raise exceptions.PermissionDenied('حساب کاربری شما غیرفعال است.') roles = eligible_roles(user) if not roles: raise exceptions.PermissionDenied('نقش یا دسترسی فعال برای این حساب وجود ندارد.') return create_login_options(user, roles) def mobile_user(mobile): try: mobile = normalize_mobile(mobile) except DjangoValidationError as error: raise exceptions.ValidationError(error.message_dict) user = CUser.objects.filter(mobile=mobile).first() if not user: raise exceptions.ValidationError({'mobile': 'کاربری با این شماره موبایل ثبت نشده است.'}) if not user.is_active: raise exceptions.PermissionDenied('حساب کاربری شما غیرفعال است.') roles = eligible_roles(user, customer_only=True) if not roles: raise exceptions.PermissionDenied('دسترسی شما به پنل مشتریان فعال نیست.') return user, roles @transaction.atomic def login_by_mobile(mobile: int): user, _ = mobile_user(mobile) # Serialize code requests for this account, retaining the old valid code if # the provider refuses the new message. CUser.objects.select_for_update().get(pk=user.pk) # Recheck eligibility after waiting for concurrent requests/administrative edits. user, _ = mobile_user(user.mobile) current = SmsOpt.objects.filter(mobile=user.mobile, expire__gt=timezone.now()).order_by('-created_at').first() if current: remaining = SMS_RESEND_INTERVAL - (timezone.now() - current.created_at).total_seconds() if remaining > 0: from math import ceil raise SmsRequestThrottled(ceil(remaining)) code = secrets.randbelow(900000) + 100000 if not DefaultSmsProvider().send_sms(str(user.mobile), f'کد ورود شما: {code}'): raise SmsDeliveryUnavailable() SmsOpt.objects.filter(mobile=user.mobile).delete() SmsOpt.objects.create(mobile=user.mobile, opt=code, expire=timezone.now() + timedelta(minutes=3)) return user.mobile @transaction.atomic def verify_mobile_and_get_token(mobile: int, opt: int): user, roles = mobile_user(mobile) CUser.objects.select_for_update().get(pk=user.pk) code = SmsOpt.objects.select_for_update().filter( mobile=user.mobile, opt=opt, expire__gt=timezone.now(), ).first() if not code: raise exceptions.ValidationError({'opt': 'کد تأیید اشتباه است یا اعتبار آن تمام شده است.'}) options = create_login_options(user, roles) SmsOpt.objects.filter(mobile=user.mobile).delete() return options