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