Kairo-Jinkendo/backend/services/dev_admin.py
Lars 88792eeb47
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 1s
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 13s
Dev-Seed: lars@stommer.com als Portal-Admin bei jedem Start sicherstellen.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-04 22:25:11 +02:00

188 lines
5.9 KiB
Python

"""Dev-only admin seed — ensures a known portal admin exists."""
from __future__ import annotations
import os
from typing import Iterable
from auth import hash_password
from db import get_connection
from psycopg2.extras import RealDictCursor
from services.actors import create_actor, get_human_actor
from services.audit import log_audit
from services.registration import provision_system_admin, user_count
DEV_ADMIN_EMAIL = "lars@stommer.com"
DEV_ADMIN_PASSWORD = "12345678"
DEV_ADMIN_NAME = "Lars Stommer"
DEV_TENANT_SLUG = "stommer"
DEV_TENANT_NAME = "Stommer"
def _normalize_emails(emails: Iterable[str]) -> tuple[str, ...]:
return tuple(sorted({email.strip().lower() for email in emails if email.strip()}))
def delete_users_except(keep_emails: Iterable[str]) -> int:
"""Remove all users not in keep_emails (with FK-safe order). Returns deleted count."""
keep = _normalize_emails(keep_emails)
if not keep:
raise ValueError("keep_emails must not be empty")
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute("DELETE FROM actors WHERE actor_type = 'human' AND user_id IS NULL")
cur.execute(
"""
DELETE FROM actors
WHERE user_id IN (
SELECT id FROM users WHERE lower(email) <> ALL(%s)
)
""",
(list(keep),),
)
cur.execute(
"""
DELETE FROM tenant_memberships
WHERE user_id IN (
SELECT id FROM users WHERE lower(email) <> ALL(%s)
)
""",
(list(keep),),
)
cur.execute(
"""
DELETE FROM sessions
WHERE user_id IN (
SELECT id FROM users WHERE lower(email) <> ALL(%s)
)
""",
(list(keep),),
)
cur.execute(
"""
DELETE FROM audit_log
WHERE user_id IN (
SELECT id FROM users WHERE lower(email) <> ALL(%s)
)
""",
(list(keep),),
)
cur.execute(
"DELETE FROM users WHERE lower(email) <> ALL(%s) RETURNING email",
(list(keep),),
)
removed = cur.fetchall()
conn.commit()
return len(removed)
finally:
conn.close()
def _ensure_tenant(conn, *, slug: str, name: str) -> dict:
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),
)
return dict(cur.fetchone())
def ensure_dev_admin(
*,
email: str = DEV_ADMIN_EMAIL,
password: str = DEV_ADMIN_PASSWORD,
display_name: str = DEV_ADMIN_NAME,
tenant_slug: str = DEV_TENANT_SLUG,
tenant_name: str = DEV_TENANT_NAME,
) -> dict:
"""Dev seed: remove other users, ensure portal admin + tenant + human actor."""
if os.getenv("ENVIRONMENT", "development").strip().lower() == "production":
raise RuntimeError("ensure_dev_admin darf nicht in Production laufen")
normalized_email = email.strip().lower()
removed = delete_users_except([normalized_email])
if removed:
print(f"[dev_admin] {removed} fremde User entfernt")
if user_count() == 0:
result = provision_system_admin(
email=normalized_email,
password=password,
display_name=display_name,
tenant_slug=tenant_slug,
tenant_name=tenant_name,
source="dev_seed",
)
print(f"[dev_admin] Portal-Admin angelegt: {normalized_email}")
return result
conn = get_connection()
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
tenant = _ensure_tenant(conn, slug=tenant_slug, name=tenant_name)
tenant_id = str(tenant["id"])
cur.execute(
"""
UPDATE users
SET password_hash = %s,
display_name = %s,
portal_role = 'admin',
updated_at = NOW()
WHERE lower(email) = %s
RETURNING id, email, display_name, portal_role
""",
(hash_password(password), display_name.strip(), normalized_email),
)
row = cur.fetchone()
if not row:
raise RuntimeError(f"Dev-Admin {normalized_email} fehlt nach Bereinigung")
user = dict(row)
user_id = str(user["id"])
cur.execute(
"""
INSERT INTO tenant_memberships (tenant_id, user_id, tenant_role)
VALUES (%s, %s, 'owner')
ON CONFLICT (tenant_id, user_id) DO UPDATE SET tenant_role = 'owner'
""",
(tenant_id, user_id),
)
conn.commit()
finally:
conn.close()
if not get_human_actor(tenant_id, user_id):
create_actor(
tenant_id=tenant_id,
actor_type="human",
name=display_name.strip(),
user_id=user_id,
)
log_audit(
"auth.dev_admin_ensure",
user_id=user_id,
tenant_id=tenant_id,
details={"email": normalized_email, "removed_users": removed},
)
print(f"[dev_admin] Portal-Admin bestätigt: {normalized_email}")
return {
"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"]},
}