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
This commit is contained in:
@@ -3,15 +3,22 @@
|
|||||||
Idempotent, best-effort helper for local development. Run AFTER the stack
|
Idempotent, best-effort helper for local development. Run AFTER the stack
|
||||||
is up (casdoor on http://localhost:8080, admin/123):
|
is up (casdoor on http://localhost:8080, admin/123):
|
||||||
|
|
||||||
|
# from the host (needs python3):
|
||||||
python tools/seed_casdoor.py
|
python tools/seed_casdoor.py
|
||||||
|
|
||||||
It talks to Casdoor's REST API. If the Casdoor API version differs, the
|
# or from inside the api container:
|
||||||
script logs the failure and keeps going (the important part for local dev
|
# docker compose run --rm -e CASDOOR_BASE=http://casdoor:8000 api python tools/seed_casdoor.py
|
||||||
is the app + client id/secret, which match frontend/public/casdoor-config.js).
|
|
||||||
|
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 os
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import json
|
|
||||||
|
|
||||||
BASE = os.environ.get("CASDOOR_BASE", "http://localhost:8080")
|
BASE = os.environ.get("CASDOOR_BASE", "http://localhost:8080")
|
||||||
ADMIN = os.environ.get("CASDOOR_ADMIN", "admin")
|
ADMIN = os.environ.get("CASDOOR_ADMIN", "admin")
|
||||||
@@ -24,58 +31,61 @@ CLIENT_SECRET = "inventory-app-secret-2026"
|
|||||||
REDIRECT_URI = "http://localhost:3000/login"
|
REDIRECT_URI = "http://localhost:3000/login"
|
||||||
|
|
||||||
USERS = [
|
USERS = [
|
||||||
# (name, first_name, last_name, email, password, role_tag)
|
# (name, first_name, last_name, email, password)
|
||||||
("razaei", "آقای", "رضایی", "razaei@sample.ir", "123456", "admin"),
|
("razaei", "آقای", "رضایی", "razaei@sample.ir", "123456"),
|
||||||
("mousavi", "آقای", "موسوی", "mousavi@sample.ir", "123456", "pm"),
|
("mousavi", "آقای", "موسوی", "mousavi@sample.ir", "123456"),
|
||||||
("mohaghegh", "آقای", "محقق", "mohaghegh@sample.ir", "123456", "pm"),
|
("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
|
url = BASE + path
|
||||||
data = json.dumps(body).encode() if body is not None else None
|
data = json.dumps(body).encode() if body is not None else None
|
||||||
req = urllib.request.Request(url, data=data, method=method)
|
req = urllib.request.Request(url, data=data, method=method)
|
||||||
req.add_header("Content-Type", "application/json")
|
req.add_header("Content-Type", "application/json")
|
||||||
if auth:
|
jar = cookies if cookies is not None else http.cookiejar.CookieJar()
|
||||||
import base64
|
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(jar))
|
||||||
req.add_header("Authorization", "Basic " + base64.b64encode(auth.encode()).decode())
|
|
||||||
try:
|
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"{}")
|
return resp.status, json.loads(resp.read() or b"{}")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return 0, {"error": str(exc)}
|
return 0, {"error": str(exc)}
|
||||||
|
|
||||||
|
|
||||||
def _login():
|
def _login():
|
||||||
"""Casdoor login -> JWT (its /api/login is form-encoded)."""
|
"""Casdoor admin login (JSON body, type=login) -> session cookie jar."""
|
||||||
import urllib.parse
|
jar = http.cookiejar.CookieJar()
|
||||||
data = urllib.parse.urlencode({
|
status, body = _request("POST", "/api/login", {
|
||||||
"type": "login-normal", "organization": "built-in",
|
"type": "login",
|
||||||
"username": ADMIN, "password": ADMIN_PW,
|
"organization": "built-in",
|
||||||
}).encode()
|
"username": ADMIN,
|
||||||
req = urllib.request.Request(BASE + "/api/login", data=data, method="POST")
|
"password": ADMIN_PW,
|
||||||
req.add_header("Content-Type", "application/x-www-form-urlencoded")
|
"application": "app-built-in",
|
||||||
try:
|
}, cookies=jar)
|
||||||
with urllib.request.urlopen(req, timeout=20) as resp:
|
if body.get("status") != "ok":
|
||||||
body = json.loads(resp.read() or b"{}")
|
print("admin login failed:", status, body.get("msg", body.get("error", "")))
|
||||||
return body.get("data", "") or body.get("accessToken", "")
|
return None
|
||||||
except Exception:
|
return jar
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
token = _login()
|
cookies = _login()
|
||||||
auth = f"{ADMIN}:{ADMIN_PW}" if not token else f"Bearer {token}".replace("Bearer ", "Bearer ") # placeholder
|
if cookies is None:
|
||||||
auth = f"Bearer {token}" if token else None
|
return
|
||||||
|
|
||||||
# 1) organization
|
# 1) organization (passwordType "plain" = dev: passwords stored as-is)
|
||||||
status, body = _req("POST", "/api/add-organization", {
|
status, body = _request("POST", "/api/add-organization", {
|
||||||
"owner": "admin", "name": ORG, "displayName": "سازمان تدارکات",
|
"owner": "admin",
|
||||||
}, auth)
|
"name": ORG,
|
||||||
|
"displayName": "سازمان تدارکات",
|
||||||
|
"passwordType": "plain",
|
||||||
|
}, cookies)
|
||||||
print("add-organization:", status, body.get("msg", body.get("error", "")))
|
print("add-organization:", status, body.get("msg", body.get("error", "")))
|
||||||
|
|
||||||
# 2) application (client id/secret fixed — matches casdoor-config.js)
|
# 2) application — `password` grant powers the in-page login (backend
|
||||||
status, body = _req("POST", "/api/add-application", {
|
# POST /auth/login); authorization_code keeps Casdoor's own flows working.
|
||||||
|
status, body = _request("POST", "/api/add-application", {
|
||||||
"owner": "admin",
|
"owner": "admin",
|
||||||
"name": APP,
|
"name": APP,
|
||||||
"displayName": "سامانه تدارکات و انبار",
|
"displayName": "سامانه تدارکات و انبار",
|
||||||
@@ -83,22 +93,35 @@ def main():
|
|||||||
"clientId": CLIENT_ID,
|
"clientId": CLIENT_ID,
|
||||||
"clientSecret": CLIENT_SECRET,
|
"clientSecret": CLIENT_SECRET,
|
||||||
"redirectUris": [REDIRECT_URI],
|
"redirectUris": [REDIRECT_URI],
|
||||||
"grantTypes": ["authorization_code"],
|
"grantTypes": ["authorization_code", "refresh_token", "password"],
|
||||||
"enablePassword": True,
|
"signinMethods": [{"name": "Password"}],
|
||||||
"enableSignUp": False,
|
"enableSignUp": False,
|
||||||
"tokenFormat": "JWT",
|
"tokenFormat": "JWT",
|
||||||
"expireInHours": 24,
|
"expireInHours": 24,
|
||||||
}, auth)
|
}, cookies)
|
||||||
print("add-application:", status, body.get("msg", body.get("error", "")))
|
print("add-application:", status, body.get("msg", body.get("error", "")))
|
||||||
|
|
||||||
# 3) users
|
# 3) users (roles are NOT stored here — they live in the app's users table)
|
||||||
for name, first, last, email, pw, _role in USERS:
|
for name, first, last, email, pw in USERS:
|
||||||
status, body = _req("POST", "/api/add-user", {
|
status, body = _request("POST", "/api/add-user", {
|
||||||
"owner": ORG, "name": name, "displayName": f"{first} {last}",
|
"owner": ORG,
|
||||||
"firstName": first, "lastName": last, "email": email, "password": pw,
|
"name": name,
|
||||||
"phone": "", "type": "normal-user",
|
"displayName": f"{first} {last}",
|
||||||
}, auth)
|
"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", "")))
|
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("\nDone. Casdoor console: http://localhost:8080 (admin/123)")
|
||||||
print("Login in the app: http://localhost:3000/login")
|
print("Login in the app: http://localhost:3000/login")
|
||||||
|
|||||||
+15
-1
@@ -74,7 +74,9 @@ services:
|
|||||||
# v2.6 — Casdoor (OIDC identity provider)
|
# v2.6 — Casdoor (OIDC identity provider)
|
||||||
# Console: http://localhost:8080 (admin / 123)
|
# Console: http://localhost:8080 (admin / 123)
|
||||||
# Application "inventory-app" (org "inventory", client inventory-app-client)
|
# 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:
|
casdoor-postgres:
|
||||||
image: postgres:16
|
image: postgres:16
|
||||||
@@ -115,6 +117,15 @@ services:
|
|||||||
PROCUREMENT_MINIO_SECRET_KEY: minioadmin
|
PROCUREMENT_MINIO_SECRET_KEY: minioadmin
|
||||||
PROCUREMENT_MINIO_BUCKET: procurement-files
|
PROCUREMENT_MINIO_BUCKET: procurement-files
|
||||||
PROCUREMENT_MINIO_SECURE: "false"
|
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:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
@@ -130,6 +141,9 @@ services:
|
|||||||
frontend:
|
frontend:
|
||||||
build:
|
build:
|
||||||
context: ./frontend
|
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
|
container_name: procurement-frontend
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
Reference in New Issue
Block a user