Commit Graph

12 Commits

Author SHA1 Message Date
7d6abab15c document the twenty environment variables the code reads but never mentioned
A mechanical cross-check of os.environ.get() in settings against .env.example
found twenty variables the app reads that were written down nowhere. Most have
harmless defaults, but three would have cost someone real time:

- NUM_PROXIES decides how many X-Forwarded-For hops are trusted, and guest rate
  limiting is keyed on the resulting IP. Set too high, a client can forge the
  header and walk past the limit; too low and everyone collapses onto the proxy's
  own IP. CLAUDE.md warns about it; the file an operator actually edits did not.
- KAVENEGAR_SENDER is read alongside the API key, so someone configuring SMS from
  the README alone would have set only half of it.
- INTERNAL_SERVICE_KEY and PAYMENT_WEBHOOK_SECRET are empty under DEBUG=0 by
  design, which quietly keeps the ingest and webhook routes shut. That is the
  right behaviour and exactly the kind of thing that reads as a bug when it is
  not written down.

The whole Cafe Bazaar block was undocumented too.

Three variables in .env.example are not read by settings and were left alone:
GRAFANA_ADMIN_PASSWORD is consumed by docker-compose, TADRISINO_SERVICE by
deploy.sh, and OPENAI_API_KEY_OPENAI_DIRECT is a documented parking spot.

CLAUDE.md's current-state section was a day stale (458 tests, now 483) and said
nothing about the documentation restructure, the RTL docs shell, the test-login
allowlist or the frontend test account, so anyone picking this up would have
rediscovered all four. It now also records that backups are still manual and
that sqlite3 is not installed on the server.

Also verified mechanically, with no findings: no docstring references an
endpoint absent from the OpenAPI schema, no docstring names a setting or
constant that does not exist, and every internal documentation link resolves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 02:11:29 +03:30
5aa10eb7b7 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>
2026-09-23 00:52:57 +03:30
66364e0e92 fix the campaigns concurrency test by fixing what it was catching
ExecuteCampaignRealConcurrencyTests had been written off as a known flake
for weeks — about 45% red on a full-suite run. It was not timing noise.
It was reporting a real defect, and the defect is in production code.

execute_campaign looped with .iterator(), which keeps the read cursor on
the user table open for the whole loop — a loop that writes a
CampaignDelivery per user and, on the sms branch, makes a network call to
Kavenegar in between. On SQLite that open cursor locks the table against
any concurrent writer, and the lock is not released until the loop ends.
So _retry_on_sqlite_lock could never recover from it no matter how long
it waited: widening its window from 0.5s to 2s (also done here, since the
linear backoff was too tight regardless) moved the failure rate not at
all on its own.

Both branches now materialise the ids before the first write. After the
change: 20 consecutive runs of the test itself and 3 full-suite runs, all
green.

This is not only a test fix. While we are on SQLite the same pattern
produces the same lock in production, where it surfaces as "database is
locked" on unrelated requests that happen to land during a campaign run.

Also in this pass
-----------------
- CLAUDE.md carried three sections each claiming to be the current state,
  with three different question counts (1,441 / 1,783 / reality 2,463).
  There is now one current-state section and the older two are labelled
  as history, so whoever picks this up next is not reading stale numbers
  as fact. Three other figures that had drifted are corrected against the
  database: the verdict split, the figure-gap share (881/~42% is now
  508/~14% after the five books were re-read), and the node coverage.
- Dropped four genuinely unused imports. The remaining pyflakes hits are
  the signal registrations in apps.py, which are imported for effect.
- .env.example suggested gpt-4o for vision; gpt-4.1-mini measured both
  cheaper and more accurate and is what the server actually runs.

Audited all 2,463 questions across every grade: no question with the
wrong number of options, none with zero or two correct answers, no empty
or duplicated option, no orphan lesson or node, no duplicate question
inside a lesson.

