""" منطق کمکیِ اپ Accounts که از views.py همین اپ صدا زده می‌شه. """ import logging import requests from django.conf import settings from rest_framework.exceptions import APIException logger = logging.getLogger(__name__) def _phone_suffix(phone: str) -> str: """ ⚠️ اصلاح: قبلاً لاگ‌های مربوط به OTP، شماره‌تلفنِ کامل رو لاگ می‌کردن (PIIِ غیرضروری در لاگ‌های اپلیکیشن). حالا فقط ۴ رقمِ آخر (برای correlation/دیباگ، بدونِ افشای کاملِ شماره) لاگ می‌شه — همون الگویی که قبلاً در accounts/views.py::LoginView هم برای همین منظور استفاده شده. ⚠️ اصلاحِ این دور: لاگ‌های خطای ارائه‌دهنده (provider) دیگه response.text خام رو لاگ نمی‌کنن — چون بدنه‌ی پاسخِ کاوه‌نگار می‌تونه داده‌ی حساس/غیرضروری (مثلاً جزئیاتِ گیرنده یا پیامِ متنی) داشته باشه. به‌جاش فقط context مفید و بی‌خطر (phone_suffix، status، provider_status) لاگ می‌شه. """ return phone[-4:] if phone else phone class OTPDeliveryError(APIException): """ ⚠️ اصلاح: قبلاً send_otp_sms یک استاب بود که فقط لاگ می‌کرد و همیشه (بی‌قیدوشرط) به RequestOTPView «موفقیت» برمی‌گردوند؛ یعنی در پروداکشن (DEBUG=False، جایی که devOtp دیگه توی پاسخ برنمی‌گرده) هیچ پیامکِ واقعی‌ای ارسال نمی‌شد ولی API همچنان success=True می‌داد — کاربرِ واقعی هیچ‌وقت کدش رو نمی‌گرفت و حتی نمی‌فهمید که مشکلی پیش اومده. حالا اگه پیکربندیِ سرویسِ پیامک ناقص باشه یا خودِ درخواست به ارائه‌دهنده شکست بخوره، همین استثنا raise می‌شه و RequestOTPView یک خطای صریح (نه یک پاسخِ ساختگیِ موفق) برمی‌گردونه. """ status_code = 503 default_detail = 'در حال حاضر امکان ارسال پیامک OTP وجود ندارد؛ لطفاً بعداً دوباره تلاش کنید.' default_code = 'otp_sms_delivery_failed' def send_otp_sms(*, phone: str, code: str) -> None: """ ارسالِ واقعیِ پیامکِ OTP از طریق کاوه‌نگار (kavenegar.com) — دقیقاً همون ارائه‌دهنده‌ای که در نسخه‌ی قبلی (استاب) به‌عنوان نمونه اشاره شده بود. کلید API هیچ‌وقت داخل کد نیست؛ همیشه از env خونده می‌شه (settings.KAVENEGAR_API_KEY). دو حالت: - DEBUG=True (dev/test): دقیقاً مثل قبل، فقط لاگ می‌کنه و هیچ پیامکِ واقعی‌ای نمی‌فرسته — چون در این محیط، RequestOTPView خودِ کد رو در فیلدِ devOtp پاسخ برمی‌گردونه و SMS واقعی لازم نیست (رفتار قبلی dev حفظ شده). - DEBUG=False (production): اگه KAVENEGAR_API_KEY تنظیم نشده باشه، یا درخواست به کاوه‌نگار fail بشه/کدِ غیر ۲xx برگردونه، به‌جای گزارشِ خاموشِ «موفقیت»، OTPDeliveryError raise می‌شه (کاربر پاسخ ۵۰۳ صریح می‌گیره، نه یک success دروغین). """ if settings.DEBUG: logger.info( 'OTP sms dispatch (dev, no real SMS sent): phone_suffix=%s code_len=%d', _phone_suffix(phone), len(code), ) return if not settings.KAVENEGAR_API_KEY: logger.error( 'OTP SMS misconfigured: KAVENEGAR_API_KEY is not set (phone_suffix=%s)', _phone_suffix(phone), ) raise OTPDeliveryError('پیکربندیِ سرویسِ پیامک ناقص است.') message = settings.OTP_SMS_TEMPLATE.format(code=code) payload = {'receptor': phone, 'message': message} if settings.KAVENEGAR_SENDER: payload['sender'] = settings.KAVENEGAR_SENDER try: response = requests.post( f'https://api.kavenegar.com/v1/{settings.KAVENEGAR_API_KEY}/sms/send.json', data=payload, timeout=5, ) except requests.RequestException: logger.exception('OTP SMS request to provider failed: phone_suffix=%s', _phone_suffix(phone)) raise OTPDeliveryError('ارسال پیامک OTP با خطا مواجه شد.') if response.status_code != 200: logger.error( 'OTP SMS provider returned non-200: phone_suffix=%s status=%s', _phone_suffix(phone), response.status_code, ) raise OTPDeliveryError('ارسال پیامک OTP با خطا مواجه شد.') # ⚠️ اصلاح: کاوه‌نگار حتی برای شکست‌های سطحِ اپلیکیشن (کلیدِ نامعتبر، # اعتبار ناکافی، گیرنده‌ی نامعتبر و…) HTTP 200 برمی‌گردونه؛ نتیجه‌ی # واقعی توی بدنه‌ی JSON، زیرِ return.status هست (200 یعنی موفق). قبلاً # همینِ status_code == 200 به‌تنهایی «موفقیت» در نظر گرفته می‌شد، یعنی # یک رد شدنِ واقعی از طرفِ کاوه‌نگار هم success=True دروغین برمی‌گردوند. # حالا بدنه parse می‌شه و فقط return.status == 200 موفقیت محسوب می‌شه؛ # هر پاسخِ ناقص/غیرمنتظره هم شکستِ تحویل در نظر گرفته می‌شه (نه موفقیت). try: body = response.json() provider_status = body['return']['status'] except (ValueError, KeyError, TypeError): logger.error( 'OTP SMS provider returned malformed response body: phone_suffix=%s status=%s', _phone_suffix(phone), response.status_code, ) raise OTPDeliveryError('ارسال پیامک OTP با خطا مواجه شد.') if provider_status != 200: logger.error( 'OTP SMS provider rejected message at application level: phone_suffix=%s ' 'provider_status=%s', _phone_suffix(phone), provider_status, ) raise OTPDeliveryError('ارسال پیامک OTP با خطا مواجه شد.')