"""Seed Casdoor (dev) — org `inventory`, app `inventory-app`, users. 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 # 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 bcrypt BASE = os.environ.get("CASDOOR_BASE", "http://localhost:8080") ADMIN = os.environ.get("CASDOOR_ADMIN", "admin") # Casdoor's own bootstrap default for the built-in console admin is "123" — # the seeder LOGS IN with it (it cannot change the console password). # Override with CASDOOR_ADMIN_PW after rotating (docs/SECRET-MANAGEMENT.md); # production MUST set a real value here once the console password is rotated. ADMIN_PW = os.environ.get("CASDOOR_ADMIN_PW", "123") ORG = "inventory" APP = "inventory-app" CLIENT_ID = "inventory-app-client" # Must MATCH the API's PROCUREMENT_CASDOOR_CLIENT_SECRET (compose default # below) — changing one without the other breaks POST /auth/login. CLIENT_SECRET = os.environ.get("CASDOOR_CLIENT_SECRET", "inventory-app-secret-2026") REDIRECT_URI = "http://localhost/login" # Traefik edge (v1.3); unused by the in-page flow # One password for all seeded dev users — override with CASDOOR_SEED_PASSWORD; # stored as a bcrypt hash (passwordType "bcrypt"), never plaintext. SEED_PASSWORD = os.environ.get("CASDOOR_SEED_PASSWORD", "123456") USERS = [ # (name, first_name, last_name, email) ("razaei", "آقای", "رضایی", "razaei@sample.ir"), ("mousavi", "آقای", "موسوی", "mousavi@sample.ir"), ("mohaghegh", "آقای", "محقق", "mohaghegh@sample.ir"), ("tester", "کاربر", "تست", "tester@sample.ir"), ] 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") jar = cookies if cookies is not None else http.cookiejar.CookieJar() opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(jar)) try: 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 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 _bcrypt(pw: str) -> str: return bcrypt.hashpw(pw.encode("utf-8"), bcrypt.gensalt()).decode("ascii") def main(): cookies = _login() if cookies is None: return # 1) organization (bcrypt — passwords are never stored plaintext) status, body = _request("POST", "/api/add-organization", { "owner": "admin", "name": ORG, "displayName": "سازمان تدارکات", "passwordType": "bcrypt", }, cookies) print("add-organization:", status, body.get("msg", body.get("error", ""))) # 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": "سامانه تدارکات و انبار", "organization": ORG, "clientId": CLIENT_ID, "clientSecret": CLIENT_SECRET, "redirectUris": [REDIRECT_URI], "grantTypes": ["authorization_code", "refresh_token", "password"], "signinMethods": [{"name": "Password"}], "enableSignUp": False, "tokenFormat": "JWT", "expireInHours": 24, }, cookies) print("add-application:", status, body.get("msg", body.get("error", ""))) # 3) users (roles are NOT stored here — they live in the app's users table) pw_hash = _bcrypt(SEED_PASSWORD) for name, first, last, email 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_hash, "passwordType": "bcrypt", "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,password", { "passwordType": "bcrypt", "password": pw_hash, }, cookies) print(f"set-password-type {name}:", status, body.get("msg", body.get("error", ""))) print("\nDone. Casdoor console: http://localhost:8080") print(f"Seeded users password: {SEED_PASSWORD} (change via CASDOOR_SEED_PASSWORD)") print("Login in the app: http://localhost/login") if __name__ == "__main__": main()