Files
ERP/README.md
T

15 KiB
Raw Blame History

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

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.

The currently committed seed (backups/علی محقق/) instead ships a single admin — ali.mohaghegh («علی محقق») — see 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).

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):
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.

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

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:

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"}
API readiness (DB + MinIO + Casdoor) http://localhost:8000/health/ready
Metrics http://localhost:8000/metrics
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)

Automated end-to-end checks (authenticates via SMOKE_USER / SMOKE_PASSWORD — the seeder users):

backend\tools\smoke_test.py   # read paths
backend\tools\smoke_test_write.py / smoke_test_misc.py

Every API route now requires a Casdoor JWT — a bare curl http://localhost/api/v1/quotes returns 401 by design. Day-2 operations (deploys, backups, monitoring): docs/OPERATIONS.md.


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

File bytes live in MinIO; the quote_attachments table holds the metadata.

Ad-hoc bucket inspection from the host (see mc.cmd):

.\mc.cmd ls --recursive


Database credentials / connection

Host:     localhost
Port:     5433
Database: procurement
Username: postgres
Password: postgres

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:
# 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"

  1. Restore your own dump — create dumps on the server (kept server-side, e.g. /srv/erp/backups/), never in git:
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

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.
  • Material-Supplier Lookup: Clicking a material in the catalog (e.g., on the /units page) dynamically fetches its assigned suppliers (GET /api/materials/{id}/suppliers). The backend uses an atomic JOIN to guarantee accurate supplier data (avoiding SQLAlchemy identity-map mismatches), and the UI displays a clean empty state («تامین‌کننده‌ای برای این متریال ثبت نشده است») if none exist.
  • 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.