diff --git a/README.md b/README.md index 6ddfc1c..f3f462d 100644 --- a/README.md +++ b/README.md @@ -5,20 +5,26 @@ folder — no external files or sibling folders required. | 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) | | **casdoor** | `procurement-casdoor` | Casdoor (OIDC identity provider) | `8080` | | **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) - │ - ├── postgres :5432 (procurement DB) - ├── minio :9000 (invoice/proforma files, bucket `procurement-files`) - └── casdoor :8000 (OIDC — login for the inventory panel) + ├── Host(localhost) ──► frontend (nginx SPA :80) + │ │ + │ └── /api/* proxy ──► api :8000 (/api/v1) + │ + ├── Host(api.localhost) ──► api :8000 + ├── Host(auth.localhost) ──► casdoor :8000 (OIDC login) + └── 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 v1.3/ ├── 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) ├── mc.cmd ← MinIO Client helper (inspect the bucket) ├── README.md ├── 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 │ ├── erd.txt (v2.4 — old procurement schema) │ ├── new erd.txt (v2.5 — material → supplier → category) @@ -83,13 +93,18 @@ no browser redirect to Casdoor. «کالاهای در راه» (receiving included) and «سازمان‌دهی» (users, PMs, warehouses). The «انبارها» and «تغییر موجودی» sections are PM-only. -**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). +**Developer Mode** (top-left button on the login page) is a 3-step flow for +the admin (super user): ① superuser auth gate (admin credentials only) → +② role switcher (ادمین / مدیر تدارکات / مدیر پروژه + project) → ③ session +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 | |---|---|---| +| App | http://localhost (via Traefik) | — | | Casdoor console | http://localhost:8080 | `admin` / `123` | | 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 | |---|---| -| Frontend UI | http://localhost:3000 | +| Frontend UI | http://localhost | | API docs (Swagger) | http://localhost:8000/docs | | API health | http://localhost:8000/health → `{"status":"ok","service":"procurement-api"}` | | 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 -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 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`. + `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 (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 index d2cfd02..76fd8ed 100644 --- a/docs/casdoor-auth.md +++ b/docs/casdoor-auth.md @@ -10,12 +10,13 @@ scenarios, test results, and how to manage the system yourself. | What | Address | Credentials | |---|---|---| -| App (frontend) | http://localhost:3000 | — | +| App (frontend) | http://localhost (Traefik edge; production: your domain) | — | | 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 | — | +| 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`) @@ -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)"` | | 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` | +| 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` | | 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 | @@ -179,7 +180,7 @@ After that, log in as the admin and use the Organization panel | 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) | +| 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 | | 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`) | @@ -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 you can manage Organizations, Users, Applications, Providers, Certificates.