From c59f5ad1a3e645cb2f2ecb7a71b634023505a208 Mon Sep 17 00:00:00 2001 From: ali_mohaghegh Date: Sat, 29 Aug 2026 14:44:27 +0330 Subject: [PATCH] docs: Casdoor auth guide + README login/known-behavior update - docs/casdoor-auth.md: addresses and credentials (Casdoor console admin/123, seeded users), in-page login flow, status-code table, the User/Organization/Role/Permission/Application model, the 9 failure scenarios with debug steps, Test 1-6 results, 23-item checklist, and how to manage Casdoor (console, seeding, first-admin bootstrap) - README: replace the 'no authentication' and 'Casdoor not seeded' notes with the current login flow, developer-mode toggle and a link to the auth guide --- README.md | 35 +++++-- docs/casdoor-auth.md | 216 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 244 insertions(+), 7 deletions(-) create mode 100644 docs/casdoor-auth.md diff --git a/README.md b/README.md index 6454316..6ddfc1c 100644 --- a/README.md +++ b/README.md @@ -33,10 +33,11 @@ v1.3/ ├── README.md ├── sql/ │ └── migration.sql ← DB schema v2.6 (auto-runs on first boot) -├── docs/ ← ERDs + migration plan +├── docs/ ← ERDs + migration plan + auth guide │ ├── erd.txt (v2.4 — old procurement schema) │ ├── new erd.txt (v2.5 — material → supplier → category) │ ├── NewERD.txt (v2.6 — integrated procurement + inventory) +│ ├── casdoor-auth.md (v2.6 — Casdoor login, roles, failure scenarios) │ └── material-supplier-category-migration-plan.md ├── backend/ ← build context of the `api` service │ ├── Dockerfile @@ -69,18 +70,33 @@ v1.3/ ## 🔐 Login -The whole system is behind a login page (`/login`): +The whole system is behind a login page (`/login`). Login is **in-page**: +the form (username / password / optional organization) is sent to +`POST /api/v1/auth/login`; the backend exchanges it with Casdoor +(password grant), verifies the returned JWT, and starts the session — +no browser redirect to Casdoor. - **مدیر پروژه (PM)** → lands in the inventory panel (their project) — can record stock changes, manage the project's warehouses and confirm arrivals («کالاهای در راه»). -- **مدیر تدارکات / ادمین** (the same person) → lands in the procurement office — can manage +- **مدیر تدارکات / ادمین** → lands in the procurement office — can manage quotes, suppliers, projects, and in the inventory panel: dashboard, daily control, «کالاهای در راه» (receiving included) and «سازمان‌دهی» (users, PMs, warehouses). The «انبارها» and «تغییر موجودی» sections are PM-only. -Until Casdoor is fully seeded, use «ورود آزمایشی (توسعه)» on the login page -(role + project). Casdoor (org `inventory`, app `inventory-app`) is the planned -production identity source. +**Developer Mode** (top-left button on the login page) allows test login as +مدیر تدارکات / ادمین / مدیر پروژه without Casdoor. 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 | +|---|---|---| +| Casdoor console | http://localhost:8080 | `admin` / `123` | +| Seeded app users (org `inventory`) | `razaei` (admin), `mousavi` (مدیر تدارکات), `mohaghegh` (مدیر پروژه), `tester` (مدیر پروژه، پروژه «تست») | password `123456` | + +Run `backend/tools/seed_casdoor.py` once after the first boot to create the +Casdoor organization, application and users (idempotent). Full details — +addresses, credentials, role model, failure scenarios and the checklist — +are in [`docs/casdoor-auth.md`](docs/casdoor-auth.md). --- @@ -179,7 +195,12 @@ docker compose down -v # ⚠️ full reset — wipes DB + MinIO vol ## Known behavior -- The backend has **no authentication** (dev placeholder) — do not expose to the public internet. +- Authentication is Casdoor-based (JWT verified via JWKS): `/auth`, `/users`, + `/inventory` and `/pm` are protected (401 without a token, 403 without a role). + The procurement CRUD endpoints (quotes, suppliers, materials, …) are still + open — dev placeholder, do not expose to the public internet. +- Roles live in the app's `users` table (Casdoor is identity-only): `pm`, + `procurement_manager`, `admin`. See `docs/casdoor-auth.md`. - 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` for the units-dashboard tables and `quote_attachments` at startup. diff --git a/docs/casdoor-auth.md b/docs/casdoor-auth.md new file mode 100644 index 0000000..d2cfd02 --- /dev/null +++ b/docs/casdoor-auth.md @@ -0,0 +1,216 @@ +# Casdoor Authentication & Authorization Guide (v2.6) + +This document explains how the procurement + inventory system uses Casdoor: +addresses, credentials, the login flow, the authorization model, failure +scenarios, test results, and how to manage the system yourself. + +--- + +## 1. Addresses & credentials + +| What | Address | Credentials | +|---|---|---| +| App (frontend) | http://localhost:3000 | — | +| API (backend) | http://localhost:8000 (Swagger: `/docs`) | — | +| Casdoor console | http://localhost:8080 | **admin / 123** (built-in org) | +| Casdoor OIDC discovery | http://localhost:8080/.well-known/openid-configuration | — | +| Casdoor public JWKS | http://localhost:8080/.well-known/jwks | — | +| In-network Casdoor (from the api container) | http://casdoor:8000 | — | + +### Seeded application users (org `inventory`) + +| Username | Password | Casdoor org | App role (our `users` table) | +|---|---|---|---| +| `razaei` | `123456` | inventory | `admin` (super user) | +| `mousavi` | `123456` | inventory | `procurement_manager` (مدیر تدارکات) | +| `mohaghegh` | `123456` | inventory | `pm` (مدیر پروژه، بدون پروژه) | +| `tester` | `123456` | inventory | `pm` (مدیر پروژه — پروژه «تست») | + +These users are created by `backend/tools/seed_casdoor.py` (idempotent — safe +to re-run). + +--- + +## 2. Login flow (in-page, no redirect) + +``` +Browser ──► POST /api/v1/auth/login {username, password, organization?} + │ + ▼ +Backend ──► POST http://casdoor:8000/api/login/oauth/access_token + grant_type=password, client_id/secret, username, password + │ + ▼ access_token (JWT) +Backend ──► verifies locally: JWKS (/.well-known/jwks, by kid), + iss == http://localhost:8080, aud == inventory-app-client + ──► auto-provisions the app `users` row (role NULL on first login) + ──► returns {access_token, user: {role, role_label, project_id, ...}} + │ + ▼ +Frontend stores the token (localStorage) + profile, navigates by role: + pm → /inventory (store panel) + admin / procurement_manager → / (procurement office) +``` + +- **Two Casdoor URLs exist on purpose:** `PROCUREMENT_CASDOOR_ENDPOINT` + (`http://casdoor:8000`) is the in-network service URL used for token exchange + and JWKS fetching; `PROCUREMENT_CASDOOR_ISSUER` (`http://localhost:8080`) is + the browser-facing origin that Casdoor embeds in the `iss` claim (its + `origin` env in docker-compose). Both must be set — see the `api` service + environment block in `docker-compose.yml`. +- **Logout:** JWTs are stateless; logout clears the token + profile on the + client. The next protected call then gets `401`. There is no server-side + session to revoke (a token stays valid until `exp` unless a blacklist is + introduced — out of scope). +- The old browser-redirect (PKCE / authorization-code) flow was removed + together with the public `casdoor-config.js` (which exposed the client + secret). The `password` grant is what powers the in-page form. + +--- + +## 3. Authentication — status codes + +| Case | Status | Code | Message | +|---|---|---|---| +| Valid token | 200 | — | data | +| Missing Authorization header | 401 | `UNAUTHORIZED` | احراز هویت لازم است — توکن Casdoor یا حالت توسعه را ارسال کنید | +| Corrupted / unreadable token | 401 | `UNAUTHORIZED` | توکن نامعتبر است | +| Unknown kid / bad signature | 401 | `UNAUTHORIZED` | امضای توکن نامعتبر است | +| Expired / wrong iss / wrong aud | 401 | `UNAUTHORIZED` | توکن نامعتبر یا منقضی شده است | +| Valid token, user has no role yet | 403 | `FORBIDDEN` | نقش شما هنوز در سیستم تعیین نشده است | +| Valid token, wrong role for the section | 403 | `FORBIDDEN` | دسترسی به این بخش برای نقش شما مجاز نیست | +| Casdoor unreachable | 503 | `UPSTREAM_UNAVAILABLE` | سرویس احراز هویت (Casdoor) در دسترس نیست | +| Wrong Casdoor client config | 503 | `UPSTREAM_UNAVAILABLE` | سرویس احراز هویت به درستی پیکربندی نشده است | +| Wrong username/password | 401 | `UNAUTHORIZED` | نام کاربری یا رمز عبور نادرست است | + +--- + +## 4. Authorization — how User / Organization / Role / Permission / Application relate + +**Decision (agreed): roles stay in the app's own database. Casdoor is used for +identity only.** No new authorization model was introduced. + +- **Casdoor Application** (`inventory-app`, client `inventory-app-client`): + the OIDC client. Grant types: `authorization_code`, `refresh_token`, + `password`. Token format JWT (so the backend can verify via JWKS). +- **Casdoor Organization** (`inventory`): the tenant; every app user belongs + to it. (The Casdoor console admin `admin/123` lives in the separate + `built-in` org.) +- **User — two records per person:** + 1. Casdoor user (login credential: name + password). + 2. App `users` row, linked by `users.casdoor_id = Casdoor user id` (the JWT + `sub` claim). Auto-provisioned on first login. +- **Role** lives in `users.role` (not in Casdoor): `pm` | `procurement_manager` + | `admin` (NULL until assigned). Assigned from the app's Organization panel + (`/inventory/org`) by an admin / procurement manager. +- **Permission** is enforced per-endpoint in the backend routers, not in + Casdoor permission objects: + - `admin` (super user): everything; can create users and assign roles/projects + (adds PMs); can use Developer Mode to act as either of the two main roles. + - `procurement_manager` (مدیر تدارکات): runs the procurement office; can + also add/manage users and project managers (same Organization panel rights + as admin); read-only view of the inventory panel. + - `pm` (مدیر پروژه): store only — inventory dashboard, stock changes, daily + control, warehouses, arrivals. Scoped to the single project in + `projects.pm_user_id`. +- **Protected vs open endpoints (current split):** `/auth`, `/users`, + `/inventory`, `/pm` require authentication (Casdoor JWT, or dev headers in + Developer Mode). The procurement CRUD endpoints (quotes, suppliers, + materials, …) are still open — documented known behavior, not part of this + phase. + +### Bootstrapping the first admin + +A user is auto-provisioned with `role = NULL` on first login, and assigning +roles requires an admin — so bootstrap the very first admin directly in the DB: + +```sql +-- list provisioned users +SELECT id, casdoor_id, first_name, last_name, role FROM users; +-- promote one of them (casdoor_id = the Casdoor user id) +UPDATE users SET role = 'admin' WHERE casdoor_id = ''; +``` + +After that, log in as the admin and use the Organization panel +(`/inventory/org`) for everything else. + +--- + +## 5. Failure scenarios — where the error happens, what the user sees, how to debug + +| # | Scenario | Where it fails | What the user sees | Backend log (api container) | How to debug | +|---|---|---|---|---|---| +| 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` | +| 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` | +| 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) | +| 7 | Missing JWT | Protected endpoint dependency | 401 «احراز هویت لازم است…» | normal access log, 401 | Send `Authorization: Bearer `; in the app this means the session was cleared — log in again | +| 8 | Wrong audience | Backend JWT verification | 401 «توکن نامعتبر یا منقضی شده است» | `JWT verification failed: … audience` | Token `aud` must contain `inventory-app-client` (= `PROCUREMENT_CASDOOR_AUDIENCE`) | +| 9 | Wrong issuer | Backend JWT verification | 401 «توکن نامعتبر یا منقضی شده است» | `JWT verification failed: … issuer` | Token `iss` must equal `PROCUREMENT_CASDOOR_ISSUER` (`http://localhost:8080`); Casdoor's `origin` env must stay `http://localhost:8080` | +| 10 | User without permission | Role checks in `auth.py` / routers | 403 «نقش شما هنوز در سیستم تعیین نشده است» or «دسترسی به این بخش برای نقش شما مجاز نیست» | access log with 403 | Organization panel → assign the user a role; PMs additionally need a project via `/users/{id}/project` | + +--- + +## 6. Authentication test results (run 2026-08-29, Docker compose stack) + +| Test | Action | Result | Evidence | +|---|---|---|---| +| 1 | Login: `POST /api/v1/auth/login` (tester/123456) | **PASS** | 200, JWT + profile returned | +| 2 | Authenticated API: `GET /users/me` with token | **PASS** | 200, role `pm` + project «تست» | +| 3 | No token: `GET /inventory/warehouses` | **PASS** | 401 `UNAUTHORIZED` | +| 4 | Invalid token: corrupted Bearer | **PASS** | 401 `UNAUTHORIZED` «توکن نامعتبر است» | +| 5 | Expired token (app lifetime set to ~1s, then restored to 24h) | **PASS** | 401 «توکن نامعتبر یا منقضی شده است» | +| 6 | Logout: token cleared client-side → protected API | **PASS** | 200 before, 401 after cleanup | + +--- + +## 7. Final checklist (re-verified after the login-page redesign, 2026-08-29) + +| # | Item | Result | +|---|---|---| +| 1 | Casdoor container works | ✅ Up; console + REST API respond | +| 2 | Casdoor PostgreSQL works | ✅ Up; queried directly (orgs/users/apps) | +| 3 | Backend can reach Casdoor | ✅ api container does token exchange + JWKS fetch (`casdoor:8000`) | +| 4 | Frontend can reach Casdoor | ✅ network path open — though with the in-page flow the frontend only talks to the backend, which talks to Casdoor | +| 5 | Seeder works | ✅ ran twice; idempotent (existing objects reported, nothing breaks) | +| 6 | Organization exists | ✅ `inventory` | +| 7 | Application exists | ✅ `inventory-app` | +| 8 | Client exists | ✅ `inventory-app-client` (grants incl. `password`) | +| 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) | +| 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 | +| 13 | Login works | ✅ form login in the browser (tester → `/inventory`) | +| 14 | Token is received | ✅ JWT returned by `POST /auth/login` | +| 15 | Backend validates JWT | ✅ signature/kid/iss/aud/exp — wrong values each tested → 401 | +| 16 | JWKS works | ✅ public `/.well-known/jwks` (kid `cert-built-in`) | +| 17 | Authenticated API returns 200 | ✅ `GET /users/me` with Bearer token | +| 18 | Missing token returns 401 | ✅ | +| 19 | Invalid token returns 401 | ✅ | +| 20 | Logout works | ✅ button clears the session → back to `/login`; protected API → 401 | +| 21 | Authorization/Roles are understood | ✅ documented in §4 (roles in app DB, per-role rules) | +| 22 | No secrets accidentally exposed | ✅ `casdoor-config.js` (with clientSecret) removed from public files; secret only in backend env + seeder | +| 23 | Documentation is complete | ✅ this document (addresses, credentials, flows, failure scenarios, management) | + +--- + +## 8. Managing Casdoor yourself + +- **Console:** http://localhost:8080 — log in as **admin / 123**. From there + you can manage Organizations, Users, Applications, Providers, Certificates. +- **Seeding** (org `inventory`, app `inventory-app`, the four users) — + re-runnable at any time: + ```powershell + python backend/tools/seed_casdoor.py # from the host + # or: + docker compose run --rm -e CASDOOR_BASE=http://casdoor:8000 api python tools/seed_casdoor.py + ``` +- **Adding a user end-to-end:** add the user in the Casdoor console (org + `inventory`, set a password, passwordType `plain` for dev) → the user logs in + through the app once (auto-provisions the `users` row) → assign the role and, + for PMs, the project from the app's Organization panel (`/inventory/org`). +- **Debugging tokens:** use the discovery + JWKS endpoints above; the api + container logs every rejected token with the reason (`JWT verification + failed: …`), so `docker logs procurement-api` is the first place to look.