Files
MathApp/accounts/phone.py
Zareian 0580aec7b0 harden OTP for real SMS and close the second audit round
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>
2026-09-25 12:28:06 +03:30

32 lines
1.6 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
نرمال‌سازیِ شماره‌ی موبایلِ ایران به شکلِ یکتای ‎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