From fde2168606069d7ff66f883a63a104430affd5b6 Mon Sep 17 00:00:00 2001 From: ali_mohaghegh Date: Sat, 29 Aug 2026 14:44:01 +0330 Subject: [PATCH] feat(casdoor): seeder for current Casdoor version + wire api env and dev-login build arg - seed_casdoor.py: JSON type=login + cookie session (the old form-encoded login-normal API is gone), application with grantTypes incl. password, passwordType=plain for the seeded users, idempotent re-runs; adds a dedicated tester user - docker-compose: Casdoor env for the api service (endpoint casdoor:8000 in-network, issuer http://localhost:8080 as advertised by discovery) and the DEV_LOGIN_ENABLED build arg for the frontend --- backend/tools/seed_casdoor.py | 115 ++++++++++++++++++++-------------- docker-compose.yml | 16 ++++- 2 files changed, 84 insertions(+), 47 deletions(-) diff --git a/backend/tools/seed_casdoor.py b/backend/tools/seed_casdoor.py index 42eb95d..5fdbf1e 100644 --- a/backend/tools/seed_casdoor.py +++ b/backend/tools/seed_casdoor.py @@ -3,15 +3,22 @@ Idempotent, best-effort helper for local development. Run AFTER the stack is up (casdoor on http://localhost:8080, admin/123): + # from the host (needs python3): python tools/seed_casdoor.py -It talks to Casdoor's REST API. If the Casdoor API version differs, the -script logs the failure and keeps going (the important part for local dev -is the app + client id/secret, which match frontend/public/casdoor-config.js). + # or from inside the api container: + # docker compose run --rm -e CASDOOR_BASE=http://casdoor:8000 api python tools/seed_casdoor.py + +Casdoor >= v1.9x contract: /api/login expects a JSON body with type="login" +(not the old form-encoded type=login-normal) and returns a session cookie +used for the management REST API. The application is created with the +`password` grant type — that is what the backend's POST /auth/login uses +for the in-page login flow. """ +import http.cookiejar +import json import os import urllib.request -import json BASE = os.environ.get("CASDOOR_BASE", "http://localhost:8080") ADMIN = os.environ.get("CASDOOR_ADMIN", "admin") @@ -24,58 +31,61 @@ CLIENT_SECRET = "inventory-app-secret-2026" REDIRECT_URI = "http://localhost:3000/login" USERS = [ - # (name, first_name, last_name, email, password, role_tag) - ("razaei", "آقای", "رضایی", "razaei@sample.ir", "123456", "admin"), - ("mousavi", "آقای", "موسوی", "mousavi@sample.ir", "123456", "pm"), - ("mohaghegh", "آقای", "محقق", "mohaghegh@sample.ir", "123456", "pm"), + # (name, first_name, last_name, email, password) + ("razaei", "آقای", "رضایی", "razaei@sample.ir", "123456"), + ("mousavi", "آقای", "موسوی", "mousavi@sample.ir", "123456"), + ("mohaghegh", "آقای", "محقق", "mohaghegh@sample.ir", "123456"), + ("tester", "کاربر", "تست", "tester@sample.ir", "123456"), ] -def _req(method: str, path: str, body=None, auth: str | None = None): +def _request(method: str, path: str, body=None, cookies=None): url = BASE + path data = json.dumps(body).encode() if body is not None else None req = urllib.request.Request(url, data=data, method=method) req.add_header("Content-Type", "application/json") - if auth: - import base64 - req.add_header("Authorization", "Basic " + base64.b64encode(auth.encode()).decode()) + jar = cookies if cookies is not None else http.cookiejar.CookieJar() + opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(jar)) try: - with urllib.request.urlopen(req, timeout=20) as resp: + with opener.open(req, timeout=20) as resp: return resp.status, json.loads(resp.read() or b"{}") except Exception as exc: return 0, {"error": str(exc)} def _login(): - """Casdoor login -> JWT (its /api/login is form-encoded).""" - import urllib.parse - data = urllib.parse.urlencode({ - "type": "login-normal", "organization": "built-in", - "username": ADMIN, "password": ADMIN_PW, - }).encode() - req = urllib.request.Request(BASE + "/api/login", data=data, method="POST") - req.add_header("Content-Type", "application/x-www-form-urlencoded") - try: - with urllib.request.urlopen(req, timeout=20) as resp: - body = json.loads(resp.read() or b"{}") - return body.get("data", "") or body.get("accessToken", "") - except Exception: - return "" + """Casdoor admin login (JSON body, type=login) -> session cookie jar.""" + jar = http.cookiejar.CookieJar() + status, body = _request("POST", "/api/login", { + "type": "login", + "organization": "built-in", + "username": ADMIN, + "password": ADMIN_PW, + "application": "app-built-in", + }, cookies=jar) + if body.get("status") != "ok": + print("admin login failed:", status, body.get("msg", body.get("error", ""))) + return None + return jar def main(): - token = _login() - auth = f"{ADMIN}:{ADMIN_PW}" if not token else f"Bearer {token}".replace("Bearer ", "Bearer ") # placeholder - auth = f"Bearer {token}" if token else None + cookies = _login() + if cookies is None: + return - # 1) organization - status, body = _req("POST", "/api/add-organization", { - "owner": "admin", "name": ORG, "displayName": "سازمان تدارکات", - }, auth) + # 1) organization (passwordType "plain" = dev: passwords stored as-is) + status, body = _request("POST", "/api/add-organization", { + "owner": "admin", + "name": ORG, + "displayName": "سازمان تدارکات", + "passwordType": "plain", + }, cookies) print("add-organization:", status, body.get("msg", body.get("error", ""))) - # 2) application (client id/secret fixed — matches casdoor-config.js) - status, body = _req("POST", "/api/add-application", { + # 2) application — `password` grant powers the in-page login (backend + # POST /auth/login); authorization_code keeps Casdoor's own flows working. + status, body = _request("POST", "/api/add-application", { "owner": "admin", "name": APP, "displayName": "سامانه تدارکات و انبار", @@ -83,22 +93,35 @@ def main(): "clientId": CLIENT_ID, "clientSecret": CLIENT_SECRET, "redirectUris": [REDIRECT_URI], - "grantTypes": ["authorization_code"], - "enablePassword": True, + "grantTypes": ["authorization_code", "refresh_token", "password"], + "signinMethods": [{"name": "Password"}], "enableSignUp": False, "tokenFormat": "JWT", "expireInHours": 24, - }, auth) + }, cookies) print("add-application:", status, body.get("msg", body.get("error", ""))) - # 3) users - for name, first, last, email, pw, _role in USERS: - status, body = _req("POST", "/api/add-user", { - "owner": ORG, "name": name, "displayName": f"{first} {last}", - "firstName": first, "lastName": last, "email": email, "password": pw, - "phone": "", "type": "normal-user", - }, auth) + # 3) users (roles are NOT stored here — they live in the app's users table) + for name, first, last, email, pw in USERS: + status, body = _request("POST", "/api/add-user", { + "owner": ORG, + "name": name, + "displayName": f"{first} {last}", + "firstName": first, + "lastName": last, + "email": email, + "password": pw, + "passwordType": "plain", + "phone": "", + "type": "normal-user", + }, cookies) print(f"add-user {name}:", status, body.get("msg", body.get("error", ""))) + # Ensure passwordType even for already-existing users (this Casdoor + # version refuses login when both user and org passwordType are empty). + status, body = _request("POST", f"/api/update-user?id={ORG}/{name}&columns=password_type", { + "passwordType": "plain", + }, cookies) + print(f"set-password-type {name}:", status, body.get("msg", body.get("error", ""))) print("\nDone. Casdoor console: http://localhost:8080 (admin/123)") print("Login in the app: http://localhost:3000/login") diff --git a/docker-compose.yml b/docker-compose.yml index 2acffea..38d7be5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -74,7 +74,9 @@ services: # v2.6 — Casdoor (OIDC identity provider) # Console: http://localhost:8080 (admin / 123) # Application "inventory-app" (org "inventory", client inventory-app-client) - # is seeded by casdoor/seed.py — see tools/seed_casdoor.py in the repo. + # is created by backend/tools/seed_casdoor.py — run it once after the + # first boot: + # docker compose run --rm -e CASDOOR_BASE=http://casdoor:8000 api python tools/seed_casdoor.py # ========================================================================== casdoor-postgres: image: postgres:16 @@ -115,6 +117,15 @@ services: PROCUREMENT_MINIO_SECRET_KEY: minioadmin PROCUREMENT_MINIO_BUCKET: procurement-files PROCUREMENT_MINIO_SECURE: "false" + # Casdoor: endpoint is the in-network service URL; the issuer must match + # the origin Casdoor advertises to browsers (its OIDC discovery doc). + PROCUREMENT_CASDOOR_ENDPOINT: http://casdoor:8000 + PROCUREMENT_CASDOOR_ISSUER: http://localhost:8080 + PROCUREMENT_CASDOOR_CLIENT_ID: inventory-app-client + PROCUREMENT_CASDOOR_CLIENT_SECRET: inventory-app-secret-2026 + PROCUREMENT_CASDOOR_ORGANIZATION: inventory + PROCUREMENT_CASDOOR_APP_NAME: inventory-app + PROCUREMENT_CASDOOR_AUDIENCE: inventory-app-client ports: - "8000:8000" @@ -130,6 +141,9 @@ services: frontend: build: context: ./frontend + args: + # Developer Mode panel on the login page — set false for production. + DEV_LOGIN_ENABLED: ${DEV_LOGIN_ENABLED:-true} container_name: procurement-frontend ports: