give every developer a way into the API that does not depend on one person
With DEBUG=0 and Kavenegar unconfigured, POST /api/auth/otp/request returns a hard 503, so the only way to sign in was for someone with admin access to hand- create an OTPCode row. That made every frontend developer's login depend on one person being available. TEST_LOGIN_PHONES is the pattern real apps use for exactly this (Firebase Auth's fictional numbers, Apple's App Store demo account): a short allowlist of phone:code pairs that skip SMS delivery entirely. Unlike DEV_MASTER_OTP it is not "any phone with one code" and it is not tied to DEBUG -- each number carries its own code, and everything else takes the normal OTP path untouched. The code never appears in an API response, so knowing the number is not enough to log in, and the existing per-phone login attempt limit still applies, so the code cannot be brute-forced. Malformed configuration raises ImproperlyConfigured at startup rather than being skipped silently, and /api/health reports how many test numbers are open (count only) so they cannot be forgotten on a server that has real users. The code is capped at six digits because LoginRequestSerializer caps otp at max_length=6; a longer code would have been rejected before reaching the check and looked like the feature was broken. The guest path the frontend uses today is untouched, and leaving the variable empty reproduces the previous behaviour exactly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -132,6 +132,14 @@ def _warnings() -> list[str]:
|
||||
)
|
||||
if getattr(settings, 'DEV_MASTER_OTP', None) is not None:
|
||||
out.append('کدِ یکسانِ ورود (DEV_MASTER_OTP) فعال است — هر کسی میتواند بهجای هر کاربری وارد شود.')
|
||||
test_phones = getattr(settings, 'TEST_LOGIN_PHONES', {})
|
||||
if test_phones:
|
||||
# نه خودِ شماره و نه کد برنمیگردد؛ فقط تعداد، تا کسی که تنظیماتِ سرور
|
||||
# را ندیده هم بداند چند حسابِ تست با کدِ ثابت باز است و بتواند ببنددشان.
|
||||
out.append(
|
||||
f'{len(test_phones)} شمارهی تستِ ورود با کدِ ثابت تعریف شده (TEST_LOGIN_PHONES). '
|
||||
'برای توسعه لازم است؛ پیش از تحویلِ نهایی به کاربرانِ واقعی حذفشان کنید.'
|
||||
)
|
||||
backend = settings.CACHES['default']['BACKEND']
|
||||
if not settings.DEBUG and 'LocMem' in backend:
|
||||
out.append('کش محلیِ هر پروسه است؛ محدودیتِ نرخ بینِ workerها مشترک نیست.')
|
||||
|
||||
@@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/5.2/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
@@ -482,6 +483,58 @@ OTP_REQUEST_PHONE_COOLDOWN_SECONDS = 120
|
||||
# فقط وقتی DEBUG=True مقدار دارد؛ در پروداکشن None است.
|
||||
DEV_MASTER_OTP = '0000' if DEBUG else None
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# شمارههای تستِ ورود (TEST_LOGIN_PHONES)
|
||||
# -----------------------------------------------------------------------
|
||||
# مشکلی که این حل میکند: با DEBUG=0 و بدونِ کاوهنگار، هیچ توسعهدهندهای
|
||||
# نمیتواند وارد API شود مگر اینکه کسی با دسترسیِ ادمین دستی یک ردیفِ OTPCode
|
||||
# بسازد. یعنی ورودِ هر عضوِ تیم به یک آدمِ خاص وابسته بود.
|
||||
#
|
||||
# این همان الگویی است که اپهای واقعی برای همین کار دارند (Firebase Auth
|
||||
# «fictional phone numbers»، Apple «demo account» برای بررسیِ App Store):
|
||||
# چند شمارهی مشخص با کدِ ثابت، که پیامک برایشان ارسال نمیشود. برخلافِ
|
||||
# DEV_MASTER_OTP این «هر شمارهای» نیست — فقط همین چند شماره، و هر کدام
|
||||
# کدِ خودش را دارد.
|
||||
#
|
||||
# TEST_LOGIN_PHONES=09120000001:834512,09120000002:110947
|
||||
#
|
||||
# قواعدی که عمداً رعایت میشوند:
|
||||
# • کد هیچوقت در پاسخِ API برنمیگردد — هر کس شماره را بداند هم نمیتواند
|
||||
# کد را از سرور بیرون بکشد.
|
||||
# • سقفِ تلاشِ ورودِ هر شماره (OTP_LOGIN_PHONE_MAX_ATTEMPTS) روی اینها هم
|
||||
# اعمال میشود، پس حدسزدنِ کد با brute-force بسته است.
|
||||
# • ورودیِ خراب باعثِ بالا نیامدنِ سرویس میشود، نه نادیده گرفته شدنِ بیصدا.
|
||||
# • /api/health وجودشان را هشدار میدهد تا روی سرور فراموش نشوند.
|
||||
#
|
||||
# ⚠️ شماره باید *دقیقاً* همان رشتهای باشد که کلاینت میفرستد. این کدبیس شماره
|
||||
# را normalize نمیکند (accounts/serializers.py فقط CharField است)، پس
|
||||
# «09121111111» و «+989121111111» دو کاربرِ جدا هستند و تطبیق رشتهای است.
|
||||
#
|
||||
# ⚠️ این حسابها حسابِ تستاند: کدِ واقعی و تصادفی بگذارید (نه 1234)، و هیچ
|
||||
# دادهی واقعیِ کاربری رویشان نگذارید.
|
||||
def _parse_test_login_phones(raw: str) -> dict[str, str]:
|
||||
pairs = {}
|
||||
for item in raw.split(','):
|
||||
item = item.strip()
|
||||
if not item:
|
||||
continue
|
||||
phone, sep, code = item.partition(':')
|
||||
phone, code = phone.strip(), code.strip()
|
||||
# سقفِ ۶ رقمِ کد دلبخواه نیست: LoginRequestSerializer فیلدِ otp را
|
||||
# max_length=6 گرفته، پس کدِ بلندتر پیش از رسیدن به اینجا ۴۰۰ میشد و
|
||||
# بهنظر میآمد کدِ تست کار نمیکند.
|
||||
if not sep or not re.fullmatch(r'\+?\d{10,15}', phone) or not re.fullmatch(r'\d{4,6}', code):
|
||||
raise ImproperlyConfigured(
|
||||
f'TEST_LOGIN_PHONES مقدارِ نامعتبر دارد: «{item}». '
|
||||
'قالبِ درست «شماره:کد» است؛ شماره ۱۰ تا ۱۵ رقم (با + اختیاری) '
|
||||
'و کد ۴ تا ۶ رقم. مثال: TEST_LOGIN_PHONES=09120000001:834512'
|
||||
)
|
||||
pairs[phone] = code
|
||||
return pairs
|
||||
|
||||
|
||||
TEST_LOGIN_PHONES = _parse_test_login_phones(os.environ.get('TEST_LOGIN_PHONES', ''))
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# OTP SMS delivery (accounts/services.py: send_otp_sms)
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user