adding
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user