New section 'Working with the deployed system (server)': public app and Casdoor panel URLs (aramesh / casdoor .bennu-api-bahrestan.ir) and step-by-step MinIO console access via an SSH tunnel (ssh -L 9001:127.0.0.1:9001 root@171.22.25.75 -> http://localhost:9001), including the aramesh-minio credentials, the procurement-files bucket, why the console is loopback-only (port 9000 is reserved for the deploy-webhook receiver), the secret-handling warning and the tunnel teardown. Also refreshed sections that went stale this session: the login form description no longer mentions the removed optional organization field; the restore section now documents the committed admin-only seed dumps in backups/علی محقق/ (the old restore.ps1/sh scripts and demo dumps were deleted) with the current login and the still-available demo seeders.
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 / 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.
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:
-
Open PowerShell and start the tunnel (keep the window open while you use the panel):
ssh -L 9001:127.0.0.1:9001 root@171.22.25.75You will also land in a normal SSH session — that is fine; the tunnel lives as long as the window stays open.
-
In your browser, open: http://localhost:9001
-
Log in with:
Username Password aramesh-miniofeb10fadb5441019dfc33bf12a9b0f74e7b6c478
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
9000stays reserved for the deploy-webhook receiver, which is why the console uses9001. - 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.irbehind 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 cidownloads)
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"} |
| 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):
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):
.\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
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:
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):
- App seeder (realistic demo dataset; keeps the Casdoor org as-is):
docker exec procurement-api sh -c "cd /srv/backend && python -m tools.seed" - Casdoor seeder (creates org
inventory+ app + users razaei / mousavi / mohaghegh / tester — run once after a fresh volume):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
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,/inventoryand/pmare 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
userstable (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; seedocs/casdoor-auth.md). - Traefik v3 is the public edge (ports 80/443); the frontend nginx only
serves the SPA and proxies
/apiinternally. 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_datavolume (seedocker-entrypoint-initdb.d); the API also runsCREATE TABLE IF NOT EXISTSfor the units-dashboard tables andquote_attachmentsat startup. - Invoice upload requires an
accepted/arrivedquote; 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 psanddocker compose logs api.