Kavenegar is being switched on, so every OTP request now costs credit: - phone numbers are normalised to 09XXXXXXXXX everywhere (login, OTP, family invite, TEST_LOGIN_PHONES keys) and existing rows are migrated; non-Iranian and landline numbers are rejected before any SMS - cooldown and a daily per-phone cap are counted in the database, so the three gunicorn workers no longer triple them - a global daily OTP SMS cap (default 300) stops SMS pumping - per-IP rates widened for CGNAT / school Wi-Fi (env-overridable) - provider message id is logged; SMS text warns not to share the code Second audit round: an offer no longer shortens a longer paid plan, Zarinpal error bodies no longer 500, island XP reaches the parent report, a non-numeric questionId no longer 500s after the answer is generated. 596 tests green. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
32 lines
1.6 KiB
Python
32 lines
1.6 KiB
Python
"""
|
||
نرمالسازیِ شمارهی موبایلِ ایران به شکلِ یکتای 09XXXXXXXXX.
|
||
|
||
چرا: پیش از این `phone` رشتهی خام بود و 0912…، +98912…، 98912… و همان
|
||
شماره با ارقامِ فارسی چهار «شماره»ی جدا بودند — هر کدام cooldown و سقفِ پیامکِ
|
||
خودش را داشت و حسابِ جدا میساخت (security-before-production.md، بندِ ۱۷).
|
||
هرچه شکلِ 09\\d{9} نگیرد رد میشود، پس شمارهی خارجی یا خطِ ثابت هم به
|
||
کاوهنگار نمیرسد (پیامکِ بینالمللی گران است و هدفِ SMS pumping).
|
||
|
||
عمداً بیوابستگی به DRF است تا config/settings.py هم بتواند واردش کند
|
||
(کلیدهای TEST_LOGIN_PHONES)؛ فیلدِ serializer در accounts/serializers.py است.
|
||
"""
|
||
import re
|
||
|
||
_DIGITS = str.maketrans('۰۱۲۳۴۵۶۷۸۹٠١٢٣٤٥٦٧٨٩', '01234567890123456789')
|
||
_SEPARATORS = re.compile(r'[\s\-().\u200c\u200e\u200f]')
|
||
_MOBILE = re.compile(r'09\d{9}')
|
||
|
||
|
||
def normalize_phone(value) -> str | None:
|
||
"""شکلِ یکتای 09XXXXXXXXX، یا None اگر موبایلِ ایران نباشد."""
|
||
s = _SEPARATORS.sub('', str(value or '').translate(_DIGITS))
|
||
if s.startswith('+98'):
|
||
s = '0' + s[3:]
|
||
elif s.startswith('0098'):
|
||
s = '0' + s[4:]
|
||
elif s.startswith('98') and len(s) == 12:
|
||
s = '0' + s[2:]
|
||
elif s.startswith('9') and len(s) == 10:
|
||
s = '0' + s
|
||
return s if _MOBILE.fullmatch(s) else None
|