458 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 04:43:44 +03:30
0c374fed99 default the env template to DEBUG off
`.env.example` روی هر دو ماشین کپی می‌شود — لپ‌تاپِ توسعه‌دهنده و سرور — و
مقدارش 1 بود. همان فایل روی سرورِ عمومی نشست و ماه‌ها آنجا ماند، با کدِ
ورودِ 0000 باز برای هر شماره‌ای.

پیش‌فرض باید حالتی باشد که فراموش کردنش بی‌خطر بماند: 0 روی لپ‌تاپ فقط یعنی
خطاها خلاصه‌ترند، ولی 1 روی سرور یعنی هر کسی می‌تواند به‌جای هر کاربری وارد
شود. هزینهٔ این دو اشتباه قابلِ مقایسه نیست.

DEBUG جای دیگری هم تعریف نشده: `DJANGO_DEBUG` فقط از محیط خوانده می‌شود و
`.env` در gitignore است، پس هیچ کامیتی حالتِ هیچ ماشینی را عوض نمی‌کند.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 17:58:33 +03:30
a3a47b459d stop an existing .env from cancelling the defaults meant to replace it
پیش‌فرض‌های دیروز روی یک سرورِ تازه کار می‌کردند ولی روی سرورِ واقعی نه، و
تستِ با .env واقعی نشانش داد: هر دو حالتِ شایع خنثی‌شان می‌کرد.

مقدارِ موجود ولی ناقص: .env سرور از قبل
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1 دارد. چون مقدار خالی نبود، کاملاً
جایگزینِ پیش‌فرض می‌شد و دامنه‌ی api.math.tadrisino2.ir باز هم غایب می‌ماند —
یعنی همان «Disallowed Host» قبلی، و باز هم ویرایشِ دستیِ فایل. حالا
دامنه‌های خودمان به مقدارِ محیطی اضافه می‌شوند نه جایگزینش؛ همین برای
CSRF_TRUSTED_ORIGINS و CORS_ALLOWED_ORIGINS هم اعمال شد.

مقدارِ موجود ولی خالی: PUBLIC_BASE_URL در .env هست و خالی است، و
os.environ.get فقط وقتی کلید *نباشد* پیش‌فرض می‌دهد. نتیجه‌اش paymentUrl
نسبی و callbackِ خالیِ زرین‌پال بود — خرابی‌ای که فقط موقعِ اولین پرداختِ
واقعی معلوم می‌شود. حالا مقدارِ خالی هم به پیش‌فرض می‌افتد.

این سه‌تا همان‌هایی‌اند که تا امروز مسئولِ سرور باید دستی به .env اضافه
می‌کرد و چون در .env.example هم نبودند، هر بار از نو کشف می‌شدند. حالا در
همان فایل مستند شده‌اند و تست نگهبانشان است.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 00:00:35 +03:30
e5d2427560 keep one env example instead of two
دو فایلِ نمونه یعنی دو جا که باید هم‌زمان به‌روز بمانند، و همان چیزی بود که
قبلاً باعث شد نمونه عقب بماند و کسی که سرور را بالا می‌آورد نداند چه بنویسد.
حالا یک فایل، هم برای ماشینِ محلی هم برای سرور.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 21:15:42 +03:30
092fcf13f2 add CSRF_TRUSTED_ORIGINS setting for reverse-proxied domains
Admin login (and any POST form) fails with "Origin checking failed"
behind a proxy that terminates TLS, since Django had no trusted-origins
list configured at all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 22:26:46 +03:30
b5476e1c5d connect Zarinpal as the real payment gateway
payment.services.call_payment_gateway was a stub returning a fake
payment.gateway/pay/{id} URL. It's replaced with a real Zarinpal
integration (payment/zarinpal.py, Payment API v4):

- create_order/CreateOrderView still never call Zarinpal directly —
  paymentUrl points at our own GET /api/payment/orders/{id}/pay
  (PaymentRedirectView), which requests the real Zarinpal authority on
  first visit and redirects to StartPay. This keeps order creation from
  depending on Zarinpal's uptime/latency, and means none of the existing
  purchase/idempotency tests needed to change.
