Files
MMD e1495afee9 fix(auth): auth deps use their own DB session; smoke suites made dataset-independent
RUNTIME BUG (caught by live smoke tests): the global auth dependency
queried the request-scoped session, auto-beginning its transaction — any
endpoint opening 'async with db.begin()' afterwards crashed with
'transaction already begun' (POST /materials 500). get_current_user /
get_actor now resolve in a short-lived session (expire_on_commit=False
keeps the context usable detached); inventory/pm scopes keep passing the
request session explicitly.

Smoke suites rewritten against CURRENT contracts:
- assertions are behavior-based (shapes, envelopes, filters), no magic
  seed counts; dynamic lookups instead of hard-coded names/ids
- rerun-tolerant: tolerate 409-on-create by reusing prior artifacts
- aligned with current rules: revert blocked after inventory receipts
  (v2.6), attachments allowed in every status, termsheets optional at
  quote level
- SMOKE_USER/SMOKE_PASSWORD env for login (default tester/123456)

Verified on a fresh volume: migration + audit_log bootstrap, Casdoor
seed (bcrypt), demo data, and 29+76+61 smoke checks all green.
2026-09-19 00:49:15 +03:30

151 lines
5.9 KiB
Python

"""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()