diff --git a/CLAUDE.md b/CLAUDE.md index f2950c3..f59cc4e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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. diff --git a/README.md b/README.md index 0b0c320..32614cd 100644 --- a/README.md +++ b/README.md @@ -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) توضیح داده شده. diff --git a/config/cache.py b/config/cache.py index d8be5a6..488fa2c 100644 --- a/config/cache.py +++ b/config/cache.py @@ -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` در همان حال ۵۰۳ می‌دهد چون پروبِ کش شکست می‌خورد): diff --git a/config/settings.py b/config/settings.py index 8156ac6..da6efa4 100644 --- a/config/settings.py +++ b/config/settings.py @@ -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}, } diff --git a/config/tests_cache_resilience.py b/config/tests_cache_resilience.py index d4220b6..d8ea2b6 100644 --- a/config/tests_cache_resilience.py +++ b/config/tests_cache_resilience.py @@ -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') diff --git a/docs/operations.md b/docs/operations.md index 015705e..7519711 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -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 را قفل می‌کرد | diff --git a/docs/security-before-production.md b/docs/security-before-production.md index f0f11e7..06ac333 100644 --- a/docs/security-before-production.md +++ b/docs/security-before-production.md @@ -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` | diff --git a/docs/technical-handbook.md b/docs/technical-handbook.md index 5c19b47..b1cfb2e 100644 --- a/docs/technical-handbook.md +++ b/docs/technical-handbook.md @@ -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.