Files
shopship/apps/account/api/services.py
T
2026-08-24 15:54:08 +03:30

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