make the domain configuration, not code: two env vars move everything

A domain change is coming (the Zarinpal terminal rejects the current callback
domain). ALLOWED_HOSTS, CSRF_TRUSTED_ORIGINS, CORS defaults, the Bazaar
redirect URI and deploy.sh's public health URL each spelled the domain out.
They now derive from PUBLIC_BASE_URL and FRONTEND_URL, which keep today's
domain as their default; env values for the three lists are still added on
top, so the old domain can stay allowed while DNS moves.

ZARINPAL_CALLBACK_URL used os.environ.get(key, default), so an empty key in
.env produced an empty callback and every payment would fail closed. It now
uses the same `.strip() or` fallback as its neighbours; the new test caught it.

config/tests_domains.py loads settings in a subprocess with a new domain and
fails if any derived value still mentions the old one. docs/operations.md
gains the step-by-step domain move (DNS, .env, Zarinpal/Bazaar panels,
frontend config.js, verification). CLAUDE.md records that the frontend is
now ours and where the rebuild stands. 624 tests green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-26 23:46:25 +03:30
parent 2f8ed50b86
commit 4b246e0c70
6 changed files with 188 additions and 84 deletions

View File

@@ -119,48 +119,37 @@ than a coding one, and never assume a step was done just because it was describe
## 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 app is two repos and two domains: the React frontend `Tadrisino/MathApp_frontend`
(deployed at `math.tadrisino2.ir`) and this backend (`api.math.tadrisino2.ir`).
**Since 2026-09-26 the owner maintains both:** the frontend developer is on leave and
`AliReza_Zareian` has `write` on the frontend repo (branch protection on its `main`
unknown — not visible without admin). Local clone: `~/Desktop/MathApp_frontend`.
**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.
**State on 2026-09-26:** the frontend was rebuilt from the single 3,071-line `App.tsx`
into a typed, modular app (branch `rebuild/v2`; its README is the map). Every item of
the old handoff list (`~/Desktop/کارهای-فرانت-تدریسینو.html`) is implemented, plus
screens for profile, streak, support, parents and invite. Verified by a headless-Chrome
walkthrough against a local backend; real SMS login and a real Zarinpal payment were not
exercised. **The live site still serves the 2026-09-21 build** that calls a relative
`/api` and gets HTML back — nothing reaches users until someone builds and deploys the
frontend, and who deploys `math.tadrisino2.ir` is not known to us yet.
**State on 2026-09-23 (checked against upstream `main`, last commit 2026-09-21):** the
screens are wired to the API (PR #4/#5 upstream), but `App.tsx` builds the client with
`env.VITE_API_URL ?? "/api"` — while their own `.env.example` and `client.ts` name the
variable `VITE_API_BASE_URL`. The deployed bundle has neither, so it calls the relative
`/api` and gets HTML back: **math.tadrisino2.ir cannot reach the backend** until either
that line falls back to `https://api.math.tadrisino2.ir/api` or nginx proxies `/api`.
Our PR #3 fixed the same trap and is still open, now likely conflicting with their
rewrite. Every path their client calls exists and was driven live against the server.
The app also generates a random `deviceId` per browser, so the seeded
`frontend-test-device` account only loads after
`localStorage.setItem("deviceId", "frontend-test-device")`.
**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.
**Domains are configuration, not code.** Backend: only `PUBLIC_BASE_URL` and
`FRONTEND_URL`; hosts, CSRF, CORS, Zarinpal callback/return, Bazaar redirect and the
`deploy.sh` health URL derive from them (`config/tests_domains.py` guards it). Frontend:
`dist/config.js` at runtime, else `VITE_API_BASE_URL` from `.env.production`. A domain
move is expected (Zarinpal `-14`); the procedure is in `docs/operations.md`
(«عوض کردنِ دامنه»).
**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.
the first time, and the rebuilt client found two more the old one had (`POST` instead
of `PATCH` on notifications, a dropped guest refresh token). Read the contract from
`python manage.py spectacular --file schema.yaml --format openapi` and check shapes
against a running server. `content/tests_frontend_contract.py` is the backend-side guard.
**Content the frontend expects but nobody has filled in yet:** `Unit.blurb`,
**Content the frontend shows 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.
admin panel.
## Operations