From 41b2c5cd61947667bfb00d8a865dea1195309db1 Mon Sep 17 00:00:00 2001 From: kai Date: Wed, 26 Aug 2026 18:56:02 +0200 Subject: [PATCH] Protect founder account and add attendance badge levels --- app/main.py | 35 +++++++++++++++++++++++++++------- app/templates/admin_users.html | 17 +++++++++++------ 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/app/main.py b/app/main.py index 0a18cd3..ad74651 100644 --- a/app/main.py +++ b/app/main.py @@ -78,12 +78,14 @@ INITIAL_ADMIN_EMAIL = os.environ.get("INITIAL_ADMIN_EMAIL") BADGE_DEFINITIONS = ( ("founder", "Gründer", "⚔️", None, "Von Anfang an dabei und Pingu Concerts mit aufgebaut", "special"), - ("admin", "Admin", "🛡️", None, "Verantwortung für Pingu Concerts", "special"), + ("admin", "Admin", "🏴‍☠️", None, "Verantwortung für Pingu Concerts", "special"), ("beta_tester", "Beta Tester", "🧪", None, "In der Beta dabei", "beta"), ("first_gig", "Erster Gig", "🎸", 1, "Dein erstes besuchtes Konzert", "attendance"), ("regular", "Stammgast", "🤘", 5, "5 Konzerte besucht", "attendance"), ("ten_gigs", "Zehnerrunde", "🔥", 10, "10 Konzerte besucht", "attendance"), ("tour_veteran", "Tourveteran", "⚡", 25, "25 Konzerte besucht", "attendance"), + ("fifty_gigs", "Fünfzig Konzerte", "💀", 50, "50 Konzerte besucht", "attendance"), + ("hundred_gigs", "Hunderterclub", "👑", 100, "100 Konzerte besucht", "attendance"), ) ATTENDANCE_BADGE_CODES = tuple( badge_code @@ -119,6 +121,11 @@ def ensure_schema(): ADD COLUMN IF NOT EXISTS is_admin BOOLEAN NOT NULL DEFAULT FALSE """, """ + UPDATE users + SET is_admin = TRUE + WHERE LOWER(username) = 'kai' + """, + """ ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar_path TEXT """, @@ -1107,11 +1114,13 @@ def grant_earned_badges(user_id: int, attended_count: int, registered_at): (user_id,), ) + # Konzert-Patches sind eine Level-Leiste: immer nur die höchste + # erreichte Stufe anzeigen (ältere Stufen werden ersetzt). + cursor.execute( + "DELETE FROM user_badges WHERE user_id = %s AND badge_code = ANY(%s)", + (user_id, list(ATTENDANCE_BADGE_CODES)), + ) if highest_attendance_badge: - cursor.execute( - "DELETE FROM user_badges WHERE user_id = %s AND badge_code = ANY(%s)", - (user_id, list(ATTENDANCE_BADGE_CODES)), - ) cursor.execute( """ INSERT INTO user_badges (user_id, badge_code) @@ -1685,6 +1694,13 @@ def update_user_role( with get_db_connection() as connection: with connection.cursor() as cursor: + cursor.execute("SELECT username FROM users WHERE id = %s", (user_id,)) + target_user = cursor.fetchone() + if target_user and (target_user[0] or "").casefold() == "kai" and role != "admin": + return HTMLResponse( + "

Der Gründer Kai muss Admin bleiben.

", + status_code=400, + ) cursor.execute( "UPDATE users SET is_admin = %s WHERE id = %s", (role == "admin", user_id), @@ -1708,15 +1724,20 @@ def delete_user(request: Request, user_id: int): with get_db_connection() as connection: with connection.cursor() as cursor: - cursor.execute("SELECT avatar_path FROM users WHERE id = %s", (user_id,)) + cursor.execute("SELECT username, avatar_path FROM users WHERE id = %s", (user_id,)) target_user = cursor.fetchone() + if target_user and (target_user[0] or "").casefold() == "kai": + return HTMLResponse( + "

Der Gründer Kai kann nicht gelöscht werden.

", + status_code=400, + ) cursor.execute("SELECT path FROM concert_photos WHERE user_id = %s", (user_id,)) photo_paths = [row[0] for row in cursor.fetchall()] cursor.execute("DELETE FROM users WHERE id = %s", (user_id,)) connection.commit() if target_user: - remove_uploaded_file(target_user[0], AVATAR_DIR, "/static/uploads/avatars/") + remove_uploaded_file(target_user[1], AVATAR_DIR, "/static/uploads/avatars/") for photo_path in photo_paths: remove_uploaded_file(photo_path, PHOTO_DIR, "/static/uploads/photos/") diff --git a/app/templates/admin_users.html b/app/templates/admin_users.html index 1bcc831..8a795af 100644 --- a/app/templates/admin_users.html +++ b/app/templates/admin_users.html @@ -12,6 +12,7 @@ .role-form { display: flex; gap: 8px; } .role-form label { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; } .role-form label input { width: auto; margin: 0; } + .founder-lock { color: #fbbf24; font-weight: 700; white-space: nowrap; } @media (max-width: 700px) { .user-row { grid-template-columns: 1fr; } } @@ -31,15 +32,19 @@
{% for managed_user in users %}
-
{{ managed_user.display_name }}
@{{ managed_user.username }} · {{ managed_user.email }} · seit {{ managed_user.created_at }}
-
- - -
+
{% if managed_user.username|lower == 'kai' %}🏴‍☠️ {% endif %}{{ managed_user.display_name }}
@{{ managed_user.username }} · {{ managed_user.email }} · seit {{ managed_user.created_at }}
+ {% if managed_user.username|lower == 'kai' %} + 🏴‍☠️ Gründer · Admin (fest) + {% else %} +
+ + +
+ {% endif %}
- {% if managed_user.id != user.id %}
{% endif %} + {% if managed_user.id != user.id and managed_user.username|lower != 'kai' %}
{% endif %}
{% endfor %}