Protect founder account and add attendance badge levels
This commit is contained in:
+25
-4
@@ -78,12 +78,14 @@ INITIAL_ADMIN_EMAIL = os.environ.get("INITIAL_ADMIN_EMAIL")
|
|||||||
|
|
||||||
BADGE_DEFINITIONS = (
|
BADGE_DEFINITIONS = (
|
||||||
("founder", "Gründer", "⚔️", None, "Von Anfang an dabei und Pingu Concerts mit aufgebaut", "special"),
|
("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"),
|
("beta_tester", "Beta Tester", "🧪", None, "In der Beta dabei", "beta"),
|
||||||
("first_gig", "Erster Gig", "🎸", 1, "Dein erstes besuchtes Konzert", "attendance"),
|
("first_gig", "Erster Gig", "🎸", 1, "Dein erstes besuchtes Konzert", "attendance"),
|
||||||
("regular", "Stammgast", "🤘", 5, "5 Konzerte besucht", "attendance"),
|
("regular", "Stammgast", "🤘", 5, "5 Konzerte besucht", "attendance"),
|
||||||
("ten_gigs", "Zehnerrunde", "🔥", 10, "10 Konzerte besucht", "attendance"),
|
("ten_gigs", "Zehnerrunde", "🔥", 10, "10 Konzerte besucht", "attendance"),
|
||||||
("tour_veteran", "Tourveteran", "⚡", 25, "25 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(
|
ATTENDANCE_BADGE_CODES = tuple(
|
||||||
badge_code
|
badge_code
|
||||||
@@ -119,6 +121,11 @@ def ensure_schema():
|
|||||||
ADD COLUMN IF NOT EXISTS is_admin BOOLEAN NOT NULL DEFAULT FALSE
|
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
|
ALTER TABLE users
|
||||||
ADD COLUMN IF NOT EXISTS avatar_path TEXT
|
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,),
|
(user_id,),
|
||||||
)
|
)
|
||||||
|
|
||||||
if highest_attendance_badge:
|
# Konzert-Patches sind eine Level-Leiste: immer nur die höchste
|
||||||
|
# erreichte Stufe anzeigen (ältere Stufen werden ersetzt).
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"DELETE FROM user_badges WHERE user_id = %s AND badge_code = ANY(%s)",
|
"DELETE FROM user_badges WHERE user_id = %s AND badge_code = ANY(%s)",
|
||||||
(user_id, list(ATTENDANCE_BADGE_CODES)),
|
(user_id, list(ATTENDANCE_BADGE_CODES)),
|
||||||
)
|
)
|
||||||
|
if highest_attendance_badge:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO user_badges (user_id, badge_code)
|
INSERT INTO user_badges (user_id, badge_code)
|
||||||
@@ -1685,6 +1694,13 @@ def update_user_role(
|
|||||||
|
|
||||||
with get_db_connection() as connection:
|
with get_db_connection() as connection:
|
||||||
with connection.cursor() as cursor:
|
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(
|
cursor.execute(
|
||||||
"UPDATE users SET is_admin = %s WHERE id = %s",
|
"UPDATE users SET is_admin = %s WHERE id = %s",
|
||||||
(role == "admin", user_id),
|
(role == "admin", user_id),
|
||||||
@@ -1708,15 +1724,20 @@ def delete_user(request: Request, user_id: int):
|
|||||||
|
|
||||||
with get_db_connection() as connection:
|
with get_db_connection() as connection:
|
||||||
with connection.cursor() as cursor:
|
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()
|
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,))
|
cursor.execute("SELECT path FROM concert_photos WHERE user_id = %s", (user_id,))
|
||||||
photo_paths = [row[0] for row in cursor.fetchall()]
|
photo_paths = [row[0] for row in cursor.fetchall()]
|
||||||
cursor.execute("DELETE FROM users WHERE id = %s", (user_id,))
|
cursor.execute("DELETE FROM users WHERE id = %s", (user_id,))
|
||||||
connection.commit()
|
connection.commit()
|
||||||
|
|
||||||
if target_user:
|
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:
|
for photo_path in photo_paths:
|
||||||
remove_uploaded_file(photo_path, PHOTO_DIR, "/static/uploads/photos/")
|
remove_uploaded_file(photo_path, PHOTO_DIR, "/static/uploads/photos/")
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
.role-form { display: flex; gap: 8px; }
|
.role-form { display: flex; gap: 8px; }
|
||||||
.role-form label { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
|
.role-form label { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
|
||||||
.role-form label input { width: auto; margin: 0; }
|
.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; } }
|
@media (max-width: 700px) { .user-row { grid-template-columns: 1fr; } }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@@ -31,15 +32,19 @@
|
|||||||
<div class="admin-list">
|
<div class="admin-list">
|
||||||
{% for managed_user in users %}
|
{% for managed_user in users %}
|
||||||
<div class="admin-row user-row">
|
<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>
|
<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 }}">
|
<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>
|
<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>
|
<button class="button" type="submit">Speichern</button>
|
||||||
</form>
|
</form>
|
||||||
|
{% endif %}
|
||||||
<div class="role-form">
|
<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>
|
<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>
|
</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>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user