Do not connect to the shared Redis: CACHE_URL stays empty, own Redis is an open decision

The owner decided (2026-10-07) that we do not connect to anything other teams run on the box, Redis
included. CACHE_URL was emptied on the server and our keys were removed from the shared Redis.

- settings: drop KEY_PREFIX / CACHE_KEY_PREFIX (it only existed because the Redis was shared); comments
  say CACHE_URL is intentionally empty and ResilientRedisCache is ready for our own Redis.
- config/cache.py docstring and tests updated accordingly (11 tests, still run against a local redis-server).
- CLAUDE.md: new server rule 8 (touch only our own files and service), status block, historical line
  that said "db1 is free" now says not to connect. README, operations, handbook, security doc (item 9
  reopened as an open decision) say the same.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 14:41:39 +03:30
parent c129f64180
commit d584d62904
8 changed files with 59 additions and 42 deletions

View File

@@ -88,7 +88,7 @@ python manage.py test accounts # single app
python manage.py test accounts.tests.SomeTestCase.test_something # single test
```
Local dev requires `DJANGO_DEBUG=1` (enables Swagger auth via `DEV_MASTER_OTP`/`devOtp` in responses). `DEBUG` defaults to `False`. Production also requires `DJANGO_SECRET_KEY`, `DJANGO_ALLOWED_HOSTS` (comma-separated), and ideally `CACHE_URL` (Redis). Without it the app still boots on per-process `LocMemCache` and `/api/health` warns — throttles and admin-driven cache invalidation are then per worker. On the server it points at the shared Redis, database 1 (since 2026-10-07).
Local dev requires `DJANGO_DEBUG=1` (enables Swagger auth via `DEV_MASTER_OTP`/`devOtp` in responses). `DEBUG` defaults to `False`. Production also requires `DJANGO_SECRET_KEY`, `DJANGO_ALLOWED_HOSTS` (comma-separated), and ideally `CACHE_URL` (Redis). Without it the app still boots on per-process `LocMemCache` and `/api/health` warns — throttles and admin-driven cache invalidation are then per worker. On the server it is empty on purpose (owner, 2026-10-07): the Redis on that machine belongs to other teams and we do not connect to it. Our own Redis is an open pre-launch decision; until then limits are ≈3× the configured number (3 workers).
No pytest; uses Django's built-in test runner. No linter/formatter configured.
@@ -397,6 +397,11 @@ Rules that follow from this, and are not negotiable:
7. **Disk is the standing risk.** It has been sitting at ~95% full (about 6 GB free on a
113 GB volume). Check `df -h /` before anything that writes a lot — a full disk on
this machine takes the mail server and Gitea down with us, not just our app.
8. **Touch only our own files and our own service.** Do not connect to, write into or "just
use" anything another team runs on this box — their Redis (even an empty database),
databases, queues, nginx or firewall rules. Those are requests to the server admin
(listed in the launch report), not things to do ourselves. Owner's rule, 2026-10-07, after
an agent pointed `CACHE_URL` at the shared Redis.
### Handling the server password (every agent, every time)
@@ -452,16 +457,15 @@ anyone who can read this repo can forge a token for any user.**
با کلیدِ داخلِ ریپو برای کاربرهای ۱۰، ۱۱، ۱۲ پیش از تغییر `200` و بعدش `401` گرفت. توکنِ refresh در
دیتابیس است، نه امضاشده، پس نشست‌ها نمی‌میرند و فرانت بی‌صدا توکنِ تازه می‌گیرد؛ این را با ورودِ واقعی
**نسنجیدم**.
- **`CACHE_URL=redis://127.0.0.1:6379/1`** (Redisِ مشترکِ ماشین، بدونِ رمز، `maxmemory 0`؛ دیتابیسِ ۰ مالِ
دیگران است و دست نخورد). کلیدها پیشوندِ `tadrisino-mathapp` دارند و کش `config.cache.ResilientRedisCache`
است: خطای Redis ۵۰۰ نمی‌شود، خطای ERRORِ لاگ‌شده (هر ۳۰ ثانیه یک بار) و **سقف‌های مبتنی بر کش اعمال
نمی‌شوند** (fail-open: throttleهای DRF، پنجره‌ی ساعتیِ OTP، سقفِ ورودِ ادمین، قفلِ بازسنجیِ پرداخت)،
`/api/health` ۵۰۳ می‌دهد؛ آنچه در دیتابیس شمرده می‌شود (cooldown و سقفِ روزانه‌ی پیامک، حدسِ هر کد) می‌ماند.
Timeoutِ اتصال و خواندن ۱ ثانیه. آزموده شد: ۱۱ تستِ واحد (از جمله redis-serverِ محلی که وسطِ کار می‌میرد و
بدونِ ری‌استارتِ برنامه برمی‌گردد)، تمرینِ قطعی روی خودِ سرور (پروسه‌ی جدا با کش روی پورتِ بسته: همه ۲۰۰،
۰٫۳ ثانیه برای ۷ درخواست، health ۵۰۳)، و رگبارِ ۶۰۰ درخواستی روی سایتِ زنده (۱۲۴ تا ۲۰۰، پیش‌تر ۳۶۰).
**آزموده نشد:** قطعیِ واقعیِ Redisِ مشترک (مالِ ما نیست)، و رفتارِ آن زیرِ فشارِ حافظه. برگشت: خطِ
`CACHE_URL` را در `.env` خالی کنید و ری‌استارت.
- **Redis: وصل نیست، عمداً** (تصمیمِ مالک ۲۰۲۶-۱۰-۰۷). Redisِ روی آن ماشین مالِ تیم‌های دیگر است و به آن، یا به
هیچ حافظه‌ی موقت و سرویسِ دیگرانی، وصل نمی‌شویم. `CACHE_URL` در `.env` خالی و کش `LocMemCache` است، پس
تا Redisِ اختصاصیِ خودمان نیست هر سقفِ مبتنی بر کش ≈ ۳ برابرِ عدد است (۳ worker)؛ سقف‌های دیتابیسی
(پیامک، حدسِ کد) درست‌اند. «Redisِ اختصاصی» در گزارشِ لانچ یک تصمیمِ باز است (کنارِ Postgres).
کد برای آن روز آماده است: `config.cache.ResilientRedisCache` (فقط با `CACHE_URL` فعال می‌شود) قطعیِ Redis را
۵۰۰ نمی‌کند، ERROR لاگ می‌کند، و تا برگشتنش سقف‌های مبتنی بر کش اعمال نمی‌شوند (fail-open) و `/api/health`
۵۰۳ می‌دهد؛ ۱۱ تستِ واحد دارد، از جمله redis-serverِ محلی که وسطِ کار می‌میرد و برمی‌گردد. یک بار ۲۰۲۶-۱۰-۰۷
چند ساعت به آن Redisِ مشترک وصل بود و قطع و کلیدهایمان پاک شد؛ آن‌جا آزموده شد که کش روی سرور کار می‌کند.
**آزموده نشد:** قطعیِ واقعیِ Redis، و رفتارش زیرِ فشارِ حافظه.
- **آزمونِ زنده با کاربرِ آزمایشی** (۳۲ از ۳۲ سبز؛ کاربر و داده‌اش پاک شد، شمارش‌ها با پیش از آزمون برابر):
ورود با OTP، توکنِ تازه، تمدید (توکنِ تمدیدِ قدیمی ۴۰۱)، جزیره‌ی قفل ۴۰۳، بی‌جواب ۴۰۹، ادعای کمتر از
تعدادِ سوال ۴۰۹، پایانِ جزیره و درس بعد از جواب، Idempotency-Key، و خروج. رابطِ کاربریِ فرانت در مرورگر
@@ -920,8 +924,9 @@ Still open, and deliberately not acted on:
can forge one. Fixing it means removing the key from `.env` *and* from the systemd unit
and letting settings generate one. The owner decided this is the server admin's call,
not ours. `manage.py check --deploy` flags it as `security.W009` on every release.
- `CACHE_URL` is unset, so rate limiting is per-worker (`LocMemCache`). Redis is already
running on the box at `127.0.0.1:6379`; `db0` belongs to another app, `db1` is free.
- `CACHE_URL` is unset, so rate limiting is per-worker (`LocMemCache`). (A Redis runs on the box,
but it belongs to other teams: **do not connect to it**, owner's decision 2026-10-07. The
way out is our own Redis, see the status block above.)
- The frontend is still not wired to any API. `src/api/` was merged (PR #1) but is
imported nowhere; `QuestionScreen` in `App.tsx` still renders four hardcoded
multiplication questions, and **PR #3** (`zareian/api-client-2`) is still open upstream.

View File

@@ -139,8 +139,9 @@ curl https://mathapi.tadrisino2.ir/api/health
- **بکاپ فقط روی همان ماشین است.** `deploy.sh` پیش از `migrate` از `db.sqlite3` بکاپ می‌گیرد
(۲ تای آخر)، ولی نسخه‌ای بیرون از سرور نیست. جزئیات در
[`docs/operations.md`](docs/operations.md).
- **`CACHE_URL` روی سرور ست نشده**، پس محدودیتِ نرخ بینِ workerها مشترک نیست.
Redis روی همان ماشین بالاست. `/api/health` این را هشدار می‌دهد.
- **`CACHE_URL` روی سرور خالی است، عمداً**، پس محدودیتِ نرخ بینِ workerها مشترک نیست. Redisِ روی آن ماشین
مالِ تیم‌های دیگر است و به آن وصل نمی‌شویم؛ Redisِ اختصاصیِ خودمان یک تصمیمِ باز پیش از لانچ است.
`/api/health` این را هشدار می‌دهد.
- **`KAVENEGAR_API_KEY` تنظیم نشده**، پس `POST /api/auth/otp/request` برای
شماره‌های واقعی `503` می‌دهد. راهِ ورودِ تیمِ توسعه در
[`docs/operations.md`](docs/operations.md) توضیح داده شده.

View File

@@ -1,10 +1,14 @@
"""
RedisCache که با قطعی‌ی Redis درخواست‌ها را خراب نمی‌کند، ولی بی‌صدا هم نیست.
چرا: از ۲۰۲۶-۱۰-۰۷ سقفِ نرخ، کشِ برنامه‌ی درسی، کشِ جوابِ دستیار و شمارنده‌های ورود روی Redisِ مشترکِ
ماشین‌اند. `django.core.cache.backends.redis.RedisCache` با هر خطای شبکه استثنا می‌دهد و چون
DRF برای هر درخواستِ throttleدار کش را می‌خواند، قطعیِ Redis یعنی ۵۰۰ برای تقریباً همه‌ی API.
Redis مالِ ما نیست و ما کنترلی روی بالا بودنش نداریم.
وضعیت: فقط وقتی فعال است که `CACHE_URL` ست باشد. روی سرور (۲۰۲۶-۱۰-۰۷) ست نیست و کش `LocMemCache`
است: Redisِ آن ماشین مالِ تیم‌های دیگر است و عمداً به آن وصل نمی‌شویم. این کلاس برای Redisِ اختصاصیِ
خودمان نوشته و آزموده شده است.
چرا: سقفِ نرخ، کشِ برنامه‌ی درسی، کشِ جوابِ دستیار و شمارنده‌های ورود از کش می‌خوانند.
`django.core.cache.backends.redis.RedisCache` با هر خطای شبکه استثنا می‌دهد و چون DRF برای هر
درخواستِ throttleدار کش را می‌خواند، قطعیِ Redis یعنی ۵۰۰ برای تقریباً همه‌ی API (هر هفت تستِ
`config/tests_cache_resilience.py` روی کشِ معمولی می‌شکند).
رفتار هنگامِ خطا (همه با `logger.error`، حداکثر هر ۳۰ ثانیه یک بار برای هر پروسه تا لاگ پر نشود؛
و `/api/health` در همان حال ۵۰۳ می‌دهد چون پروبِ کش شکست می‌خورد):

View File

@@ -353,12 +353,11 @@ CACHE_URL = os.environ.get('CACHE_URL', '')
if CACHE_URL:
CACHES = {
'default': {
# ResilientRedisCache: قطعیِ Redisِ مشترکِ ماشین API را ۵۰۰ نمی‌کند (config/cache.py شرح می‌دهد
# چه چیزی در عوضش اعمال نمی‌شود). KEY_PREFIX چون Redis بینِ چند اپ مشترک است و کلیدِ ما با
# هیچ کلیدِ دیگری قاطی نشود؛ مقدارش یک انتخابِ نام‌گذاری است و عوض‌کردنش فقط کشِ فعلی را خالی می‌کند.
# ResilientRedisCache: قطعیِ Redis API را ۵۰۰ نمی‌کند (config/cache.py شرح می‌دهد چه چیزی در
# عوضش اعمال نمی‌شود). روی سرور فعلاً CACHE_URL خالی است: Redisِ روی آن ماشین مالِ تیم‌های
# دیگر است و عمداً به آن وصل نمی‌شویم؛ این مسیر برای Redisِ اختصاصیِ خودمان آماده است.
'BACKEND': 'config.cache.ResilientRedisCache',
'LOCATION': CACHE_URL,
'KEY_PREFIX': os.environ.get('CACHE_KEY_PREFIX', 'tadrisino-mathapp'),
# بدونِ timeout، Redisِ گیرکرده هر درخواست را تا پایانِ timeoutِ gunicorn (۱۲۰ ثانیه) نگه می‌دارد.
'OPTIONS': {'socket_connect_timeout': 1, 'socket_timeout': 1},
}

View File

@@ -24,12 +24,9 @@ from . import cache as cache_module
BACKEND = 'config.cache.ResilientRedisCache'
def _caches(location, prefix=None):
conf = {'BACKEND': BACKEND, 'LOCATION': location,
'OPTIONS': {'socket_connect_timeout': 1, 'socket_timeout': 1}}
if prefix:
conf['KEY_PREFIX'] = prefix
return {'default': conf}
def _caches(location):
return {'default': {'BACKEND': BACKEND, 'LOCATION': location,
'OPTIONS': {'socket_connect_timeout': 1, 'socket_timeout': 1}}}
class _Quiet:
@@ -141,9 +138,9 @@ class RealRedisTests(_Quiet, TestCase):
self.proc.terminate()
self.proc.wait(timeout=5)
def test_normal_behaviour_and_key_prefix(self):
def test_normal_behaviour_and_keys_have_a_ttl(self):
import redis
with override_settings(CACHES=_caches(f'redis://127.0.0.1:{self.port}/1', prefix='tadrisino-test')):
with override_settings(CACHES=_caches(f'redis://127.0.0.1:{self.port}/1')):
c = caches['default']
c.set('k', {'a': 1}, 30)
self.assertEqual(c.get('k'), {'a': 1})
@@ -153,17 +150,16 @@ class RealRedisTests(_Quiet, TestCase):
with self.assertRaises(ValueError):
c.incr('missing')
raw = redis.Redis(host='127.0.0.1', port=self.port, db=1)
self.assertTrue(all(b'tadrisino-test' in k for k in raw.keys('*')))
self.assertEqual(redis.Redis(host='127.0.0.1', port=self.port, db=0).dbsize(), 0) # دیتابیسِ دیگران دست نخورد
self.assertEqual(redis.Redis(host='127.0.0.1', port=self.port, db=0).dbsize(), 0) # فقط دیتابیسِ خودمان
self.assertGreater(raw.ttl(raw.keys('*k')[0]), 0) # TTL گذاشته شده؛ کلیدِ بی‌انقضا نمی‌ماند
def test_throttle_is_shared_and_health_is_ok(self):
with override_settings(CACHES=_caches(f'redis://127.0.0.1:{self.port}/1', prefix='t')):
with override_settings(CACHES=_caches(f'redis://127.0.0.1:{self.port}/1')):
response = APIClient().get('/api/health')
self.assertIs(response.json()['checks']['cache']['ok'], True)
def test_redis_dying_mid_flight_and_coming_back_without_restarting_the_app(self):
with override_settings(CACHES=_caches(f'redis://127.0.0.1:{self.port}/1', prefix='t')):
with override_settings(CACHES=_caches(f'redis://127.0.0.1:{self.port}/1')):
c = caches['default']
c.set('k', 'before', 60)
self.assertEqual(c.get('k'), 'before')

View File

@@ -111,8 +111,7 @@ 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` هشدار می‌دهد. روی سرور `redis://127.0.0.1:6379/1` است (Redisِ مشترکِ ماشین). قطعیِ Redis API را ۵۰۰ نمی‌کند ولی تا برگشتنش سقف‌های مبتنی بر کش اعمال نمی‌شوند و health ۵۰۳ می‌دهد (`config/cache.py`). برگشت: خالی‌کردنِ این خط و ری‌استارت |
| `CACHE_KEY_PREFIX` | `tadrisino-mathapp`؛ پیشوندِ کلیدهای ما روی Redisِ مشترک |
| `CACHE_URL` | سرویس بالا می‌آید ولی نرخ بینِ workerها مشترک نیست؛ `/api/health` هشدار می‌دهد. **روی سرور خالی است عمداً**: Redisِ آن ماشین مالِ تیم‌های دیگر است و به آن وصل نمی‌شویم؛ بعد از ساختنِ Redisِ اختصاصیِ خودمان این را پر کنید. با آن، قطعیِ Redis API را ۵۰۰ نمی‌کند ولی تا برگشتنش سقف‌های مبتنی بر کش اعمال نمی‌شوند و health ۵۰۳ می‌دهد (`config/cache.py`) |
| `KAVENEGAR_API_KEY` | `POST /api/auth/otp/request` خطای صریحِ `503` می‌دهد، نه موفقیتِ ساختگی |
| `ZARINPAL_MERCHANT_ID` | مسیرِ پرداخت fail-closed می‌ماند (`503`) |
| `gunicorn.conf.py` (در ریپو، نه env) | gunicorn خودش از پوشه‌ی کاری می‌خواندش: workerهای `gthread` با ۸ نخ و timeoutِ ۱۲۰. بدونِ آن، سه استریمِ هم‌زمانِ دستیار کلِ API را قفل می‌کرد |

View File

@@ -1,6 +1,6 @@
# پیش از پروداکشنِ واقعی: چیزهایی که عمداً باز مانده‌اند
**آخرین بازبینی با کد: ۲۰۲۶-۱۰-۰۷ (۱۵ مهر ۱۴۰۵)؛ با سرورِ زنده: ۲۰۲۶-۱۰-۰۴ برای بقیه‌ی بندها؛ آنچه ۱۰-۰۷ دیپلو شد (بک‌اند `4c60108`، ری‌استارت ۱۳:۵۹) از بیرون سنجیده شد: کلیدِ امضا، Redis، سقفِ نرخ، سلامت.**
**آخرین بازبینی با کد: ۲۰۲۶-۱۰-۰۷ (۱۵ مهر ۱۴۰۵)؛ با سرورِ زنده: ۲۰۲۶-۱۰-۰۴ برای بقیه‌ی بندها؛ آنچه ۱۰-۰۷ دیپلو شد از بیرون و با آزمونِ زنده سنجیده شد: کلیدِ امضا، سقفِ نرخ، پایانِ جزیره/درس، سلامت.**
این پروژه تا این تاریخ در حالتِ **دمو و ارائه‌ی کارآموزی** است، نه محصولِ نهایی. چند چیز
عمداً باز گذاشته شده تا توسعه‌دهنده‌ها، فرانت‌کار و ارائه کار کنند. هر کدام برای یک سرورِ
@@ -27,6 +27,7 @@
| ۵ | Swagger، ReDoc و schemaی کاملِ API عمومی‌اند | متوسط | کد |
| ۷ | پنلِ `/admin/` روی دامنه‌ی عمومی | متوسط | nginx + کد |
| ۸ | سرور بدونِ ArvanCloud روی پورتِ ۸۰ هم جواب می‌دهد | متوسط | ادمینِ سرور |
| ۹ | کشِ مشترک بینِ workerها نیست (Redisِ اختصاصی)؛ سقف‌های مبتنی بر کش ≈ ۳ برابرند | متوسط | تصمیم + ساختِ Redis |
| ۱۱ | کدِ OTP بی‌هش در دیتابیس و در فهرستِ پنل | کم | کد |
| ۱۲ | هزینه‌ی دستیارِ AI با ساختِ شماره‌ی تازه (هر شماره ۳ سوالِ رایگان) قابلِ بالا بردن است | کم (مالی) | کد |
| ۱۴ | SQLite روی دیسکِ مشترکِ پُر؛ بکاپِ پیش از migrate خودکار است ولی بیرون از ماشین نیست | کم (داده) | زیرساخت |
@@ -115,6 +116,18 @@
درخواست دور زده می‌شود، و حفاظتِ DDoS/WAFِ آروان هم برای این مسیر وجود ندارد. سقف‌های
per-phone و per-code که در دیتابیس شمرده می‌شوند متأثر نیستند. پورت‌های ۸۰ و ۴۴۳ هر دو باز بودند.
## ۹. کشِ مشترک بینِ workerها نیست
- **چیست:** `CACHE_URL` روی سرور خالی است و کش `LocMemCache` است؛ ۳ worker هرکدام جدا می‌شمارند، پس سقف‌های
مبتنی بر کش (throttleهای DRF، پنجره‌ی ساعتیِ OTP، سقفِ ورودِ ادمین) ≈ ۳ برابرِ عددِ تنظیم‌شده‌اند و با ری‌استارت
صفر می‌شوند. سقف‌های دیتابیسی (cooldown و سقفِ روزانه‌ی پیامک، حدسِ کد) درست‌اند.
- **چرا باز مانده:** Redisِ روی ماشین مالِ تیم‌های دیگر است و تصمیمِ مالک (۲۰۲۶-۱۰-۰۷) این است که به آن، یا به
هیچ سرویسِ دیگرانی، وصل نشویم.
- **رفع:** Redisِ اختصاصیِ خودمان (نمونه‌ی جدا، فقط localhost، با رمز؛ ساختنش با مدیرِ سرور یا یک سرویسِ بیرونی)،
بعد `CACHE_URL`. کد آماده است: `config/cache.py` (قطعیِ Redis API را ۵۰۰ نمی‌کند).
- **چطور بسنجید:** `/api/health` باید `LocMemCache` نگوید و هشدارِ کش نداشته باشد؛ یک رگبارِ موازی روی یک مسیرِ
throttleدار باید حدودِ یک‌بارِ سقف (نه سه برابر) بپذیرد.
## ۱۱. کدِ OTP بی‌هش در دیتابیس
- **چیست:** `OTPCode.code` متنِ ساده است و در فهرستِ پنلِ ادمین (`OTPCodeAdmin`) دیده می‌شود.
@@ -257,7 +270,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ها مشترک می‌کند و قطعیِ Redis API را ۵۰۰ نمی‌کند (fail-open، ERROR در لاگ، health ۵۰۳؛ شرحِ کامل در `config/cache.py`) | `config/tests_cache_resilience.py` (۱۱ تست، با redis-serverِ محلی که وسطِ کار می‌میرد)؛ رگبارِ زنده: ۱۲۴ تا `200` |
| با `CACHE_URL` ست‌شده، قطعیِ Redis API را ۵۰۰ نمی‌کند (fail-open، ERROR در لاگ، health ۵۰۳؛ شرح در `config/cache.py`). الان ست نیست | `config/tests_cache_resilience.py` (۱۱ تست، با redis-serverِ محلی که وسطِ کار می‌میرد) |
| کتابخانه‌ها بدونِ آسیب‌پذیریِ شناخته‌شده (`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` |

View File

@@ -831,7 +831,7 @@ if (Math.sign(y) !== Math.sign(py)) { const r = bisect(f, px, x); if (Math.abs(f
| ۴ | دسترسیِ سرور: حسابِ مشترک با رمز و sudoِ کامل | بالا | کلیدِ SSH و حسابِ محدود — کارِ ادمینِ سرور |
| ۵ | `/api/health`، Swagger و جدولِ رتبه بی‌ورود عمومی‌اند؛ پنلِ ادمین روی دامنه‌ی عمومی | متوسط | محدود کردن با ورود یا IP |
| ۶ | سرور بی‌آروان هم روی پورتِ ۸۰ جواب می‌دهد (امکانِ جعلِ IP برای دور زدنِ سقف) | متوسط | بستنِ پورت جز برای آروان — ادمینِ سرور |
| ۷ | کشِ مشترک (Redis) تنظیم نشده؛ سقف‌های نرخ برای هر worker جداست | متوسط | `CACHE_URL` روی Redisِ موجودِ سرور |
| ۷ | کشِ مشترک (Redis) تنظیم نشده؛ سقف‌های نرخ برای هر worker جداست | متوسط | Redisِ اختصاصیِ خودمان (به Redisِ مشترکِ ماشین وصل نمی‌شویم؛ تصمیمِ باز) |
| ۸ | کدِ پیامکی و refresh token بی‌هش در دیتابیس؛ توکن در `localStorage` ِ مرورگر | کم | هش کردن؛ کوکیِ httpOnly |
| ۹ | آزمونِ جزیره قفل بودنِ جزیره را نمی‌سنجد (می‌شود جزیره‌ی جلوتر را با API تمام کرد) | کم | بررسیِ جزیره‌ی قبلی در `NodeQuizCompleteView` |
@@ -936,7 +936,7 @@ fetch ← `npm ci` ← بررسیِ نوع‌ها ← build در `dist.next` ←
| # | گلوگاه | راه‌حلِ ساده‌ی بعدی | نیاز |
|---|---|---|---|
| ۱ | SQLite فقط یک نویسنده دارد | Postgres (همین حالا روی سرور هست)؛ تغییرِ تنظیمات + انتقالِ داده؛ حالتِ `IMMEDIATE` برداشته می‌شود و قفلِ ردیف واقعی می‌شود. | ادمینِ سرور |
| ۲ | کش و سقفِ نرخِ جدا برای هر worker | `CACHE_URL` روی Redisِ موجود (db1 آزاد است). | یک خط در `.env` |
| ۲ | کش و سقفِ نرخِ جدا برای هر worker | Redisِ اختصاصیِ خودمان، بعد `CACHE_URL`. Redisِ موجودِ ماشین مالِ دیگران است و وصل نمی‌شویم. | تصمیمِ باز + ساختِ Redis |
| ۳ | جوابِ دستیار نخ را نگه می‌دارد | صفِ کار (مثلاً Celery، که RabbitMQ و Redis‌اش روی سرور هست) یا ویوی async برای `/api/ai/ask`. | کد |
| ۴ | یک سرور برای همه | چند نسخه‌ی gunicorn پشتِ nginx؛ فرانت از CDNِ آروان. | زیرساخت |
| ۵ | هزینه‌ی دستیار با کاربرِ زیاد | کش، سقفِ روزانه (هست)، و سنجشِ مدلِ ارزان‌تر روی همان ابزارِ `rag_model_benchmark`. | سنجش |
@@ -989,7 +989,7 @@ fetch ← `npm ci` ← بررسیِ نوع‌ها ← build در `dist.next` ←
1. بستنِ موارد ۱ تا ۳ِ امنیت و پاک‌سازیِ داده‌ی دمو.
2. بکاپِ خودکارِ دیتابیس، بیرون از همان سرور.
3. یک پرداختِ واقعیِ کامل با زرین‌پال.
4. `CACHE_URL` (Redis) و بعد Postgres.
4. Redisِ اختصاصی (`CACHE_URL`) و بعد Postgres.
5. صفِ کار برای دستیار؛ سنجشِ شکل روی نمونه‌ی بزرگ‌تر.
6. تعریفِ سه بسته در پنلِ کافه‌بازار و اپِ اندرویدی با Poolakey.