diff --git a/CLAUDE.md b/CLAUDE.md index 065eb24..3479e52 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -64,8 +64,57 @@ API docs: Swagger `/api/docs/`, Redoc `/api/redoc/`, admin `/admin/`. **Payment (Zarinpal)**: `create_order`/`CreateOrderView` never call Zarinpal directly — `paymentUrl` points at our own `GET /api/payment/orders/{id}/pay` (`PaymentRedirectView`), which calls Zarinpal's PaymentRequest API on first visit (storing `Order.gateway_authority`) and redirects to StartPay; this keeps order creation from depending on Zarinpal's uptime. Zarinpal redirects the user back to `GET /api/payment/zarinpal/callback` (`ZarinpalCallbackView`), which always re-verifies server-to-server via `payment/zarinpal.py::verify_payment` before marking an order paid — a forged `Status=OK` in the query string alone does nothing. Both `payment/zarinpal.py` and `payment/bazaar.py` fail closed (503 / invalid) when their credentials aren't configured, never a fake success. +## Frontend ↔ backend (read before wiring the two together) + +The app is two repos and two domains: the React frontend lives in +`Tadrisino/MathApp_frontend` (deployed at `math.tadrisino2.ir`, owned by another +developer) and this backend serves `api.math.tadrisino2.ir`. Our typed client for them +is `src/api/` inside their repo, delivered by pull request — we have read-only Gitea +access there, so changes go via a fork + PR, never a direct push. + +**The one trap that keeps biting.** Their `App.tsx` calls relative paths +(`fetch('/api/auth/login')`). Those resolve against the *frontend* origin, where nginx +answers any unknown path with `index.html` — so the response is `200` with an HTML body +and the code dies on `res.json()` with `Unexpected token '<'`. Verify with +`curl https://math.tadrisino2.ir/api/grades`: HTTP 200, HTML. Two fixes exist, both +shipped in PR #3: a `vite` dev proxy (dev works unchanged) and an exported `apiUrl()` +for builds. **The better long-term fix is infrastructure, not code** — proxy `/api` on +the frontend domain to this backend, which makes everything same-origin and removes CORS +entirely. That needs the server admin, not us. + +**Order matters when bringing the stack up:** backend first, then frontend. If the +frontend is tested while `api.math.tadrisino2.ir` is down, every screen fails and it +looks like the client is broken. + +**Never hand-write the client from memory.** Three of fifteen calls were silently wrong +the first time. Dump the real contract with +`python manage.py spectacular --file schema.yaml --format openapi`, then drive the actual +client module against a running server (`runserver` on a spare port + `npx tsx`) before +sending anything. `content/tests_frontend_contract.py` is the backend-side guard. + +**Content the frontend expects but nobody has filled in yet:** `Unit.blurb`, +`Plan.features`/`badge`, `Lesson.example`/`steps`. All optional, all editable from the +admin panel. `Node.icon`/`color` fall back to a deterministic palette so cards are never +colourless. + +## Operations + +`./deploy.sh` is the whole release: pull, install, migrate, collectstatic, restart, then +it polls `/api/health` and prints the likely cause if the service does not come back. +Settings boot with working defaults (hosts, CSRF, CORS, secret key, cache), so a stale or +partial `.env` on the server no longer breaks startup — env values are *merged with* +those defaults, not replaced by them. + +`GET /api/health` is the outside view: database, unapplied migrations, cache, content +counts, security warnings, and recent traffic. It is how you check whether anyone has +actually connected without asking a human. + ## Known gaps (per README pre-deploy checklist) - Still on SQLite, not Postgres. - Throttle rates need periodic review against real traffic. - `DEV_MASTER_OTP` (`0000`) must stay gated behind `DEBUG` — never enabled in prod. +- The server runs with `DJANGO_DEBUG=1` on a public domain, so OTP `0000` currently logs + anyone in as anyone. Harmless until real users exist; `/api/health` warns about it. +- No video content exists yet, though `Lesson.kind`/`video_url` can express it. +- Question generation and textbook ingest are blocked on AI API credit.