136 lines
5.4 KiB
Python
136 lines
5.4 KiB
Python
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
|