All checks were successful
Deploy Development / deploy (push) Successful in 34s
Test Suite / pytest-backend (push) Successful in 9s
Test Suite / lint-backend (push) Successful in 2s
Test Suite / compose-smoke (push) Has been skipped
Test Suite / k6 /api/health Baseline (push) Successful in 18s
Test Suite / playwright-smoke (push) Successful in 12s
Co-authored-by: Cursor <cursoragent@cursor.com>
153 lines
4.3 KiB
Python
153 lines
4.3 KiB
Python
"""First-user / system-admin provisioning (bootstrap + registration)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import re
|
|
from typing import Any
|
|
|
|
from auth import create_session, get_user_by_email, hash_password
|
|
from db import get_connection
|
|
from psycopg2.extras import RealDictCursor
|
|
from fastapi import HTTPException
|
|
from services.actors import create_actor
|
|
from services.audit import log_audit
|
|
|
|
|
|
def user_count() -> int:
|
|
conn = get_connection()
|
|
try:
|
|
with conn.cursor() as cur:
|
|
cur.execute("SELECT COUNT(*) FROM users")
|
|
return int(cur.fetchone()[0])
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def registration_open() -> bool:
|
|
return user_count() == 0
|
|
|
|
|
|
def setup_status() -> dict[str, Any]:
|
|
count = user_count()
|
|
return {
|
|
"has_users": count > 0,
|
|
"registration_open": count == 0,
|
|
"user_count": count,
|
|
}
|
|
|
|
|
|
def _slugify(value: str) -> str:
|
|
slug = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
|
|
return (slug[:48] or "default")
|
|
|
|
|
|
def provision_system_admin(
|
|
*,
|
|
email: str,
|
|
password: str,
|
|
display_name: str,
|
|
tenant_slug: str | None = None,
|
|
tenant_name: str | None = None,
|
|
source: str = "bootstrap",
|
|
) -> dict[str, Any]:
|
|
"""Create portal admin, default tenant, owner membership and human actor."""
|
|
if user_count() > 0:
|
|
raise ValueError("users_already_exist")
|
|
|
|
normalized_email = email.strip().lower()
|
|
slug = tenant_slug or os.getenv("KAIRO_BOOTSTRAP_TENANT_SLUG") or "default"
|
|
name = tenant_name or os.getenv("KAIRO_BOOTSTRAP_TENANT_NAME") or "Default Tenant"
|
|
|
|
conn = get_connection()
|
|
try:
|
|
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
|
cur.execute(
|
|
"""
|
|
INSERT INTO tenants (slug, name)
|
|
VALUES (%s, %s)
|
|
ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name
|
|
RETURNING id, slug, name
|
|
""",
|
|
(slug, name),
|
|
)
|
|
tenant = dict(cur.fetchone())
|
|
|
|
cur.execute(
|
|
"""
|
|
INSERT INTO users (email, password_hash, display_name, portal_role)
|
|
VALUES (%s, %s, %s, 'admin')
|
|
RETURNING id, email, display_name, portal_role
|
|
""",
|
|
(normalized_email, hash_password(password), display_name.strip()),
|
|
)
|
|
user = dict(cur.fetchone())
|
|
user_id = str(user["id"])
|
|
tenant_id = str(tenant["id"])
|
|
|
|
cur.execute(
|
|
"""
|
|
INSERT INTO tenant_memberships (tenant_id, user_id, tenant_role)
|
|
VALUES (%s, %s, 'owner')
|
|
""",
|
|
(tenant_id, user_id),
|
|
)
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
|
|
create_actor(
|
|
tenant_id=tenant_id,
|
|
actor_type="human",
|
|
name=display_name.strip(),
|
|
user_id=user_id,
|
|
)
|
|
log_audit(
|
|
"auth.register_system_admin",
|
|
user_id=user_id,
|
|
tenant_id=tenant_id,
|
|
details={"email": normalized_email, "source": source, "tenant_slug": slug},
|
|
)
|
|
|
|
session = create_session(user_id, tenant_id)
|
|
return {
|
|
"token": session["token"],
|
|
"expires_at": session["expires_at"].isoformat(),
|
|
"user": {
|
|
"id": user_id,
|
|
"email": user["email"],
|
|
"display_name": user["display_name"],
|
|
"portal_role": user["portal_role"],
|
|
},
|
|
"tenant": {"id": tenant_id, "slug": tenant["slug"], "name": tenant["name"]},
|
|
}
|
|
|
|
|
|
def register_system_admin(
|
|
*,
|
|
email: str,
|
|
password: str,
|
|
display_name: str,
|
|
organization_name: str | None = None,
|
|
) -> dict[str, Any]:
|
|
if get_user_by_email(email):
|
|
raise HTTPException(status_code=409, detail="E-Mail bereits registriert")
|
|
|
|
if not registration_open():
|
|
raise HTTPException(
|
|
status_code=403,
|
|
detail="Registrierung geschlossen — Systemadministrator ist bereits eingerichtet",
|
|
)
|
|
|
|
org = (organization_name or display_name or "Default Tenant").strip()
|
|
slug = _slugify(org)
|
|
|
|
return provision_system_admin(
|
|
email=email,
|
|
password=password,
|
|
display_name=display_name.strip(),
|
|
tenant_slug=slug,
|
|
tenant_name=org,
|
|
source="registration",
|
|
)
|