Protect founder account and add attendance badge levels

This commit is contained in:
kai
2026-08-26 18:56:02 +02:00
parent c4fa6f077c
commit 41b2c5cd61
2 changed files with 39 additions and 13 deletions
+28 -7
View File
@@ -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(
"<h1>Der Gründer Kai muss Admin bleiben.</h1>",
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(
"<h1>Der Gründer Kai kann nicht gelöscht werden.</h1>",
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/")
+11 -6
View File
@@ -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; } }
</style>
</head>
@@ -31,15 +32,19 @@
<div class="admin-list">
{% for managed_user in users %}
<div class="admin-row user-row">
<div><strong>{{ managed_user.display_name }}</strong><div class="admin-meta">@{{ managed_user.username }} · {{ managed_user.email }} · seit {{ managed_user.created_at }}</div></div>
<form class="role-form" method="post" action="/admin/users/{{ managed_user.id }}">
<select name="role" aria-label="Rolle für {{ managed_user.username }}"><option value="user" {% if not managed_user.is_admin %}selected{% endif %}>Benutzer</option><option value="admin" {% if managed_user.is_admin %}selected{% endif %}>Admin</option></select>
<button class="button" type="submit">Speichern</button>
</form>
<div><strong>{% if managed_user.username|lower == 'kai' %}🏴‍☠️ {% endif %}{{ managed_user.display_name }}</strong><div class="admin-meta">@{{ managed_user.username }} · {{ managed_user.email }} · seit {{ managed_user.created_at }}</div></div>
{% if managed_user.username|lower == 'kai' %}
<span class="founder-lock">🏴‍☠️ Gründer · Admin (fest)</span>
{% else %}
<form class="role-form" method="post" action="/admin/users/{{ managed_user.id }}">
<select name="role" aria-label="Rolle für {{ managed_user.username }}"><option value="user" {% if not managed_user.is_admin %}selected{% endif %}>Benutzer</option><option value="admin" {% if managed_user.is_admin %}selected{% endif %}>🏴‍☠️ Admin</option></select>
<button class="button" type="submit">Speichern</button>
</form>
{% endif %}
<div class="role-form">
<form method="post" action="/admin/users/{{ managed_user.id }}/account-link"><input type="hidden" name="purpose" value="password_reset"><button class="button button-secondary" type="submit">Passwort-Reset</button></form>
</div>
{% if managed_user.id != user.id %}<form method="post" action="/admin/users/{{ managed_user.id }}/delete" onsubmit="return confirm('Diesen Benutzer wirklich löschen?');"><button class="admin-danger" type="submit">Löschen</button></form>{% endif %}
{% if managed_user.id != user.id and managed_user.username|lower != 'kai' %}<form method="post" action="/admin/users/{{ managed_user.id }}/delete" onsubmit="return confirm('Diesen Benutzer wirklich löschen?');"><button class="admin-danger" type="submit">Löschen</button></form>{% endif %}
</div>
{% endfor %}
</div>