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:
31
CLAUDE.md
31
CLAUDE.md
@@ -88,7 +88,7 @@ python manage.py test accounts # single app
|
|||||||
python manage.py test accounts.tests.SomeTestCase.test_something # single test
|
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.
|
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
|
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
|
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.
|
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)
|
### 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 در
|
با کلیدِ داخلِ ریپو برای کاربرهای ۱۰، ۱۱، ۱۲ پیش از تغییر `200` و بعدش `401` گرفت. توکنِ refresh در
|
||||||
دیتابیس است، نه امضاشده، پس نشستها نمیمیرند و فرانت بیصدا توکنِ تازه میگیرد؛ این را با ورودِ واقعی
|
دیتابیس است، نه امضاشده، پس نشستها نمیمیرند و فرانت بیصدا توکنِ تازه میگیرد؛ این را با ورودِ واقعی
|
||||||
**نسنجیدم**.
|
**نسنجیدم**.
|
||||||
- **`CACHE_URL=redis://127.0.0.1:6379/1`** (Redisِ مشترکِ ماشین، بدونِ رمز، `maxmemory 0`؛ دیتابیسِ ۰ مالِ
|
- **Redis: وصل نیست، عمداً** (تصمیمِ مالک ۲۰۲۶-۱۰-۰۷). Redisِ روی آن ماشین مالِ تیمهای دیگر است و به آن، یا به
|
||||||
دیگران است و دست نخورد). کلیدها پیشوندِ `tadrisino-mathapp` دارند و کش `config.cache.ResilientRedisCache`
|
هیچ حافظهی موقت و سرویسِ دیگرانی، وصل نمیشویم. `CACHE_URL` در `.env` خالی و کش `LocMemCache` است، پس
|
||||||
است: خطای Redis ۵۰۰ نمیشود، خطای ERRORِ لاگشده (هر ۳۰ ثانیه یک بار) و **سقفهای مبتنی بر کش اعمال
|
تا Redisِ اختصاصیِ خودمان نیست هر سقفِ مبتنی بر کش ≈ ۳ برابرِ عدد است (۳ worker)؛ سقفهای دیتابیسی
|
||||||
نمیشوند** (fail-open: throttleهای DRF، پنجرهی ساعتیِ OTP، سقفِ ورودِ ادمین، قفلِ بازسنجیِ پرداخت)،
|
(پیامک، حدسِ کد) درستاند. «Redisِ اختصاصی» در گزارشِ لانچ یک تصمیمِ باز است (کنارِ Postgres).
|
||||||
`/api/health` ۵۰۳ میدهد؛ آنچه در دیتابیس شمرده میشود (cooldown و سقفِ روزانهی پیامک، حدسِ هر کد) میماند.
|
کد برای آن روز آماده است: `config.cache.ResilientRedisCache` (فقط با `CACHE_URL` فعال میشود) قطعیِ Redis را
|
||||||
Timeoutِ اتصال و خواندن ۱ ثانیه. آزموده شد: ۱۱ تستِ واحد (از جمله redis-serverِ محلی که وسطِ کار میمیرد و
|
۵۰۰ نمیکند، ERROR لاگ میکند، و تا برگشتنش سقفهای مبتنی بر کش اعمال نمیشوند (fail-open) و `/api/health`
|
||||||
بدونِ ریاستارتِ برنامه برمیگردد)، تمرینِ قطعی روی خودِ سرور (پروسهی جدا با کش روی پورتِ بسته: همه ۲۰۰،
|
۵۰۳ میدهد؛ ۱۱ تستِ واحد دارد، از جمله redis-serverِ محلی که وسطِ کار میمیرد و برمیگردد. یک بار ۲۰۲۶-۱۰-۰۷
|
||||||
۰٫۳ ثانیه برای ۷ درخواست، health ۵۰۳)، و رگبارِ ۶۰۰ درخواستی روی سایتِ زنده (۱۲۴ تا ۲۰۰، پیشتر ۳۶۰).
|
چند ساعت به آن Redisِ مشترک وصل بود و قطع و کلیدهایمان پاک شد؛ آنجا آزموده شد که کش روی سرور کار میکند.
|
||||||
**آزموده نشد:** قطعیِ واقعیِ Redisِ مشترک (مالِ ما نیست)، و رفتارِ آن زیرِ فشارِ حافظه. برگشت: خطِ
|
**آزموده نشد:** قطعیِ واقعیِ Redis، و رفتارش زیرِ فشارِ حافظه.
|
||||||
`CACHE_URL` را در `.env` خالی کنید و ریاستارت.
|
|
||||||
- **آزمونِ زنده با کاربرِ آزمایشی** (۳۲ از ۳۲ سبز؛ کاربر و دادهاش پاک شد، شمارشها با پیش از آزمون برابر):
|
- **آزمونِ زنده با کاربرِ آزمایشی** (۳۲ از ۳۲ سبز؛ کاربر و دادهاش پاک شد، شمارشها با پیش از آزمون برابر):
|
||||||
ورود با OTP، توکنِ تازه، تمدید (توکنِ تمدیدِ قدیمی ۴۰۱)، جزیرهی قفل ۴۰۳، بیجواب ۴۰۹، ادعای کمتر از
|
ورود با OTP، توکنِ تازه، تمدید (توکنِ تمدیدِ قدیمی ۴۰۱)، جزیرهی قفل ۴۰۳، بیجواب ۴۰۹، ادعای کمتر از
|
||||||
تعدادِ سوال ۴۰۹، پایانِ جزیره و درس بعد از جواب، Idempotency-Key، و خروج. رابطِ کاربریِ فرانت در مرورگر
|
تعدادِ سوال ۴۰۹، پایانِ جزیره و درس بعد از جواب، 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
|
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,
|
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.
|
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
|
- `CACHE_URL` is unset, so rate limiting is per-worker (`LocMemCache`). (A Redis runs on the box,
|
||||||
running on the box at `127.0.0.1:6379`; `db0` belongs to another app, `db1` is free.
|
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
|
- 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
|
imported nowhere; `QuestionScreen` in `App.tsx` still renders four hardcoded
|
||||||
multiplication questions, and **PR #3** (`zareian/api-client-2`) is still open upstream.
|
multiplication questions, and **PR #3** (`zareian/api-client-2`) is still open upstream.
|
||||||
|
|||||||
@@ -139,8 +139,9 @@ curl https://mathapi.tadrisino2.ir/api/health
|
|||||||
- **بکاپ فقط روی همان ماشین است.** `deploy.sh` پیش از `migrate` از `db.sqlite3` بکاپ میگیرد
|
- **بکاپ فقط روی همان ماشین است.** `deploy.sh` پیش از `migrate` از `db.sqlite3` بکاپ میگیرد
|
||||||
(۲ تای آخر)، ولی نسخهای بیرون از سرور نیست. جزئیات در
|
(۲ تای آخر)، ولی نسخهای بیرون از سرور نیست. جزئیات در
|
||||||
[`docs/operations.md`](docs/operations.md).
|
[`docs/operations.md`](docs/operations.md).
|
||||||
- **`CACHE_URL` روی سرور ست نشده**، پس محدودیتِ نرخ بینِ workerها مشترک نیست.
|
- **`CACHE_URL` روی سرور خالی است، عمداً**، پس محدودیتِ نرخ بینِ workerها مشترک نیست. Redisِ روی آن ماشین
|
||||||
Redis روی همان ماشین بالاست. `/api/health` این را هشدار میدهد.
|
مالِ تیمهای دیگر است و به آن وصل نمیشویم؛ Redisِ اختصاصیِ خودمان یک تصمیمِ باز پیش از لانچ است.
|
||||||
|
`/api/health` این را هشدار میدهد.
|
||||||
- **`KAVENEGAR_API_KEY` تنظیم نشده**، پس `POST /api/auth/otp/request` برای
|
- **`KAVENEGAR_API_KEY` تنظیم نشده**، پس `POST /api/auth/otp/request` برای
|
||||||
شمارههای واقعی `503` میدهد. راهِ ورودِ تیمِ توسعه در
|
شمارههای واقعی `503` میدهد. راهِ ورودِ تیمِ توسعه در
|
||||||
[`docs/operations.md`](docs/operations.md) توضیح داده شده.
|
[`docs/operations.md`](docs/operations.md) توضیح داده شده.
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
"""
|
"""
|
||||||
RedisCache که با قطعیی Redis درخواستها را خراب نمیکند، ولی بیصدا هم نیست.
|
RedisCache که با قطعیی Redis درخواستها را خراب نمیکند، ولی بیصدا هم نیست.
|
||||||
|
|
||||||
چرا: از ۲۰۲۶-۱۰-۰۷ سقفِ نرخ، کشِ برنامهی درسی، کشِ جوابِ دستیار و شمارندههای ورود روی Redisِ مشترکِ
|
وضعیت: فقط وقتی فعال است که `CACHE_URL` ست باشد. روی سرور (۲۰۲۶-۱۰-۰۷) ست نیست و کش `LocMemCache`
|
||||||
ماشیناند. `django.core.cache.backends.redis.RedisCache` با هر خطای شبکه استثنا میدهد و چون
|
است: Redisِ آن ماشین مالِ تیمهای دیگر است و عمداً به آن وصل نمیشویم. این کلاس برای Redisِ اختصاصیِ
|
||||||
DRF برای هر درخواستِ throttleدار کش را میخواند، قطعیِ Redis یعنی ۵۰۰ برای تقریباً همهی API.
|
خودمان نوشته و آزموده شده است.
|
||||||
Redis مالِ ما نیست و ما کنترلی روی بالا بودنش نداریم.
|
|
||||||
|
چرا: سقفِ نرخ، کشِ برنامهی درسی، کشِ جوابِ دستیار و شمارندههای ورود از کش میخوانند.
|
||||||
|
`django.core.cache.backends.redis.RedisCache` با هر خطای شبکه استثنا میدهد و چون DRF برای هر
|
||||||
|
درخواستِ throttleدار کش را میخواند، قطعیِ Redis یعنی ۵۰۰ برای تقریباً همهی API (هر هفت تستِ
|
||||||
|
`config/tests_cache_resilience.py` روی کشِ معمولی میشکند).
|
||||||
|
|
||||||
رفتار هنگامِ خطا (همه با `logger.error`، حداکثر هر ۳۰ ثانیه یک بار برای هر پروسه تا لاگ پر نشود؛
|
رفتار هنگامِ خطا (همه با `logger.error`، حداکثر هر ۳۰ ثانیه یک بار برای هر پروسه تا لاگ پر نشود؛
|
||||||
و `/api/health` در همان حال ۵۰۳ میدهد چون پروبِ کش شکست میخورد):
|
و `/api/health` در همان حال ۵۰۳ میدهد چون پروبِ کش شکست میخورد):
|
||||||
|
|||||||
@@ -353,12 +353,11 @@ CACHE_URL = os.environ.get('CACHE_URL', '')
|
|||||||
if CACHE_URL:
|
if CACHE_URL:
|
||||||
CACHES = {
|
CACHES = {
|
||||||
'default': {
|
'default': {
|
||||||
# ResilientRedisCache: قطعیِ Redisِ مشترکِ ماشین API را ۵۰۰ نمیکند (config/cache.py شرح میدهد
|
# ResilientRedisCache: قطعیِ Redis API را ۵۰۰ نمیکند (config/cache.py شرح میدهد چه چیزی در
|
||||||
# چه چیزی در عوضش اعمال نمیشود). KEY_PREFIX چون Redis بینِ چند اپ مشترک است و کلیدِ ما با
|
# عوضش اعمال نمیشود). روی سرور فعلاً CACHE_URL خالی است: Redisِ روی آن ماشین مالِ تیمهای
|
||||||
# هیچ کلیدِ دیگری قاطی نشود؛ مقدارش یک انتخابِ نامگذاری است و عوضکردنش فقط کشِ فعلی را خالی میکند.
|
# دیگر است و عمداً به آن وصل نمیشویم؛ این مسیر برای Redisِ اختصاصیِ خودمان آماده است.
|
||||||
'BACKEND': 'config.cache.ResilientRedisCache',
|
'BACKEND': 'config.cache.ResilientRedisCache',
|
||||||
'LOCATION': CACHE_URL,
|
'LOCATION': CACHE_URL,
|
||||||
'KEY_PREFIX': os.environ.get('CACHE_KEY_PREFIX', 'tadrisino-mathapp'),
|
|
||||||
# بدونِ timeout، Redisِ گیرکرده هر درخواست را تا پایانِ timeoutِ gunicorn (۱۲۰ ثانیه) نگه میدارد.
|
# بدونِ timeout، Redisِ گیرکرده هر درخواست را تا پایانِ timeoutِ gunicorn (۱۲۰ ثانیه) نگه میدارد.
|
||||||
'OPTIONS': {'socket_connect_timeout': 1, 'socket_timeout': 1},
|
'OPTIONS': {'socket_connect_timeout': 1, 'socket_timeout': 1},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,12 +24,9 @@ from . import cache as cache_module
|
|||||||
BACKEND = 'config.cache.ResilientRedisCache'
|
BACKEND = 'config.cache.ResilientRedisCache'
|
||||||
|
|
||||||
|
|
||||||
def _caches(location, prefix=None):
|
def _caches(location):
|
||||||
conf = {'BACKEND': BACKEND, 'LOCATION': location,
|
return {'default': {'BACKEND': BACKEND, 'LOCATION': location,
|
||||||
'OPTIONS': {'socket_connect_timeout': 1, 'socket_timeout': 1}}
|
'OPTIONS': {'socket_connect_timeout': 1, 'socket_timeout': 1}}}
|
||||||
if prefix:
|
|
||||||
conf['KEY_PREFIX'] = prefix
|
|
||||||
return {'default': conf}
|
|
||||||
|
|
||||||
|
|
||||||
class _Quiet:
|
class _Quiet:
|
||||||
@@ -141,9 +138,9 @@ class RealRedisTests(_Quiet, TestCase):
|
|||||||
self.proc.terminate()
|
self.proc.terminate()
|
||||||
self.proc.wait(timeout=5)
|
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
|
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 = caches['default']
|
||||||
c.set('k', {'a': 1}, 30)
|
c.set('k', {'a': 1}, 30)
|
||||||
self.assertEqual(c.get('k'), {'a': 1})
|
self.assertEqual(c.get('k'), {'a': 1})
|
||||||
@@ -153,17 +150,16 @@ class RealRedisTests(_Quiet, TestCase):
|
|||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
c.incr('missing')
|
c.incr('missing')
|
||||||
raw = redis.Redis(host='127.0.0.1', port=self.port, db=1)
|
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 گذاشته شده؛ کلیدِ بیانقضا نمیماند
|
self.assertGreater(raw.ttl(raw.keys('*k')[0]), 0) # TTL گذاشته شده؛ کلیدِ بیانقضا نمیماند
|
||||||
|
|
||||||
def test_throttle_is_shared_and_health_is_ok(self):
|
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')
|
response = APIClient().get('/api/health')
|
||||||
self.assertIs(response.json()['checks']['cache']['ok'], True)
|
self.assertIs(response.json()['checks']['cache']['ok'], True)
|
||||||
|
|
||||||
def test_redis_dying_mid_flight_and_coming_back_without_restarting_the_app(self):
|
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 = caches['default']
|
||||||
c.set('k', 'before', 60)
|
c.set('k', 'before', 60)
|
||||||
self.assertEqual(c.get('k'), 'before')
|
self.assertEqual(c.get('k'), 'before')
|
||||||
|
|||||||
@@ -111,8 +111,7 @@ curl https://mathapi.tadrisino2.ir/api/health
|
|||||||
| `DJANGO_SECRET_KEY` | یک کلیدِ تصادفیِ قوی ساخته و در `.django_secret_key` (مجوز `0600`) ذخیره میشود |
|
| `DJANGO_SECRET_KEY` | یک کلیدِ تصادفیِ قوی ساخته و در `.django_secret_key` (مجوز `0600`) ذخیره میشود |
|
||||||
| `PUBLIC_BASE_URL` · `FRONTEND_URL` | دامنهی فعلی (`mathapi.tadrisino2.ir` / `math.tadrisino2.ir`). بقیهی تنظیمهای دامنه از همین دو ساخته میشوند؛ بخشِ «عوض کردنِ دامنه» |
|
| `PUBLIC_BASE_URL` · `FRONTEND_URL` | دامنهی فعلی (`mathapi.tadrisino2.ir` / `math.tadrisino2.ir`). بقیهی تنظیمهای دامنه از همین دو ساخته میشوند؛ بخشِ «عوض کردنِ دامنه» |
|
||||||
| `DJANGO_ALLOWED_HOSTS` · `CSRF_TRUSTED_ORIGINS` · `CORS_ALLOWED_ORIGINS` | از دو آدرسِ بالا ساخته میشوند؛ مقدارِ env فقط دامنهی *اضافه* است |
|
| `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_URL` | سرویس بالا میآید ولی نرخ بینِ workerها مشترک نیست؛ `/api/health` هشدار میدهد. **روی سرور خالی است عمداً**: Redisِ آن ماشین مالِ تیمهای دیگر است و به آن وصل نمیشویم؛ بعد از ساختنِ Redisِ اختصاصیِ خودمان این را پر کنید. با آن، قطعیِ Redis API را ۵۰۰ نمیکند ولی تا برگشتنش سقفهای مبتنی بر کش اعمال نمیشوند و health ۵۰۳ میدهد (`config/cache.py`) |
|
||||||
| `CACHE_KEY_PREFIX` | `tadrisino-mathapp`؛ پیشوندِ کلیدهای ما روی Redisِ مشترک |
|
|
||||||
| `KAVENEGAR_API_KEY` | `POST /api/auth/otp/request` خطای صریحِ `503` میدهد، نه موفقیتِ ساختگی |
|
| `KAVENEGAR_API_KEY` | `POST /api/auth/otp/request` خطای صریحِ `503` میدهد، نه موفقیتِ ساختگی |
|
||||||
| `ZARINPAL_MERCHANT_ID` | مسیرِ پرداخت fail-closed میماند (`503`) |
|
| `ZARINPAL_MERCHANT_ID` | مسیرِ پرداخت fail-closed میماند (`503`) |
|
||||||
| `gunicorn.conf.py` (در ریپو، نه env) | gunicorn خودش از پوشهی کاری میخواندش: workerهای `gthread` با ۸ نخ و timeoutِ ۱۲۰. بدونِ آن، سه استریمِ همزمانِ دستیار کلِ API را قفل میکرد |
|
| `gunicorn.conf.py` (در ریپو، نه env) | gunicorn خودش از پوشهی کاری میخواندش: workerهای `gthread` با ۸ نخ و timeoutِ ۱۲۰. بدونِ آن، سه استریمِ همزمانِ دستیار کلِ API را قفل میکرد |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# پیش از پروداکشنِ واقعی: چیزهایی که عمداً باز ماندهاند
|
# پیش از پروداکشنِ واقعی: چیزهایی که عمداً باز ماندهاند
|
||||||
|
|
||||||
**آخرین بازبینی با کد: ۲۰۲۶-۱۰-۰۷ (۱۵ مهر ۱۴۰۵)؛ با سرورِ زنده: ۲۰۲۶-۱۰-۰۴ برای بقیهی بندها؛ آنچه ۱۰-۰۷ دیپلو شد (بکاند `4c60108`، ریاستارت ۱۳:۵۹) از بیرون سنجیده شد: کلیدِ امضا، Redis، سقفِ نرخ، سلامت.**
|
**آخرین بازبینی با کد: ۲۰۲۶-۱۰-۰۷ (۱۵ مهر ۱۴۰۵)؛ با سرورِ زنده: ۲۰۲۶-۱۰-۰۴ برای بقیهی بندها؛ آنچه ۱۰-۰۷ دیپلو شد از بیرون و با آزمونِ زنده سنجیده شد: کلیدِ امضا، سقفِ نرخ، پایانِ جزیره/درس، سلامت.**
|
||||||
|
|
||||||
این پروژه تا این تاریخ در حالتِ **دمو و ارائهی کارآموزی** است، نه محصولِ نهایی. چند چیز
|
این پروژه تا این تاریخ در حالتِ **دمو و ارائهی کارآموزی** است، نه محصولِ نهایی. چند چیز
|
||||||
عمداً باز گذاشته شده تا توسعهدهندهها، فرانتکار و ارائه کار کنند. هر کدام برای یک سرورِ
|
عمداً باز گذاشته شده تا توسعهدهندهها، فرانتکار و ارائه کار کنند. هر کدام برای یک سرورِ
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
| ۵ | Swagger، ReDoc و schemaی کاملِ API عمومیاند | متوسط | کد |
|
| ۵ | Swagger، ReDoc و schemaی کاملِ API عمومیاند | متوسط | کد |
|
||||||
| ۷ | پنلِ `/admin/` روی دامنهی عمومی | متوسط | nginx + کد |
|
| ۷ | پنلِ `/admin/` روی دامنهی عمومی | متوسط | nginx + کد |
|
||||||
| ۸ | سرور بدونِ ArvanCloud روی پورتِ ۸۰ هم جواب میدهد | متوسط | ادمینِ سرور |
|
| ۸ | سرور بدونِ ArvanCloud روی پورتِ ۸۰ هم جواب میدهد | متوسط | ادمینِ سرور |
|
||||||
|
| ۹ | کشِ مشترک بینِ workerها نیست (Redisِ اختصاصی)؛ سقفهای مبتنی بر کش ≈ ۳ برابرند | متوسط | تصمیم + ساختِ Redis |
|
||||||
| ۱۱ | کدِ OTP بیهش در دیتابیس و در فهرستِ پنل | کم | کد |
|
| ۱۱ | کدِ OTP بیهش در دیتابیس و در فهرستِ پنل | کم | کد |
|
||||||
| ۱۲ | هزینهی دستیارِ AI با ساختِ شمارهی تازه (هر شماره ۳ سوالِ رایگان) قابلِ بالا بردن است | کم (مالی) | کد |
|
| ۱۲ | هزینهی دستیارِ AI با ساختِ شمارهی تازه (هر شماره ۳ سوالِ رایگان) قابلِ بالا بردن است | کم (مالی) | کد |
|
||||||
| ۱۴ | SQLite روی دیسکِ مشترکِ پُر؛ بکاپِ پیش از migrate خودکار است ولی بیرون از ماشین نیست | کم (داده) | زیرساخت |
|
| ۱۴ | SQLite روی دیسکِ مشترکِ پُر؛ بکاپِ پیش از migrate خودکار است ولی بیرون از ماشین نیست | کم (داده) | زیرساخت |
|
||||||
@@ -115,6 +116,18 @@
|
|||||||
درخواست دور زده میشود، و حفاظتِ DDoS/WAFِ آروان هم برای این مسیر وجود ندارد. سقفهای
|
درخواست دور زده میشود، و حفاظتِ DDoS/WAFِ آروان هم برای این مسیر وجود ندارد. سقفهای
|
||||||
per-phone و per-code که در دیتابیس شمرده میشوند متأثر نیستند. پورتهای ۸۰ و ۴۴۳ هر دو باز بودند.
|
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 بیهش در دیتابیس
|
## ۱۱. کدِ OTP بیهش در دیتابیس
|
||||||
|
|
||||||
- **چیست:** `OTPCode.code` متنِ ساده است و در فهرستِ پنلِ ادمین (`OTPCodeAdmin`) دیده میشود.
|
- **چیست:** `OTPCode.code` متنِ ساده است و در فهرستِ پنلِ ادمین (`OTPCodeAdmin`) دیده میشود.
|
||||||
@@ -257,7 +270,7 @@
|
|||||||
| ساختنِ دستیِ `OTPCode` و `Session` در پنل فقط برای ابرکاربر (کارمندِ عادی ۴۰۳) | `accounts/tests_security_review.py` (`StaffCannotMintLoginsTests`) |
|
| ساختنِ دستیِ `OTPCode` و `Session` در پنل فقط برای ابرکاربر (کارمندِ عادی ۴۰۳) | `accounts/tests_security_review.py` (`StaffCannotMintLoginsTests`) |
|
||||||
| پایانِ درس و پایانِ آزمونِ جزیره جوابِ ثبتشده به همهی سوالها میخواهند (۴۰۹ `answers_missing`)، روی جزیرهی قفل ۴۰۳ `node_locked` میدهند (همان قاعدهی نقشه)، جزیره یا درسِ بیسوال چیزی برای جوابدادن ندارد و اگر باز است بیامتیاز رد میشود (قفل بودنش هنوز چک میشود)، و تعدادِ سوالِ آزمون را کلاینت نمیتواند کمتر از آزمونِ استانداردِ ۴ تایی بگوید (ستارهی دروغین) | `content/tests_finish_requires_answers.py`، `content/tests_node_completion.py` |
|
| پایانِ درس و پایانِ آزمونِ جزیره جوابِ ثبتشده به همهی سوالها میخواهند (۴۰۹ `answers_missing`)، روی جزیرهی قفل ۴۰۳ `node_locked` میدهند (همان قاعدهی نقشه)، جزیره یا درسِ بیسوال چیزی برای جوابدادن ندارد و اگر باز است بیامتیاز رد میشود (قفل بودنش هنوز چک میشود)، و تعدادِ سوالِ آزمون را کلاینت نمیتواند کمتر از آزمونِ استانداردِ ۴ تایی بگوید (ستارهی دروغین) | `content/tests_finish_requires_answers.py`، `content/tests_node_completion.py` |
|
||||||
| کلیدِ امضای توکن تصادفی و فقط روی سرور است (`.django_secret_key`، ۶۰۰)؛ نه در `.env`، نه در یونیت؛ توکنِ ساختهشده با کلیدِ داخلِ ریپو `401` میگیرد؛ `DJANGO_DEBUG` هم در یونیت نیست | دستی (۲۰۲۶-۱۰-۰۷): توکنِ جعلی `200` ← `401`؛ تستِ واحد ندارد |
|
| کلیدِ امضای توکن تصادفی و فقط روی سرور است (`.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` |
|
| کتابخانهها بدونِ آسیبپذیریِ شناختهشده (`urllib3>=2.8.0`، `python-dotenv 1.2.2`) | پایگاهِ OSV روی `pip freeze` |
|
||||||
| actionهای لغو/بازپرداختِ سفارش، علامتِ «بررسیشده» و لینکِ بازار اجازهی تغییر میخواهند؛ کارمندِ فقطخواندنی (Supervisor) اجرایشان نمیکند | `payment/tests_security_review.py`، `content/tests_security_review.py` |
|
| actionهای لغو/بازپرداختِ سفارش، علامتِ «بررسیشده» و لینکِ بازار اجازهی تغییر میخواهند؛ کارمندِ فقطخواندنی (Supervisor) اجرایشان نمیکند | `payment/tests_security_review.py`، `content/tests_security_review.py` |
|
||||||
| ورودِ فایلِ سوال/برنامهی درسی اجازهی افزودن (و برای جایگزینی، حذف) میخواهد؛ مرورگرِ محتوا اجازهی دیدنِ سوال | `content/tests_security_review.py` |
|
| ورودِ فایلِ سوال/برنامهی درسی اجازهی افزودن (و برای جایگزینی، حذف) میخواهد؛ مرورگرِ محتوا اجازهی دیدنِ سوال | `content/tests_security_review.py` |
|
||||||
|
|||||||
@@ -831,7 +831,7 @@ if (Math.sign(y) !== Math.sign(py)) { const r = bisect(f, px, x); if (Math.abs(f
|
|||||||
| ۴ | دسترسیِ سرور: حسابِ مشترک با رمز و sudoِ کامل | بالا | کلیدِ SSH و حسابِ محدود — کارِ ادمینِ سرور |
|
| ۴ | دسترسیِ سرور: حسابِ مشترک با رمز و sudoِ کامل | بالا | کلیدِ SSH و حسابِ محدود — کارِ ادمینِ سرور |
|
||||||
| ۵ | `/api/health`، Swagger و جدولِ رتبه بیورود عمومیاند؛ پنلِ ادمین روی دامنهی عمومی | متوسط | محدود کردن با ورود یا IP |
|
| ۵ | `/api/health`، Swagger و جدولِ رتبه بیورود عمومیاند؛ پنلِ ادمین روی دامنهی عمومی | متوسط | محدود کردن با ورود یا IP |
|
||||||
| ۶ | سرور بیآروان هم روی پورتِ ۸۰ جواب میدهد (امکانِ جعلِ IP برای دور زدنِ سقف) | متوسط | بستنِ پورت جز برای آروان — ادمینِ سرور |
|
| ۶ | سرور بیآروان هم روی پورتِ ۸۰ جواب میدهد (امکانِ جعلِ IP برای دور زدنِ سقف) | متوسط | بستنِ پورت جز برای آروان — ادمینِ سرور |
|
||||||
| ۷ | کشِ مشترک (Redis) تنظیم نشده؛ سقفهای نرخ برای هر worker جداست | متوسط | `CACHE_URL` روی Redisِ موجودِ سرور |
|
| ۷ | کشِ مشترک (Redis) تنظیم نشده؛ سقفهای نرخ برای هر worker جداست | متوسط | Redisِ اختصاصیِ خودمان (به Redisِ مشترکِ ماشین وصل نمیشویم؛ تصمیمِ باز) |
|
||||||
| ۸ | کدِ پیامکی و refresh token بیهش در دیتابیس؛ توکن در `localStorage` ِ مرورگر | کم | هش کردن؛ کوکیِ httpOnly |
|
| ۸ | کدِ پیامکی و refresh token بیهش در دیتابیس؛ توکن در `localStorage` ِ مرورگر | کم | هش کردن؛ کوکیِ httpOnly |
|
||||||
| ۹ | آزمونِ جزیره قفل بودنِ جزیره را نمیسنجد (میشود جزیرهی جلوتر را با API تمام کرد) | کم | بررسیِ جزیرهی قبلی در `NodeQuizCompleteView` |
|
| ۹ | آزمونِ جزیره قفل بودنِ جزیره را نمیسنجد (میشود جزیرهی جلوتر را با API تمام کرد) | کم | بررسیِ جزیرهی قبلی در `NodeQuizCompleteView` |
|
||||||
|
|
||||||
@@ -936,7 +936,7 @@ fetch ← `npm ci` ← بررسیِ نوعها ← build در `dist.next` ←
|
|||||||
| # | گلوگاه | راهحلِ سادهی بعدی | نیاز |
|
| # | گلوگاه | راهحلِ سادهی بعدی | نیاز |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| ۱ | SQLite فقط یک نویسنده دارد | Postgres (همین حالا روی سرور هست)؛ تغییرِ تنظیمات + انتقالِ داده؛ حالتِ `IMMEDIATE` برداشته میشود و قفلِ ردیف واقعی میشود. | ادمینِ سرور |
|
| ۱ | SQLite فقط یک نویسنده دارد | Postgres (همین حالا روی سرور هست)؛ تغییرِ تنظیمات + انتقالِ داده؛ حالتِ `IMMEDIATE` برداشته میشود و قفلِ ردیف واقعی میشود. | ادمینِ سرور |
|
||||||
| ۲ | کش و سقفِ نرخِ جدا برای هر worker | `CACHE_URL` روی Redisِ موجود (db1 آزاد است). | یک خط در `.env` |
|
| ۲ | کش و سقفِ نرخِ جدا برای هر worker | Redisِ اختصاصیِ خودمان، بعد `CACHE_URL`. Redisِ موجودِ ماشین مالِ دیگران است و وصل نمیشویم. | تصمیمِ باز + ساختِ Redis |
|
||||||
| ۳ | جوابِ دستیار نخ را نگه میدارد | صفِ کار (مثلاً Celery، که RabbitMQ و Redisاش روی سرور هست) یا ویوی async برای `/api/ai/ask`. | کد |
|
| ۳ | جوابِ دستیار نخ را نگه میدارد | صفِ کار (مثلاً Celery، که RabbitMQ و Redisاش روی سرور هست) یا ویوی async برای `/api/ai/ask`. | کد |
|
||||||
| ۴ | یک سرور برای همه | چند نسخهی gunicorn پشتِ nginx؛ فرانت از CDNِ آروان. | زیرساخت |
|
| ۴ | یک سرور برای همه | چند نسخهی gunicorn پشتِ nginx؛ فرانت از CDNِ آروان. | زیرساخت |
|
||||||
| ۵ | هزینهی دستیار با کاربرِ زیاد | کش، سقفِ روزانه (هست)، و سنجشِ مدلِ ارزانتر روی همان ابزارِ `rag_model_benchmark`. | سنجش |
|
| ۵ | هزینهی دستیار با کاربرِ زیاد | کش، سقفِ روزانه (هست)، و سنجشِ مدلِ ارزانتر روی همان ابزارِ `rag_model_benchmark`. | سنجش |
|
||||||
@@ -989,7 +989,7 @@ fetch ← `npm ci` ← بررسیِ نوعها ← build در `dist.next` ←
|
|||||||
1. بستنِ موارد ۱ تا ۳ِ امنیت و پاکسازیِ دادهی دمو.
|
1. بستنِ موارد ۱ تا ۳ِ امنیت و پاکسازیِ دادهی دمو.
|
||||||
2. بکاپِ خودکارِ دیتابیس، بیرون از همان سرور.
|
2. بکاپِ خودکارِ دیتابیس، بیرون از همان سرور.
|
||||||
3. یک پرداختِ واقعیِ کامل با زرینپال.
|
3. یک پرداختِ واقعیِ کامل با زرینپال.
|
||||||
4. `CACHE_URL` (Redis) و بعد Postgres.
|
4. Redisِ اختصاصی (`CACHE_URL`) و بعد Postgres.
|
||||||
5. صفِ کار برای دستیار؛ سنجشِ شکل روی نمونهی بزرگتر.
|
5. صفِ کار برای دستیار؛ سنجشِ شکل روی نمونهی بزرگتر.
|
||||||
6. تعریفِ سه بسته در پنلِ کافهبازار و اپِ اندرویدی با Poolakey.
|
6. تعریفِ سه بسته در پنلِ کافهبازار و اپِ اندرویدی با Poolakey.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user