122 lines
6.6 KiB
Python
122 lines
6.6 KiB
Python
"""
|
|
منطق کمکیِ اپ 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 با خطا مواجه شد.')
|