- GET /api/payment/zarinpal/callback (ZarinpalCallbackView) is where
  Zarinpal redirects the user back to; it always re-verifies
  server-to-server via zarinpal.verify_payment before marking an order
  paid, so a forged Status=OK in the query string alone does nothing.
- zarinpal.py fails closed (503 / invalid) when ZARINPAL_MERCHANT_ID
  isn't configured, matching payment/bazaar.py's existing convention —
  never a fake successful payment.

Adds Order.gateway_authority, PUBLIC_BASE_URL/ZARINPAL_* settings, and
a payment_pay throttle scope.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 13:27:13 +03:30
4839123698 remove dead RAG tutor module and stale config notes
rag/services/tutor.py
  - deleted: answer_question() had no callers (AskAIView calls
    retrieve_context + generate_answer directly) and carried an unused
    import.
rag/services/config.py
  - docstring: drop the claim that the lesson-filter key is settings-driven
    (it is auto-selected per subject in known_collections.py).
.env.example
  - drop the leftover note about the removed RAG_QDRANT_LESSON_FILTER_KEY.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 13:02:43 +03:30
eeb6bd6138 scale prep — persistent DB connections, read throttle, prod renderer, auth-cache knob
settings.py
  - DATABASES.default.CONN_MAX_AGE from DB_CONN_MAX_AGE env (default 60);
    harmless on SQLite, saves a TCP+auth handshake per request on Postgres.
  - DEFAULT_RENDERER_CLASSES: BrowsableAPIRenderer only when DEBUG — no
    HTML API browser or its overhead in production.
  - DEFAULT_THROTTLE_RATES['read'] = READ_THROTTLE_RATE env (default
    120/min): a cheap ceiling for the hot GET endpoints.
  - AUTH_USER_CACHE_TTL env (default 0 = off): TTL for the per-request
    user-row cache added in a later commit; 0 keeps current behaviour.
.env.example
  - document DB_CONN_MAX_AGE / AUTH_USER_CACHE_TTL / READ_THROTTLE_RATE.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-30 08:44:50 +03:30
ff47c8d4ba register rag app, RAG provider settings, autoload .env
settings.py
  - load .env via python-dotenv (override=False) so local runs and the RAG
    keys work without `export`; production/docker env still wins.
  - add 'rag' to INSTALLED_APPS.
  - new RAG / AI provider block: OPENAI_API_KEY / OPENAI_BASE_URL (GapGPT)
    / OPENAI_CHAT_MODEL / OPENAI_VISION_MODEL / OPENAI_EMBEDDING_MODEL /
    OPENAI_TIMEOUT_SECONDS, QDRANT_URL / QDRANT_API_KEY,
    RAG_COLLECTION_NAME_TEMPLATE / RAG_GRADE_TITLE_OVERRIDES /
    RAG_RETRIEVAL_TOP_K / RAG_FORCE_MOCK / RAG_ANSWER_CACHE_TTL.
  - add 'rag_ingest' to DEFAULT_THROTTLE_RATES.
  - under `manage.py test`: force RAG_FORCE_MOCK on and RAG_ANSWER_CACHE_TTL
    to 0 so no test hits OpenAI/Qdrant or a warm cache.
urls.py
  - mount rag.urls under /api/.
requirements.txt
  - python-dotenv; openai, qdrant-client; pymupdf / pdf2image / pillow
    (optional, only for PDF ingestion).
.env.example
  - document the OPENAI_* / QDRANT_* / RAG_* keys, the discovered Qdrant
    collection names, and the parked OPENAI_API_KEY_OPENAI_DIRECT slot
    (no provider-switch logic by design).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 02:30:12 +03:30
80298b47b1 throttling, abuse-protection & logging/monitoring pass 2026-08-23 23:44:00 +03:30