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