diff --git a/CLAUDE.md b/CLAUDE.md index 5265fc9..f2950c3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -441,7 +441,7 @@ preferred setup, because there is then no key to leak. **Never let the server ru `django-insecure-…` fallback from `config/settings.py`: that string is committed here, so anyone who can read this repo can forge a token for any user.** -## وضعیت ۲۰۲۶-۱۰-۰۷ — روی سرور (`4c60108`، ری‌استارت ۱۳:۵۹) +## وضعیت ۲۰۲۶-۱۰-۰۷ — روی سرور (`2779270`، ری‌استارت ۱۴:۱۶) بکاپ‌های همان روز در `.deploy-backups/`: `env-before-secretkey-redis-20261007-135850`، `unit-gunicorn-mathapp-before-20261007-135850.service`، `db-before-deploy-20261007-13*.sqlite3`. @@ -453,9 +453,19 @@ anyone who can read this repo can forge a token for any user.** دیتابیس است، نه امضاشده، پس نشست‌ها نمی‌میرند و فرانت بی‌صدا توکنِ تازه می‌گیرد؛ این را با ورودِ واقعی **نسنجیدم**. - **`CACHE_URL=redis://127.0.0.1:6379/1`** (Redisِ مشترکِ ماشین، بدونِ رمز، `maxmemory 0`؛ دیتابیسِ ۰ مالِ - دیگران است و دست نخورد). سقفِ نرخ حالا مشترک است: رگبارِ ۶۰۰ درخواستی ۱۲۸ تا `200` داد (پیش از آن - ۳۶۰ = ۳ worker × ۱۲۰). ⚠️ اگر این Redis بخوابد، هر مسیرِ throttleدار خطا می‌دهد؛ برگشت: خطِ `CACHE_URL` را - از `.env` خالی کنید و ری‌استارت. + دیگران است و دست نخورد). کلیدها پیشوندِ `tadrisino-mathapp` دارند و کش `config.cache.ResilientRedisCache` + است: خطای Redis ۵۰۰ نمی‌شود، خطای ERRORِ لاگ‌شده (هر ۳۰ ثانیه یک بار) و **سقف‌های مبتنی بر کش اعمال + نمی‌شوند** (fail-open: throttleهای DRF، پنجره‌ی ساعتیِ OTP، سقفِ ورودِ ادمین، قفلِ بازسنجیِ پرداخت)، + `/api/health` ۵۰۳ می‌دهد؛ آنچه در دیتابیس شمرده می‌شود (cooldown و سقفِ روزانه‌ی پیامک، حدسِ هر کد) می‌ماند. + Timeoutِ اتصال و خواندن ۱ ثانیه. آزموده شد: ۱۱ تستِ واحد (از جمله redis-serverِ محلی که وسطِ کار می‌میرد و + بدونِ ری‌استارتِ برنامه برمی‌گردد)، تمرینِ قطعی روی خودِ سرور (پروسه‌ی جدا با کش روی پورتِ بسته: همه ۲۰۰، + ۰٫۳ ثانیه برای ۷ درخواست، health ۵۰۳)، و رگبارِ ۶۰۰ درخواستی روی سایتِ زنده (۱۲۴ تا ۲۰۰، پیش‌تر ۳۶۰). + **آزموده نشد:** قطعیِ واقعیِ Redisِ مشترک (مالِ ما نیست)، و رفتارِ آن زیرِ فشارِ حافظه. برگشت: خطِ + `CACHE_URL` را در `.env` خالی کنید و ری‌استارت. +- **آزمونِ زنده با کاربرِ آزمایشی** (۳۲ از ۳۲ سبز؛ کاربر و داده‌اش پاک شد، شمارش‌ها با پیش از آزمون برابر): + ورود با OTP، توکنِ تازه، تمدید (توکنِ تمدیدِ قدیمی ۴۰۱)، جزیره‌ی قفل ۴۰۳، بی‌جواب ۴۰۹، ادعای کمتر از + تعدادِ سوال ۴۰۹، پایانِ جزیره و درس بعد از جواب، Idempotency-Key، و خروج. رابطِ کاربریِ فرانت در مرورگر + رانده **نشد** (همان فراخوانی‌ها با API زده شد). - پایانِ درس/جزیره بی‌جواب و روی جزیره‌ی قفل پذیرفته نمی‌شود، ۱۵ مسیر سقفِ نرخ گرفتند، `deploy.sh` پیش از `migrate` بکاپ می‌گیرد و سلامت را از سوکتِ gunicorn می‌سنجد. - نسخه‌ی کتابخانه‌های `requirements.txt` قفل شد (نسخه‌های روی سرور). diff --git a/docs/operations.md b/docs/operations.md index 309de12..015705e 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -111,7 +111,8 @@ curl https://mathapi.tadrisino2.ir/api/health | `DJANGO_SECRET_KEY` | یک کلیدِ تصادفیِ قوی ساخته و در `.django_secret_key` (مجوز `0600`) ذخیره می‌شود | | `PUBLIC_BASE_URL` · `FRONTEND_URL` | دامنه‌ی فعلی (`mathapi.tadrisino2.ir` / `math.tadrisino2.ir`). بقیه‌ی تنظیم‌های دامنه از همین دو ساخته می‌شوند؛ بخشِ «عوض کردنِ دامنه» | | `DJANGO_ALLOWED_HOSTS` · `CSRF_TRUSTED_ORIGINS` · `CORS_ALLOWED_ORIGINS` | از دو آدرسِ بالا ساخته می‌شوند؛ مقدارِ env فقط دامنه‌ی *اضافه* است | -| `CACHE_URL` | سرویس بالا می‌آید ولی نرخ بینِ workerها مشترک نیست؛ `/api/health` هشدار می‌دهد | +| `CACHE_URL` | سرویس بالا می‌آید ولی نرخ بینِ workerها مشترک نیست؛ `/api/health` هشدار می‌دهد. روی سرور `redis://127.0.0.1:6379/1` است (Redisِ مشترکِ ماشین). قطعیِ Redis API را ۵۰۰ نمی‌کند ولی تا برگشتنش سقف‌های مبتنی بر کش اعمال نمی‌شوند و health ۵۰۳ می‌دهد (`config/cache.py`). برگشت: خالی‌کردنِ این خط و ری‌استارت | +| `CACHE_KEY_PREFIX` | `tadrisino-mathapp`؛ پیشوندِ کلیدهای ما روی Redisِ مشترک | | `KAVENEGAR_API_KEY` | `POST /api/auth/otp/request` خطای صریحِ `503` می‌دهد، نه موفقیتِ ساختگی | | `ZARINPAL_MERCHANT_ID` | مسیرِ پرداخت fail-closed می‌ماند (`503`) | | `gunicorn.conf.py` (در ریپو، نه env) | gunicorn خودش از پوشه‌ی کاری می‌خواندش: workerهای `gthread` با ۸ نخ و timeoutِ ۱۲۰. بدونِ آن، سه استریمِ هم‌زمانِ دستیار کلِ API را قفل می‌کرد | diff --git a/docs/security-before-production.md b/docs/security-before-production.md index ec92287..f0f11e7 100644 --- a/docs/security-before-production.md +++ b/docs/security-before-production.md @@ -257,7 +257,7 @@ | ساختنِ دستیِ `OTPCode` و `Session` در پنل فقط برای ابرکاربر (کارمندِ عادی ۴۰۳) | `accounts/tests_security_review.py` (`StaffCannotMintLoginsTests`) | | پایانِ درس و پایانِ آزمونِ جزیره جوابِ ثبت‌شده به همه‌ی سوال‌ها می‌خواهند (۴۰۹ `answers_missing`)، روی جزیره‌ی قفل ۴۰۳ `node_locked` می‌دهند (همان قاعده‌ی نقشه)، جزیره یا درسِ بی‌سوال چیزی برای جواب‌دادن ندارد و اگر باز است بی‌امتیاز رد می‌شود (قفل بودنش هنوز چک می‌شود)، و تعدادِ سوالِ آزمون را کلاینت نمی‌تواند کمتر از آزمونِ استانداردِ ۴ تایی بگوید (ستاره‌ی دروغین) | `content/tests_finish_requires_answers.py`، `content/tests_node_completion.py` | | کلیدِ امضای توکن تصادفی و فقط روی سرور است (`.django_secret_key`، ۶۰۰)؛ نه در `.env`، نه در یونیت؛ توکنِ ساخته‌شده با کلیدِ داخلِ ریپو `401` می‌گیرد؛ `DJANGO_DEBUG` هم در یونیت نیست | دستی (۲۰۲۶-۱۰-۰۷): توکنِ جعلی `200` ← `401`؛ تستِ واحد ندارد | -| کشِ مشترک (Redis، دیتابیسِ ۱) سقفِ نرخ را بینِ workerها مشترک می‌کند | دستی (۲۰۲۶-۱۰-۰۷): رگبارِ ۶۰۰ درخواستی ۱۲۸ تا `200` (پیش‌تر ۳۶۰)؛ تستِ واحد ندارد | +| کشِ مشترک (Redis، دیتابیسِ ۱) سقفِ نرخ را بینِ workerها مشترک می‌کند و قطعیِ Redis API را ۵۰۰ نمی‌کند (fail-open، ERROR در لاگ، health ۵۰۳؛ شرحِ کامل در `config/cache.py`) | `config/tests_cache_resilience.py` (۱۱ تست، با redis-serverِ محلی که وسطِ کار می‌میرد)؛ رگبارِ زنده: ۱۲۴ تا `200` | | کتابخانه‌ها بدونِ آسیب‌پذیریِ شناخته‌شده (`urllib3>=2.8.0`، `python-dotenv 1.2.2`) | پایگاهِ OSV روی `pip freeze` | | actionهای لغو/بازپرداختِ سفارش، علامتِ «بررسی‌شده» و لینکِ بازار اجازه‌ی تغییر می‌خواهند؛ کارمندِ فقط‌خواندنی (Supervisor) اجرایشان نمی‌کند | `payment/tests_security_review.py`، `content/tests_security_review.py` | | ورودِ فایلِ سوال/برنامه‌ی درسی اجازه‌ی افزودن (و برای جایگزینی، حذف) می‌خواهد؛ مرورگرِ محتوا اجازه‌ی دیدنِ سوال | `content/tests_security_review.py` |