- Delete backups/*.dump and all real credentials from README/docs
(prod MinIO creds, Casdoor passwords, server IP, admin password)
- All stack secrets now come from git-ignored .env (.env.example added);
docker-compose.prod.yml refuses to boot without them (${VAR:?})
- seed_casdoor.py: bcrypt password hashing, generated console/client secrets
- Harden infra/deploy-webhook: HMAC-verified Gitea webhook receiver
+ systemd unit + README (replaces unauthenticated deploy endpoint)
- docs/SECRET-MANAGEMENT.md: secret inventory + rotation checklist
336 lines
14 KiB
Markdown
336 lines
14 KiB
Markdown
# 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 credentials are set when you run the seeder (`CASDOOR_ADMIN_PW`) — never commit them |
|
||
| Seeded app users (org `inventory`) | `razaei` (admin), `mousavi` (مدیر تدارکات), `mohaghegh` (مدیر پروژه), `tester` (مدیر پروژه، پروژه «تست») | password from `CASDOOR_SEED_PASSWORD` (bcrypt-hashed at rest — see `backend/tools/seed_casdoor.py`) |
|
||
|
||
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 <user>@<server-host>
|
||
```
|
||
|
||
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 the production MinIO root credentials from the server's
|
||
`/srv/erp/.env` (`MINIO_ROOT_USER` / `MINIO_ROOT_PASSWORD`) — they are
|
||
**never** written in this repo or its history. See
|
||
[`docs/SECRET-MANAGEMENT.md`](docs/SECRET-MANAGEMENT.md).
|
||
|
||
You will see the **`procurement-files`** bucket — the invoices/proformas the
|
||
ERP stores, browsable and downloadable from there.
|
||
|
||
⚠️ Those credentials are **full admin access to the file storage**. They live
|
||
only in the server-side `.env`; if one ever leaks, rotate it immediately
|
||
(checklist in `docs/SECRET-MANAGEMENT.md`).
|
||
|
||
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 <this-folder>
|
||
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
|
||
```
|
||
|
||
---
|
||
|
||
## Recovery / seeding
|
||
|
||
The databases are **not** in git — Docker volumes hold the data, and
|
||
database dumps must never be committed (they contain user rows and signing
|
||
material). Two supported ways to bring a fresh stack up:
|
||
|
||
1. **Empty boot** — schema v2.6 auto-applies on the first boot of an empty
|
||
`postgres_data` volume; then run the seeders:
|
||
|
||
```powershell
|
||
# Casdoor org + app + users (prints/generates credentials — do not commit them)
|
||
docker compose run --rm -e CASDOOR_BASE=http://casdoor:8000 api python tools/seed_casdoor.py
|
||
# Demo dataset (optional):
|
||
docker exec procurement-api sh -c "cd /srv/backend && python -m tools.seed"
|
||
```
|
||
|
||
2. **Restore your own dump** — create dumps on the server (kept server-side,
|
||
e.g. `/srv/erp/backups/`), never in git:
|
||
|
||
```powershell
|
||
docker exec procurement-postgres pg_dump -U postgres -d procurement -Fc > /srv/erp/backups/procurement-$(date +%F).dump
|
||
docker exec procurement-postgres pg_dump -U postgres -d casdoor -Fc > /srv/erp/backups/casdoor-$(date +%F).dump
|
||
```
|
||
|
||
Restore with `pg_restore --clean --if-exists` (stop `api` + `casdoor`
|
||
first — this **replaces all data**). The scheduled backup sidecar
|
||
(see `docs/OPERATIONS.md`) automates this.
|
||
|
||
## 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) and **required on
|
||
every API route** except `/health`, `/health/ready` and `/auth/login`
|
||
(401 without a token, 403 without a role). The former dev-header fallback
|
||
(`X-User-Role` / `X-User-Name-B64`) is disabled unless
|
||
`PROCUREMENT_DEV_LOGIN_ENABLED=true` — never enable it in production.
|
||
- 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`.
|