# Procurement Stack v1.3 — Dockerized (PostgreSQL + FastAPI + React + MinIO) Self-contained ship package: everything needed to run the stack is in this folder — no external files or sibling folders required. | Service | Container | Tech | Exposed port | |---|---|---|---| | **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 (internal) | via Traefik | | **traefik** | `procurement-traefik` | Traefik v3 — public reverse proxy (edge) | `80` (web), `443` (websecure) | ``` Browser ──► Traefik :80 (edge) │ ├── 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) ``` --- ## 📁 Layout ```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) │ └── 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) │ ├── 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 │ ├── requirements.txt │ ├── .dockerignore │ ├── app/ (source code — procurement + inventory + auth) │ └── tools/ (smoke-test + seed scripts, copied into the image) └── frontend/ ← build context of the `frontend` service ├── Dockerfile ├── nginx.conf ├── .dockerignore ├── .env.example (copy to .env if you need a custom API base URL) ├── index.html ├── package.json ├── package-lock.json ├── tsconfig*.json ├── vite.config.ts ├── public/ └── src/ └── pages/ ├── Dashboard/ (procurement office) ├── Wizard/ (add new offer) ├── Suppliers/ (suppliers & topics) ├── Projects/ (projects) ├── Units/ (unit conversions) └── Inventory/ (warehouse management panel — React port) ``` --- ## 🔐 Login The whole system is behind a login page (`/login`). Login is **in-page**: the form (username / password) 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 («کالاهای در راه»). - **مدیر تدارکات / ادمین** → 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. **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` | 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). > The **currently committed seed** (`backups/علی محقق/`) instead ships a > single admin — `ali.mohaghegh` («علی محقق») — see > [Restoring the committed seed data](#restoring-the-committed-seed-data). --- ## 🌐 Working with the deployed system (server) The production deployment of this stack runs on the server and is reachable at these public addresses (HTTPS): | What | Address | |---|---| | **App (ERP)** | https://aramesh.bennu-api-bahrestan.ir/ | | **Casdoor admin panel** | https://casdoor.bennu-api-bahrestan.ir/ | Log in to the app with your Casdoor account (org `inventory`); the role is assigned inside the app (`users.role` — see «Login» above and [`docs/casdoor-auth.md`](docs/casdoor-auth.md)). ### MinIO console on the server (SSH tunnel) The MinIO console is deliberately **loopback-only** on the server (`127.0.0.1:9001`) — verified unreachable from the internet — so the way in is an SSH tunnel from your machine: 1. Open PowerShell and start the tunnel (**keep the window open** while you use the panel): ```powershell ssh -L 9001:127.0.0.1:9001 root@171.22.25.75 ``` You will also land in a normal SSH session — that is fine; the tunnel lives as long as the window stays open. 2. In your browser, open: **http://localhost:9001** 3. Log in with: | Username | Password | |---|---| | `aramesh-minio` | `feb10fadb5441019dfc33bf12a9b0f74e7b6c478` | You will see the **`procurement-files`** bucket — the invoices/proformas the ERP stores, browsable and downloadable from there. ⚠️ These credentials are **full admin access to the file storage**. Treat them as a production secret (this README is inside the repo, so anyone with repo access can read them — rotate them on the server if they ever leak). Close the PowerShell window (or `Ctrl+C`) when you are done to drop the tunnel. Notes from the server setup: - The panel is published only on the server's loopback, so the internet cannot reach it at all — the tunnel is what carries it to your browser. - Host port `9000` stays reserved for the deploy-webhook receiver, which is why the console uses `9001`. - If you ever need the raw S3 API from your machine, or a clickable URL for the console (e.g. `storage.aramesh.bennu-api-bahrestan.ir` behind Traefik with a password prompt on top), that can be arranged on request — the tunnel remains the safer default. --- ## Prerequisites (target machine) - **Docker Desktop** (Windows) or Docker Engine + Compose plugin (Linux/macOS) - ~3–4 GB free disk for images (`postgres:18`, `python:3.13-slim`, `node:22-alpine`, `nginx:alpine`, `minio/minio`, `minio/mc`) - Internet access on first build (image + `npm ci` downloads) --- ## Start the whole stack ```powershell cd docker compose up -d --build ``` First run downloads base images and builds the frontend (takes a few minutes). Subsequent runs: `docker compose up -d`. Wait until all services are up: ```powershell docker compose ps ``` All should show `Up` (postgres also `(healthy)`). --- ## Verify | What | Where | |---|---| | 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` (Casdoor uses the same instance, db `casdoor`) | Quick end-to-end check (Traefik → frontend proxy → API → DB): ```powershell curl http://localhost/api/v1/quotes?expand=supplier,project,line_items,termsheets,events,decision,receipt ``` --- ## MinIO (object storage for invoice / proforma files) | Item | Value | |---|---| | S3 API endpoint | `localhost:9000` (from the API container: `minio:9000`) | | Web console | http://localhost:9001 | | Root user / password | `minioadmin` / `minioadmin` (dev — change for production) | | Bucket | `procurement-files` (created automatically by the `minio-init` container) | | Object key layout | `{quote_id}/{kind}/{attachment_id}` where kind = `invoice` \| `proforma` | File **bytes** live in MinIO; the `quote_attachments` table holds the metadata. Ad-hoc bucket inspection from the host (see `mc.cmd`): ```powershell .\mc.cmd ls --recursive ``` --- ## Database credentials / connection ```text Host: localhost Port: 5433 Database: procurement Username: postgres Password: postgres ``` ```powershell docker exec -it procurement-postgres psql -U postgres -d procurement ``` --- ## Restoring the committed seed data The databases are **not** in git — Docker volumes hold the data. The repo ships recovery dumps in `backups/علی محقق/`: | File | Contents | |---|---| | `casdoor.dump` | Casdoor schema + console admin (`admin` / `123`), org `inventory`, app `inventory-app`, the JWT signing cert, and the single org user `ali.mohaghegh` | | `procurement.dump` | Procurement schema + the single `users` row «علی محقق» with role `admin`; all other tables empty (categories/units re-seed at API startup) | ⚠️ This **replaces all data** in the `procurement` and `casdoor` databases. On a running stack: ```powershell docker compose stop api casdoor docker cp "backups/علی محقق/procurement.dump" procurement-postgres:/tmp/ docker cp "backups/علی محقق/casdoor.dump" procurement-postgres:/tmp/ docker exec procurement-postgres pg_restore -U postgres -d procurement --clean --if-exists /tmp/procurement.dump docker exec procurement-postgres pg_restore -U postgres -d casdoor --clean --if-exists /tmp/casdoor.dump docker compose start casdoor api ``` Login afterwards: `ali.mohaghegh` / `hulkfuck002` (Casdoor rejects spaces in usernames, so «ali mohaghegh» is not a valid login name). Want demo data instead? Two seeders are still available (they create different users — see the Login table above): 1. **App seeder** (realistic demo dataset; keeps the Casdoor org as-is): ```powershell docker exec procurement-api sh -c "cd /srv/backend && python -m tools.seed" ``` 2. **Casdoor seeder** (creates org `inventory` + app + users razaei / mousavi / mohaghegh / tester — run once after a fresh volume): ```powershell python backend/tools/seed_casdoor.py # from the host, or from the api container ``` To create a fresh dump of your own current state: stop `api` + `casdoor`, `docker exec procurement-postgres pg_dump -U postgres -d procurement -Fc` (+ the `casdoor` DB) into `backups/`, then start them again. ## Common operations ```powershell docker compose logs -f api # see logs docker compose down # stop (keep data) docker compose up -d --build # rebuild after code changes docker compose down -v # ⚠️ full reset — wipes DB + MinIO volumes ``` --- ## Known behavior - 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`. 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. - Invoice upload requires an `accepted`/`arrived` quote; proforma has no restriction. - The supplier detail page (`/suppliers/:id`) shows the purchase history with invoice/proforma download links from MinIO. - If builds fail with disk-full errors: `docker system prune -a`, then rebuild. - If the frontend shows «اتصال به سرور برقرار نشد» → the api container is not reachable; check `docker compose ps` and `docker compose logs api`.