feat(admin): add project metadata config

This commit is contained in:
2026-06-12 15:08:37 +08:00
parent 23c008f602
commit a6e7a2e75c
15 changed files with 2460 additions and 10 deletions
@@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""Build metadata user sync payloads from an old-user to Keycloak mapping CSV."""
from __future__ import annotations
import argparse
import csv
import json
from pathlib import Path
REQUIRED_COLUMNS = {"keycloak_id", "username", "email"}
def parse_bool(value: str | None) -> bool:
if value is None or value == "":
return True
return value.strip().lower() not in {"0", "false", "no", "n", "disabled"}
def build_payload(mapping_csv: Path) -> dict:
with mapping_csv.open(newline="", encoding="utf-8") as handle:
reader = csv.DictReader(handle)
missing = REQUIRED_COLUMNS.difference(reader.fieldnames or [])
if missing:
raise SystemExit(f"missing required CSV columns: {', '.join(sorted(missing))}")
users = []
for row in reader:
users.append(
{
"keycloak_id": row["keycloak_id"].strip(),
"username": row["username"].strip(),
"email": row["email"].strip(),
"role": (row.get("role") or "user").strip().lower(),
"is_active": parse_bool(row.get("is_active")),
}
)
return {"users": users}
def main() -> None:
parser = argparse.ArgumentParser(
description=(
"Convert old local user mappings into a JSON body for "
"POST /api/v1/admin/users/sync/batch. Passwords are never migrated."
)
)
parser.add_argument("mapping_csv", type=Path)
parser.add_argument("-o", "--output", type=Path)
args = parser.parse_args()
payload = build_payload(args.mapping_csv)
content = json.dumps(payload, ensure_ascii=False, indent=2)
if args.output:
args.output.write_text(content + "\n", encoding="utf-8")
else:
print(content)
if __name__ == "__main__":
main()