diff --git a/backend/migrations/041_bootstrap_superadmin.sql b/backend/migrations/041_bootstrap_superadmin.sql new file mode 100644 index 0000000..82c6bcb --- /dev/null +++ b/backend/migrations/041_bootstrap_superadmin.sql @@ -0,0 +1,20 @@ +-- Migration 041: Super-Admin für bestehende Installationen +-- Bisheriger Bootstrap (registration_role) setzte nur 'admin'. Viele Endpunkte +-- (z. B. Verein löschen, Super-Rolle vergeben) verlangen 'superadmin'. +-- Wenn noch kein Super-Admin existiert: den ältesten Nutzer mit role = admin hochstufen. + +UPDATE profiles p +SET role = 'superadmin', updated_at = NOW() +FROM ( + SELECT id + FROM profiles + WHERE lower(trim(COALESCE(role, ''))) = 'admin' + ORDER BY id ASC + LIMIT 1 +) sub +WHERE p.id = sub.id + AND NOT EXISTS ( + SELECT 1 + FROM profiles + WHERE lower(trim(COALESCE(role, ''))) = 'superadmin' + ); diff --git a/backend/routers/auth.py b/backend/routers/auth.py index bdfa1e3..8222213 100644 --- a/backend/routers/auth.py +++ b/backend/routers/auth.py @@ -187,9 +187,10 @@ def verification_link(token: str) -> str: def registration_role(cur, email_lower: str) -> str: """ - bootstrap: erste Registrierung in leerer DB → admin, + bootstrap: erste Registrierung in leerer DB → superadmin, oder E-Mail ∈ ADMIN_BOOTSTRAP_EMAILS (kommasepariert, Groß/Klein egal). + superadmin deckt alle Portal-Rechte ab (inkl. löschen / andere Super-Admins). Um alle Self-Regs als Trainer zu haben: AUTO_ADMIN_FIRST_USER=false und keine ADMIN_BOOTSTRAP_EMAILS. """ bootstrap = { @@ -198,7 +199,7 @@ def registration_role(cur, email_lower: str) -> str: if x.strip() } if email_lower in bootstrap: - return "admin" + return "superadmin" if os.getenv("AUTO_ADMIN_FIRST_USER", "true").strip().lower() not in ("1", "true", "yes"): return "trainer" cur.execute("SELECT COUNT(*) AS n FROM profiles") @@ -207,7 +208,7 @@ def registration_role(cur, email_lower: str) -> str: n = int(row["n"]) if row is not None else 0 except (KeyError, TypeError, ValueError): n = 0 - return "admin" if n == 0 else "trainer" + return "superadmin" if n == 0 else "trainer" # ── Helper: Send Email ──────────────────────────────────────────────────────── @@ -289,7 +290,7 @@ async def register(req: RegisterRequest, request: Request): verification_token = secrets.token_urlsafe(32) verification_expires = datetime.now(timezone.utc) + timedelta(hours=24) - # Rolle: erster Nutzer oder ADMIN_BOOTSTRAP_EMAILS → admin + # Rolle: erster Nutzer oder ADMIN_BOOTSTRAP_EMAILS → superadmin role = registration_role(cur, email) # Create profile (inactive until verified) — profiles.id ist SERIAL (INT), keine String-IDs einfügen. diff --git a/backend/version.py b/backend/version.py index 40b1e45..5aa8655 100644 --- a/backend/version.py +++ b/backend/version.py @@ -1,11 +1,11 @@ # Shinkan Jinkendo Version Information -APP_VERSION = "0.8.19" +APP_VERSION = "0.8.20" BUILD_DATE = "2026-05-05" -DB_SCHEMA_VERSION = "20260505040" +DB_SCHEMA_VERSION = "20260505041" MODULE_VERSIONS = { - "auth": "1.1.0", # Registrierung: optional requested_club_id → Beitrittsantrag + "auth": "1.2.0", # Erster/bootstrap Nutzer und ADMIN_BOOTSTRAP_EMAILS → superadmin (nicht mehr admin) "profiles": "1.3.0", # PUT role/tier (Portal-Admin); GET /profiles; pin_hash aus Liste entfernt "clubs": "0.4.0", # public-directory, members/directory; Vereins-GUI verwendet Endpoints "club_memberships": "1.0.0", @@ -26,6 +26,14 @@ MODULE_VERSIONS = { } CHANGELOG = [ + { + "version": "0.8.20", + "date": "2026-05-05", + "changes": [ + "Migration 041: wenn noch kein superadmin existiert, werden ältestes Profil mit role admin zu superadmin hochgestuft", + "Registrierung: erster Nutzer und ADMIN_BOOTSTRAP_EMAILS erhalten superadmin (vorher admin)", + ], + }, { "version": "0.8.19", "date": "2026-05-05", diff --git a/frontend/src/version.js b/frontend/src/version.js index b01802e..e705bf4 100644 --- a/frontend/src/version.js +++ b/frontend/src/version.js @@ -1,6 +1,6 @@ // Shinkan Jinkendo Frontend Version -export const APP_VERSION = "0.8.19" +export const APP_VERSION = "0.8.20" export const BUILD_DATE = "2026-05-05" export const PAGE_VERSIONS = {