docs: README + casdoor-auth guide for the v1.3 changes

- README: Traefik edge (app at http://localhost), single postgres (casdoor
  DB inside), prod overlay usage, partial receipts, admin panel, dev-mode
  3-step flow, layout for infra/ and 02-casdoor-db.sql
- docs/casdoor-auth.md: new section «Stack changes in this release» (DB
  merge, Traefik, RBAC, partial receipt, calendar, upload, project filter),
  updated addresses and redirect-URI notes
This commit is contained in:
ali_mohaghegh
2026-09-01 17:28:45 +03:30
parent 8825f333a7
commit 31290ffbda
2 changed files with 91 additions and 22 deletions
+47 -17
View File
@@ -5,20 +5,26 @@ folder — no external files or sibling folders required.
| Service | Container | Tech | Exposed port | | Service | Container | Tech | Exposed port |
|---|---|---|---| |---|---|---|---|
| **postgres** | `procurement-postgres` | PostgreSQL 18 | `5433` | | **postgres** | `procurement-postgres` | PostgreSQL 18 (one instance: `procurement` + `casdoor` DBs) | `5433` |
| **minio** | `procurement-minio` | MinIO object storage (S3-compatible) | `9000` (API), `9001` (console) | | **minio** | `procurement-minio` | MinIO object storage (S3-compatible) | `9000` (API), `9001` (console) |
| **casdoor** | `procurement-casdoor` | Casdoor (OIDC identity provider) | `8080` | | **casdoor** | `procurement-casdoor` | Casdoor (OIDC identity provider) | `8080` |
| **api** | `procurement-api` | FastAPI + SQLAlchemy (asyncpg) | `8000` | | **api** | `procurement-api` | FastAPI + SQLAlchemy (asyncpg) | `8000` |
| **frontend** | `procurement-frontend` | React 19 + Vite, served by nginx | `3000` | | **frontend** | `procurement-frontend` | React 19 + Vite, served by nginx (internal) | via Traefik |
| **traefik** | `procurement-traefik` | Traefik v3 — public reverse proxy (edge) | `80` (web), `443` (websecure) |
``` ```
Browser ──► frontend :3000 (nginx SPA) Browser ──► Traefik :80 (edge)
│ │
└── /api/* proxy ──► api :8000 (FastAPI, /api/v1) ├── Host(localhost) ──► frontend (nginx SPA :80)
│ │
│ └── /api/* proxy ──► api :8000 (/api/v1)
│ │
├── postgres :5432 (procurement DB) ├── Host(api.localhost) ──► api :8000
├── minio :9000 (invoice/proforma files, bucket `procurement-files`) ├── Host(auth.localhost) ──► casdoor :8000 (OIDC login)
└── casdoor :8000 (OIDC — login for the inventory panel) └── Host(s3/storage.localhost) ──► minio (API / console)
│
├── postgres :5432 (procurement + casdoor DBs)
└── minio :9000 (invoice/proforma files)
``` ```
--- ---
@@ -28,11 +34,15 @@ Browser ──► frontend :3000 (nginx SPA)
```text ```text
v1.3/ v1.3/
├── docker-compose.yml ← all paths are relative to this folder ├── docker-compose.yml ← all paths are relative to this folder
├── docker-compose.prod.yml ← production overlay (real domains + HTTPS via Let's Encrypt)
├── start.ps1 ← PowerShell helper (build + start + status) ├── start.ps1 ← PowerShell helper (build + start + status)
├── mc.cmd ← MinIO Client helper (inspect the bucket) ├── mc.cmd ← MinIO Client helper (inspect the bucket)
├── README.md ├── README.md
├── sql/ ├── sql/
│ └── migration.sql ← DB schema v2.6 (auto-runs on first boot) │ ├── migration.sql ← DB schema v2.6 (auto-runs on first boot)
│ └── 02-casdoor-db.sql ← creates the `casdoor` role+DB on the same instance
├── infra/
│ └── traefik-dynamic.yml ← Traefik shared middlewares (HTTP→HTTPS redirect)
├── docs/ ← ERDs + migration plan + auth guide ├── docs/ ← ERDs + migration plan + auth guide
│ ├── erd.txt (v2.4 — old procurement schema) │ ├── erd.txt (v2.4 — old procurement schema)
│ ├── new erd.txt (v2.5 — material → supplier → category) │ ├── new erd.txt (v2.5 — material → supplier → category)
@@ -83,13 +93,18 @@ no browser redirect to Casdoor.
«کالاهای در راه» (receiving included) and «سازمان‌دهی» (users, PMs, warehouses). «کالاهای در راه» (receiving included) and «سازمان‌دهی» (users, PMs, warehouses).
The «انبارها» and «تغییر موجودی» sections are PM-only. The «انبارها» and «تغییر موجودی» sections are PM-only.
**Developer Mode** (top-left button on the login page) allows test login as **Developer Mode** (top-left button on the login page) is a 3-step flow for
مدیر تدارکات / ادمین / مدیر پروژه without Casdoor. It is controlled by the the admin (super user): ① superuser auth gate (admin credentials only) →
`VITE_ENABLE_DEV_LOGIN` env var (in Docker: the `DEV_LOGIN_ENABLED` compose ② role switcher (ادمین / مدیر تدارکات / مدیر پروژه + project) → ③ session
build arg, default `true` — set it to `false` for production builds). activation by role (admin → **پنل مدیریت ادمین** at `/admin`, مدیر تدارکات →
procurement office, مدیر پروژه → their project's inventory panel). It is
controlled by the `VITE_ENABLE_DEV_LOGIN` env var (in Docker: the
`DEV_LOGIN_ENABLED` compose build arg, default `true` — set it to `false`
for production builds).
| What | Where | Credentials | | What | Where | Credentials |
|---|---|---| |---|---|---|
| App | http://localhost (via Traefik) | — |
| Casdoor console | http://localhost:8080 | `admin` / `123` | | Casdoor console | http://localhost:8080 | `admin` / `123` |
| Seeded app users (org `inventory`) | `razaei` (admin), `mousavi` (مدیر تدارکات), `mohaghegh` (مدیر پروژه), `tester` (مدیر پروژه، پروژه «تست») | password `123456` | | Seeded app users (org `inventory`) | `razaei` (admin), `mousavi` (مدیر تدارکات), `mohaghegh` (مدیر پروژه), `tester` (مدیر پروژه، پروژه «تست») | password `123456` |
@@ -132,16 +147,16 @@ All should show `Up` (postgres also `(healthy)`).
| What | Where | | What | Where |
|---|---| |---|---|
| Frontend UI | http://localhost:3000 | | Frontend UI | http://localhost |
| API docs (Swagger) | http://localhost:8000/docs | | API docs (Swagger) | http://localhost:8000/docs |
| API health | http://localhost:8000/health → `{"status":"ok","service":"procurement-api"}` | | API health | http://localhost:8000/health → `{"status":"ok","service":"procurement-api"}` |
| MinIO console | http://localhost:9001 (user/pass `minioadmin`/`minioadmin`) | | MinIO console | http://localhost:9001 (user/pass `minioadmin`/`minioadmin`) |
| Database | `localhost:5433`, db `procurement`, user/pass `postgres`/`postgres` | | Database | `localhost:5433`, db `procurement`, user/pass `postgres`/`postgres` (Casdoor uses the same instance, db `casdoor`) |
Quick end-to-end check (frontend proxy → API → DB): Quick end-to-end check (Traefik → frontend proxy → API → DB):
```powershell ```powershell
curl http://localhost:3000/api/v1/quotes?expand=supplier,project,line_items,termsheets,events,decision,receipt curl http://localhost/api/v1/quotes?expand=supplier,project,line_items,termsheets,events,decision,receipt
``` ```
--- ---
@@ -200,7 +215,22 @@ docker compose down -v # ⚠️ full reset — wipes DB + MinIO vol
The procurement CRUD endpoints (quotes, suppliers, materials, …) are still The procurement CRUD endpoints (quotes, suppliers, materials, …) are still
open — dev placeholder, do not expose to the public internet. open — dev placeholder, do not expose to the public internet.
- Roles live in the app's `users` table (Casdoor is identity-only): `pm`, - Roles live in the app's `users` table (Casdoor is identity-only): `pm`,
`procurement_manager`, `admin`. See `docs/casdoor-auth.md`. `procurement_manager`, `admin`. Role changes are **admin-only** (RBAC);
the procurement manager can only assign projects to project managers.
The admin has a dedicated panel at `/admin` (user sync from Casdoor +
grant/revoke «مدیر تدارکات»).
- **Partial goods receipt** («رسید جزیی») is supported in «کالاهای در راه»:
at least one item with a valid quantity is enough to submit; other items
stay in transit. The procurement ledger shows «رسیده X از Y» / «در راه Z»
badges and a «رسید جزیی» badge on the quote.
- **One PostgreSQL instance** serves both the app (`procurement`) and Casdoor
(`casdoor`) — the separate casdoor-postgres container is gone (data was
dumped/restored; see `docs/casdoor-auth.md`).
- **Traefik v3** is the public edge (ports 80/443); the frontend nginx only
serves the SPA and proxies `/api` internally. Production (real domains,
HTTPS via Let's Encrypt, HTTP→HTTPS redirect):
`docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build`
(see the prod file header for the required env vars).
- Schema runs automatically ONLY on the first boot of an empty `postgres_data` volume - Schema runs automatically ONLY on the first boot of an empty `postgres_data` volume
(see `docker-entrypoint-initdb.d`); the API also runs `CREATE TABLE IF NOT EXISTS` (see `docker-entrypoint-initdb.d`); the API also runs `CREATE TABLE IF NOT EXISTS`
for the units-dashboard tables and `quote_attachments` at startup. for the units-dashboard tables and `quote_attachments` at startup.
+43 -4
View File
@@ -10,12 +10,13 @@ scenarios, test results, and how to manage the system yourself.
| What | Address | Credentials | | What | Address | Credentials |
|---|---|---| |---|---|---|
| App (frontend) | http://localhost:3000 | — | | App (frontend) | http://localhost (Traefik edge; production: your domain) | — |
| API (backend) | http://localhost:8000 (Swagger: `/docs`) | — | | API (backend) | http://localhost:8000 (Swagger: `/docs`) | — |
| Casdoor console | http://localhost:8080 | **admin / 123** (built-in org) | | Casdoor console | http://localhost:8080 | **admin / 123** (built-in org) |
| Casdoor OIDC discovery | http://localhost:8080/.well-known/openid-configuration | — | | Casdoor OIDC discovery | http://localhost:8080/.well-known/openid-configuration | — |
| Casdoor public JWKS | http://localhost:8080/.well-known/jwks | — | | Casdoor public JWKS | http://localhost:8080/.well-known/jwks | — |
| In-network Casdoor (from the api container) | http://casdoor:8000 | — | | In-network Casdoor (from the api container) | http://casdoor:8000 | — |
| Traefik host routes (local) | `localhost` → app · `api.localhost` → api · `auth.localhost` → casdoor · `s3.localhost` / `storage.localhost` → MinIO | needs those hosts to resolve (or use the host ports) |
### Seeded application users (org `inventory`) ### Seeded application users (org `inventory`)
@@ -143,7 +144,7 @@ After that, log in as the admin and use the Organization panel
| 1 | Casdoor unavailable | `POST /auth/login` (token exchange) or JWKS fetch on protected endpoints | Login page: «سرویس احراز هویت (Casdoor) در دسترس نیست»; API: 503 | `Casdoor token endpoint unreachable (…): …` or `JWKS fetch failed (…): …` | `docker compose ps` — casdoor must be `Up`; from the api container: `python -c "import urllib.request; print(urllib.request.urlopen('http://casdoor:8000/').status)"` | | 1 | Casdoor unavailable | `POST /auth/login` (token exchange) or JWKS fetch on protected endpoints | Login page: «سرویس احراز هویت (Casdoor) در دسترس نیست»; API: 503 | `Casdoor token endpoint unreachable (…): …` or `JWKS fetch failed (…): …` | `docker compose ps` — casdoor must be `Up`; from the api container: `python -c "import urllib.request; print(urllib.request.urlopen('http://casdoor:8000/').status)"` |
| 2 | Backend unavailable | nginx `/api/` proxy | App shows «اتصال به سرور برقرار نشد» (frontend toast) | none (api is down) | `docker compose ps api`; `docker logs procurement-api`; curl `http://localhost:8000/health` | | 2 | Backend unavailable | nginx `/api/` proxy | App shows «اتصال به سرور برقرار نشد» (frontend toast) | none (api is down) | `docker compose ps api`; `docker logs procurement-api`; curl `http://localhost:8000/health` |
| 3 | Invalid client (wrong client_id/secret) | Casdoor token endpoint | 503 «سرویس احراز هویت به درستی پیکربندی نشده است» | `Casdoor rejected password grant: error=invalid_client description=…` | Compare `PROCUREMENT_CASDOOR_CLIENT_ID/SECRET` in docker-compose with the application in the Casdoor console (Applications → inventory-app) | | 3 | Invalid client (wrong client_id/secret) | Casdoor token endpoint | 503 «سرویس احراز هویت به درستی پیکربندی نشده است» | `Casdoor rejected password grant: error=invalid_client description=…` | Compare `PROCUREMENT_CASDOOR_CLIENT_ID/SECRET` in docker-compose with the application in the Casdoor console (Applications → inventory-app) |
| 4 | Invalid redirect URI | Casdoor authorization endpoint (browser flows only) | Casdoor's own error page in the browser | — (backend not involved) | The app no longer uses redirects. In the Casdoor console, the app's Redirect URIs must contain `http://localhost:3000/login` | | 4 | Invalid redirect URI | Casdoor authorization endpoint (browser flows only) | Casdoor's own error page in the browser | — (backend not involved) | The app no longer uses redirects. If a browser OIDC flow is re-enabled, the app's Redirect URIs in the Casdoor console must contain the current app URL (`http://localhost/login` with Traefik) |
| 5 | Invalid / corrupted JWT | Backend JWT verification | 401 «توکن نامعتبر است» / «امضای توکن نامعتبر است» | `JWT unreadable: …` or `JWT kid '…' not found in JWKS` | Decode the token at jwt.io; check `kid` against `http://localhost:8080/.well-known/jwks` | | 5 | Invalid / corrupted JWT | Backend JWT verification | 401 «توکن نامعتبر است» / «امضای توکن نامعتبر است» | `JWT unreadable: …` or `JWT kid '…' not found in JWKS` | Decode the token at jwt.io; check `kid` against `http://localhost:8080/.well-known/jwks` |
| 6 | Expired JWT | Backend JWT verification | 401 «توکن نامعتبر یا منقضی شده است» | `JWT verification failed: … Signature has expired` | Token `exp` claim; lifetime = Application → `expireInHours` (24h) | | 6 | Expired JWT | Backend JWT verification | 401 «توکن نامعتبر یا منقضی شده است» | `JWT verification failed: … Signature has expired` | Token `exp` claim; lifetime = Application → `expireInHours` (24h) |
| 7 | Missing JWT | Protected endpoint dependency | 401 «احراز هویت لازم است…» | normal access log, 401 | Send `Authorization: Bearer <token>`; in the app this means the session was cleared — log in again | | 7 | Missing JWT | Protected endpoint dependency | 401 «احراز هویت لازم است…» | normal access log, 401 | Send `Authorization: Bearer <token>`; in the app this means the session was cleared — log in again |
@@ -179,7 +180,7 @@ After that, log in as the admin and use the Organization panel
| 7 | Application exists | ✅ `inventory-app` | | 7 | Application exists | ✅ `inventory-app` |
| 8 | Client exists | ✅ `inventory-app-client` (grants incl. `password`) | | 8 | Client exists | ✅ `inventory-app-client` (grants incl. `password`) |
| 9 | Test users exist | ✅ razaei, mousavi, mohaghegh, tester (password 123456) | | 9 | Test users exist | ✅ razaei, mousavi, mohaghegh, tester (password 123456) |
| 10 | Redirect URI matches | ✅ `http://localhost:3000/login` registered (unused by the new flow, kept for console flows) | | 10 | Redirect URI matches | ✅ N/A for the in-page flow (redirects unused). The seeded app holds the legacy `http://localhost:3000/login`; the seeder now defaults to `http://localhost/login` |
| 11 | OIDC works | ✅ discovery doc + token endpoint + JWT issuance verified | | 11 | OIDC works | ✅ discovery doc + token endpoint + JWT issuance verified |
| 12 | PKCE works | ➖ **N/A** — the PKCE/redirect flow was replaced by the in-page password-grant login (per the redesign spec); nothing to verify | | 12 | PKCE works | ➖ **N/A** — the PKCE/redirect flow was replaced by the in-page password-grant login (per the redesign spec); nothing to verify |
| 13 | Login works | ✅ form login in the browser (tester → `/inventory`) | | 13 | Login works | ✅ form login in the browser (tester → `/inventory`) |
@@ -196,7 +197,45 @@ After that, log in as the admin and use the Organization panel
--- ---
## 8. Managing Casdoor yourself ## 8. Stack changes in this release
- **One PostgreSQL instance.** The separate casdoor-postgres container is
gone: the app DB (`procurement`) and Casdoor's DB (`casdoor`) now live on
the same `postgres:18` service. The `casdoor` role + database are created
by `sql/02-casdoor-db.sql` on a fresh volume; the migration was done via
`pg_dump`/`pg_restore` (pre-migration backups are kept on the host).
- **Traefik v3 is the public edge.** Ports 80/443 (web/websecure); host rules
for app / api / auth / s3 / storage (env-overridable, localhost defaults).
The frontend nginx now only serves the SPA and proxies `/api` internally.
Production (real domains, Let's Encrypt, HTTP→HTTPS redirect) is a one
command with `docker-compose.prod.yml` — see its header for the env vars.
Note: Traefik must be **v3.6+** (≤3.3 fails against Docker Engine 29.x),
and the docker socket is mounted read-write (Docker Desktop rejects `:ro`).
- **RBAC hardening (Organization / Admin).** Only the `admin` can change
roles (`PATCH /users/{id}`); the procurement manager can only assign
projects to project managers. The user-creation form is removed — users
come from Casdoor via `POST /users/sync` (identity mirroring, roles
untouched). The admin has a dedicated panel at `/admin` (sync + grant/
revoke «مدیر تدارکات»).
- **Developer Mode is now a 3-step admin flow** on the login page: admin
credentials → role switcher (ادمین / مدیر تدارکات / مدیر پروژه) → role-based
routing (admin → `/admin`, manager → office, PM → inventory).
- **Partial goods receipt** in «کالاهای در راه»: submitting any item with a
valid quantity is enough; the rest stays in transit. The ledger shows
«رسیده X از Y» / «در راه Z» and a «رسید جزیی» badge on the quote. The
arrival-confirmation 500 (a `db.begin()` on an already-begun session) was
fixed, and the receipt response now returns the real derived state.
- **Daily control calendar** now renders day-of-month (was rendering the
Jalali year), navigates by real Jalali months, and uses pure Jalali dates
(«امروز ۸ شهریور ۱۴۰۵»), with a blue accent on today and a ring on the
selected day.
- **Uploads** (invoice/proforma) no longer send `Content-Type: application/json`
on multipart requests — the 400/422 «دادهی ارسالشده نامعتبر است» is fixed.
- **Project filter** in the admin inventory views is fixed: empty/undefined
query params are dropped (previously `project_id=undefined` → 400 → empty
dropdowns).
## 9. Managing Casdoor yourself
- **Console:** http://localhost:8080 — log in as **admin / 123**. From there - **Console:** http://localhost:8080 — log in as **admin / 123**. From there
you can manage Organizations, Users, Applications, Providers, Certificates. you can manage Organizations, Users, Applications, Providers, Certificates.