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
Co-authored-by: Cursor <cursoragent@cursor.com>
188 lines
5.9 KiB
Python
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"]},
|
|
}
|