"""Bootstrap first admin user, tenant, membership and human actor from environment.""" from __future__ import annotations import os from psycopg2.extras import RealDictCursor from auth import create_session, hash_password from db import get_connection from services.actors import create_actor from services.audit import log_audit def _env(name: str) -> str: return os.getenv(name, "").strip() def bootstrap_admin_if_needed() -> None: if _env("SKIP_BOOTSTRAP").lower() in ("1", "true", "yes"): return email = _env("KAIRO_BOOTSTRAP_ADMIN_EMAIL") password = _env("KAIRO_BOOTSTRAP_ADMIN_PASSWORD") if not email or not password: return tenant_slug = _env("KAIRO_BOOTSTRAP_TENANT_SLUG") or "default" tenant_name = _env("KAIRO_BOOTSTRAP_TENANT_NAME") or "Default Tenant" display_name = _env("KAIRO_BOOTSTRAP_ADMIN_NAME") or "Kairo Admin" conn = get_connection() try: with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute("SELECT COUNT(*) AS n FROM users") if cur.fetchone()["n"] > 0: return cur.execute( """ INSERT INTO tenants (slug, name) VALUES (%s, %s) ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name RETURNING id """, (tenant_slug, tenant_name), ) tenant_row = cur.fetchone() if not tenant_row: cur.execute("SELECT id FROM tenants WHERE slug = %s", (tenant_slug,)) tenant_row = cur.fetchone() tenant_id = str(tenant_row["id"]) cur.execute( """ INSERT INTO users (email, password_hash, display_name, portal_role) VALUES (%s, %s, %s, 'admin') RETURNING id """, (email.lower(), hash_password(password), display_name), ) user_id = str(cur.fetchone()["id"]) cur.execute( """ INSERT INTO tenant_memberships (tenant_id, user_id, tenant_role) VALUES (%s, %s, 'owner') ON CONFLICT (tenant_id, user_id) DO NOTHING """, (tenant_id, user_id), ) conn.commit() finally: conn.close() create_actor( tenant_id=tenant_id, actor_type="human", name=display_name, user_id=user_id, ) log_audit( "bootstrap.admin_created", user_id=user_id, tenant_id=tenant_id, details={"email": email.lower(), "tenant_slug": tenant_slug}, ) print(f"[bootstrap] Admin user and tenant '{tenant_slug}' created")