This commit is contained in:
2026-08-24 15:54:08 +03:30
commit 236002a87c
133 changed files with 5946 additions and 0 deletions
+135
View File
@@ -0,0 +1,135 @@
import hashlib
import hmac
import logging
import random
import string
from datetime import timedelta
from django.conf import settings
from django.db import transaction
from django.utils import timezone
from rest_framework.exceptions import ValidationError as DRFValidationError
from apps.account.models import User, phone_validator
from apps.account.send_sms import send_otp_message
logger = logging.getLogger(__name__)
def _hash_otp(otp: str) -> str:
secret_key = settings.SECRET_KEY.encode("utf-8")
signature = hmac.new(secret_key, msg=otp.encode("utf-8"), digestmod=hashlib.sha256)
return signature.hexdigest()
class UserService:
OTP_EXPIRY_SECONDS = 120
OTP_LENGTH = 4
# فقط برای توسعه/تست — بدون نیاز به سرویس پیامک واقعی (که هنوز در .env
# تنظیم نشده)، کد تایید همیشه همین است. فقط زمانی که DEBUG=True باشد
# فعال است؛ در پروداکشن (DEBUG=False) خودکار به کد تصادفی واقعی برمی‌گردد
# — وگرنه هرکسی با دانستن شماره‌ی هرکس می‌توانست وارد حسابش شود.
DEV_FIXED_OTP = "1111"
@staticmethod
def _generate_otp() -> str:
if settings.DEBUG:
return UserService.DEV_FIXED_OTP
return "".join(random.choices(string.digits, k=UserService.OTP_LENGTH))
@staticmethod
def send_login_code(phone: str, referral_code: str | None = None) -> dict:
"""ساخت/یافتن کاربر بر اساس شماره و ارسال کد تایید"""
phone_validator(phone)
with transaction.atomic():
user, created = User.objects.get_or_create(
phone=phone,
defaults={"state": User.State.INACTIVE, "is_active": True},
)
if created and referral_code:
from apps.billing.api.services import ReferralService
ReferralService.link_referrer(user, referral_code)
if not created and user.state == User.State.BLOCKED:
raise DRFValidationError({"phone": "حساب کاربری مسدود شده است"})
if not created and user.otp_code and user.otp_expiry and timezone.now() < user.otp_expiry:
remaining = (user.otp_expiry - timezone.now()).seconds
raise DRFValidationError(
{"phone": f"کد تایید قبلی هنوز معتبر است. {remaining} ثانیه دیگر منقضی می‌شود."}
)
otp = UserService._generate_otp()
user.otp_code = _hash_otp(otp)
user.otp_expiry = timezone.now() + timedelta(seconds=UserService.OTP_EXPIRY_SECONDS)
user.save(update_fields=["otp_code", "otp_expiry"])
send_otp_message(phone, otp)
logger.info(f"OTP requested for {phone}")
return {"message": "کد تایید با موفقیت ارسال شد"}
@staticmethod
def verify_user(phone: str, otp: str) -> User:
try:
user = User.objects.get(phone=phone)
except User.DoesNotExist:
raise DRFValidationError({"phone": "کاربر یافت نشد"})
if not user.otp_code or not user.otp_expiry:
raise DRFValidationError({"otp": "کد تایید درخواست نشده است"})
if timezone.now() > user.otp_expiry:
user.otp_code = None
user.otp_expiry = None
user.save(update_fields=["otp_code", "otp_expiry"])
raise DRFValidationError({"otp": "کد تایید منقضی شده است"})
if not hmac.compare_digest(user.otp_code, _hash_otp(otp.strip())):
raise DRFValidationError({"otp": "کد وارد شده نامعتبر است"})
user.state = User.State.ACTIVE
user.last_login = timezone.now()
user.otp_code = None
user.otp_expiry = None
user.save(update_fields=["state", "last_login", "otp_code", "otp_expiry"])
from apps.billing.api.services import ReferralService
ReferralService.grant_bonus_if_needed(user)
logger.info(f"User {user.phone} verified successfully")
return user
@staticmethod
def verify_otp_for_action(user: User, otp: str) -> bool:
"""تایید OTP برای عملیات حساس (مثل حذف حساب)"""
if not user.otp_code or not user.otp_expiry:
raise DRFValidationError({"otp": "ابتدا کد تایید درخواست کنید"})
if timezone.now() > user.otp_expiry:
user.otp_code = None
user.otp_expiry = None
user.save(update_fields=["otp_code", "otp_expiry"])
raise DRFValidationError({"otp": "کد تایید منقضی شده است"})
if not hmac.compare_digest(user.otp_code, _hash_otp(otp.strip())):
raise DRFValidationError({"otp": "کد وارد شده نامعتبر است"})
user.otp_code = None
user.otp_expiry = None
user.save(update_fields=["otp_code", "otp_expiry"])
return True
@staticmethod
def update_user_profile(user: User, validated_data: dict) -> User:
update_fields = []
for key in ("name",):
if key in validated_data:
setattr(user, key, validated_data[key])
update_fields.append(key)
if update_fields:
user.save(update_fields=update_fields)
return user