admin berreich, user anmeldung
This commit is contained in:
+128
-9
@@ -159,7 +159,12 @@ app = FastAPI(title="Pingu Concerts", lifespan=lifespan)
|
|||||||
|
|
||||||
@app.middleware("http")
|
@app.middleware("http")
|
||||||
async def require_login(request: Request, call_next):
|
async def require_login(request: Request, call_next):
|
||||||
if request.url.path == "/login" or request.url.path.startswith("/static/"):
|
if (
|
||||||
|
request.url.path == "/login"
|
||||||
|
or request.url.path == "/register"
|
||||||
|
or request.url.path.startswith("/register/")
|
||||||
|
or request.url.path.startswith("/static/")
|
||||||
|
):
|
||||||
return await call_next(request)
|
return await call_next(request)
|
||||||
|
|
||||||
if get_current_user(request):
|
if get_current_user(request):
|
||||||
@@ -544,15 +549,55 @@ def resolve_venue(
|
|||||||
return selected_venue_id
|
return selected_venue_id
|
||||||
|
|
||||||
|
|
||||||
@app.get("/admin/invites")
|
def require_admin(request: Request):
|
||||||
def create_invite(request: Request):
|
|
||||||
user = get_current_user(request)
|
user = get_current_user(request)
|
||||||
|
|
||||||
if not user or not user["is_admin"]:
|
if not user or not user["is_admin"]:
|
||||||
return HTMLResponse(
|
return None
|
||||||
"<h1>Nicht erlaubt</h1>",
|
|
||||||
status_code=403,
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/admin", response_class=HTMLResponse)
|
||||||
|
def admin_page(request: Request):
|
||||||
|
user = require_admin(request)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
return HTMLResponse("<h1>Nicht erlaubt</h1>", status_code=403)
|
||||||
|
|
||||||
|
with get_db_connection() as connection:
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT id, username, email, display_name, is_admin, created_at
|
||||||
|
FROM users
|
||||||
|
ORDER BY is_admin DESC, username ASC
|
||||||
|
"""
|
||||||
)
|
)
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
|
||||||
|
users = [
|
||||||
|
{
|
||||||
|
"id": row[0],
|
||||||
|
"username": row[1],
|
||||||
|
"email": row[2],
|
||||||
|
"display_name": row[3] or row[1],
|
||||||
|
"is_admin": bool(row[4]),
|
||||||
|
"created_at": row[5].strftime("%d.%m.%Y"),
|
||||||
|
}
|
||||||
|
for row in rows
|
||||||
|
]
|
||||||
|
|
||||||
|
template = templates.get_template("admin.html")
|
||||||
|
return template.render(user=user, users=users, invite_url=None)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/admin/invites", response_class=HTMLResponse)
|
||||||
|
def create_invite(request: Request):
|
||||||
|
user = require_admin(request)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
return HTMLResponse("<h1>Nicht erlaubt</h1>", status_code=403)
|
||||||
|
|
||||||
token = secrets.token_urlsafe(32)
|
token = secrets.token_urlsafe(32)
|
||||||
|
|
||||||
@@ -576,10 +621,84 @@ def create_invite(request: Request):
|
|||||||
|
|
||||||
connection.commit()
|
connection.commit()
|
||||||
|
|
||||||
return {
|
with get_db_connection() as connection:
|
||||||
"invite_id": invite_id,
|
with connection.cursor() as cursor:
|
||||||
"invite_url": f"/register/{token}"
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT id, username, email, display_name, is_admin, created_at
|
||||||
|
FROM users
|
||||||
|
ORDER BY is_admin DESC, username ASC
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
|
||||||
|
users = [
|
||||||
|
{
|
||||||
|
"id": row[0],
|
||||||
|
"username": row[1],
|
||||||
|
"email": row[2],
|
||||||
|
"display_name": row[3] or row[1],
|
||||||
|
"is_admin": bool(row[4]),
|
||||||
|
"created_at": row[5].strftime("%d.%m.%Y"),
|
||||||
}
|
}
|
||||||
|
for row in rows
|
||||||
|
]
|
||||||
|
template = templates.get_template("admin.html")
|
||||||
|
return template.render(
|
||||||
|
user=user,
|
||||||
|
users=users,
|
||||||
|
invite_url=f"{str(request.base_url).rstrip('/')}/register/{token}",
|
||||||
|
invite_id=invite_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/admin/users/{user_id}")
|
||||||
|
def update_user_role(
|
||||||
|
request: Request,
|
||||||
|
user_id: int,
|
||||||
|
role: str = Form(...),
|
||||||
|
):
|
||||||
|
user = require_admin(request)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
return HTMLResponse("<h1>Nicht erlaubt</h1>", status_code=403)
|
||||||
|
if role not in {"admin", "user"}:
|
||||||
|
return HTMLResponse("<h1>Ungültige Rolle</h1>", status_code=400)
|
||||||
|
if user_id == user["id"] and role != "admin":
|
||||||
|
return HTMLResponse(
|
||||||
|
"<h1>Die eigenen Adminrechte können nicht entfernt werden.</h1>",
|
||||||
|
status_code=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
with get_db_connection() as connection:
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
"UPDATE users SET is_admin = %s WHERE id = %s",
|
||||||
|
(role == "admin", user_id),
|
||||||
|
)
|
||||||
|
connection.commit()
|
||||||
|
|
||||||
|
return RedirectResponse("/admin", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/admin/users/{user_id}/delete")
|
||||||
|
def delete_user(request: Request, user_id: int):
|
||||||
|
user = require_admin(request)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
return HTMLResponse("<h1>Nicht erlaubt</h1>", status_code=403)
|
||||||
|
if user_id == user["id"]:
|
||||||
|
return HTMLResponse(
|
||||||
|
"<h1>Der eigene Account kann nicht gelöscht werden.</h1>",
|
||||||
|
status_code=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
with get_db_connection() as connection:
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute("DELETE FROM users WHERE id = %s", (user_id,))
|
||||||
|
connection.commit()
|
||||||
|
|
||||||
|
return RedirectResponse("/admin", status_code=303)
|
||||||
|
|
||||||
@app.post("/register")
|
@app.post("/register")
|
||||||
def register_user(
|
def register_user(
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.edit-button {
|
.edit-button {
|
||||||
float: right;
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
background: #238636;
|
background: #238636;
|
||||||
@@ -41,6 +40,10 @@ a {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-actions {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
.edit-button:hover {
|
.edit-button:hover {
|
||||||
background: #2ea043;
|
background: #2ea043;
|
||||||
}
|
}
|
||||||
@@ -165,11 +168,6 @@ a {
|
|||||||
padding: 22px;
|
padding: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-button {
|
|
||||||
float: none;
|
|
||||||
margin: 0 0 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flyer-container {
|
.flyer-container {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Verwaltung · Pingu Concerts</title>
|
||||||
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
|
<style>
|
||||||
|
.admin-section { margin: 0 0 28px; padding: 24px; background: var(--surface); border: 1px solid var(--border); border-radius: 16px; }
|
||||||
|
.invite-link { display: block; margin-top: 14px; padding: 12px; overflow-wrap: anywhere; background: #0f1420; border: 1px solid var(--border); border-radius: 9px; color: #c4b5fd; }
|
||||||
|
.user-list { display: grid; gap: 12px; }
|
||||||
|
.user-row { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 12px; align-items: center; padding: 16px; background: #0f1420; border: 1px solid var(--border); border-radius: 10px; }
|
||||||
|
.user-meta { color: var(--muted); font-size: .9rem; margin-top: 4px; }
|
||||||
|
.role-form, .delete-form { margin: 0; display: flex; align-items: center; gap: 8px; }
|
||||||
|
.role-form select { padding: 9px; background: #0b0f19; color: var(--text); border: 1px solid var(--border); border-radius: 8px; }
|
||||||
|
.delete-button { padding: 9px 12px; color: #fecaca; border: 1px solid #7f1d1d; background: #450a0a; border-radius: 8px; cursor: pointer; }
|
||||||
|
@media (max-width: 700px) { .user-row { grid-template-columns: 1fr; } .role-form, .delete-form { width: 100%; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div class="header-inner">
|
||||||
|
<a href="/" class="logo">🎸 Pingu <span>Concerts</span></a>
|
||||||
|
<span>{{ user.display_name }}</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<div class="page-title">
|
||||||
|
<h1>⚙️ Verwaltung</h1>
|
||||||
|
<p>Einladungen und Benutzerkonten verwalten.</p>
|
||||||
|
</div>
|
||||||
|
<section class="admin-section">
|
||||||
|
<h2>Einladung erstellen</h2>
|
||||||
|
<p>Der Link kann einmalig verwendet werden, um einen neuen Account anzulegen.</p>
|
||||||
|
<form method="post" action="/admin/invites">
|
||||||
|
<button class="button" type="submit">+ Einladungslink erzeugen</button>
|
||||||
|
</form>
|
||||||
|
{% if invite_url %}
|
||||||
|
<label class="invite-link">{{ invite_url }}</label>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
<section class="admin-section">
|
||||||
|
<h2>Benutzer</h2>
|
||||||
|
<div class="user-list">
|
||||||
|
{% for managed_user in users %}
|
||||||
|
<div class="user-row">
|
||||||
|
<div>
|
||||||
|
<strong>{{ managed_user.display_name }}</strong>
|
||||||
|
<div class="user-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>
|
||||||
|
{% if managed_user.id != user.id %}
|
||||||
|
<form class="delete-form" method="post" action="/admin/users/{{ managed_user.id }}/delete" onsubmit="return confirm('Diesen Benutzer wirklich löschen?');">
|
||||||
|
<button class="delete-button" type="submit">Löschen</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -27,15 +27,6 @@
|
|||||||
← Zurück zum Kalender
|
← Zurück zum Kalender
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{% if can_edit %}
|
|
||||||
|
|
||||||
<a href="/concerts/{{ concert.id }}/edit" class="edit-button">
|
|
||||||
✏️ Konzert bearbeiten
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
|
|
||||||
<article class="concert-detail">
|
<article class="concert-detail">
|
||||||
|
|
||||||
{% if concert.flyer_path %}
|
{% if concert.flyer_path %}
|
||||||
@@ -180,6 +171,16 @@
|
|||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if can_edit %}
|
||||||
|
|
||||||
|
<div class="detail-actions">
|
||||||
|
<a href="/concerts/{{ concert.id }}/edit" class="edit-button">
|
||||||
|
✏️ Konzert bearbeiten
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
<section class="coming-soon">
|
<section class="coming-soon">
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,27 @@
|
|||||||
background: #2ea043;
|
background: #2ea043;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 18px;
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: #b8c1cc;
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-button:hover {
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: #58a6ff;
|
||||||
|
}
|
||||||
|
|
||||||
.concert-list {
|
.concert-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
@@ -144,6 +165,16 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="header-actions">
|
||||||
|
|
||||||
|
{% if user.is_admin %}
|
||||||
|
|
||||||
|
<a href="/admin" class="admin-button">
|
||||||
|
⚙️ Verwaltung
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="/concerts/new"
|
href="/concerts/new"
|
||||||
class="new-concert-button"
|
class="new-concert-button"
|
||||||
@@ -151,6 +182,8 @@
|
|||||||
+ Konzert hinzufügen
|
+ Konzert hinzufügen
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user