diff --git a/l4d2web/routes/page_routes.py b/l4d2web/routes/page_routes.py index 6ed1efc..9fc9f37 100644 --- a/l4d2web/routes/page_routes.py +++ b/l4d2web/routes/page_routes.py @@ -1,7 +1,7 @@ import json from flask import Blueprint, Response, redirect, render_template, request -from sqlalchemy import select +from sqlalchemy import func, select, update from l4d2web.auth import current_user, require_admin, require_login from l4d2web.db import session_scope @@ -55,6 +55,76 @@ def admin_users() -> str: return render_template("admin_users.html", users=users) +@bp.post("/admin/users//deactivate") +@require_admin +def admin_users_deactivate(user_id: int) -> Response: + actor = current_user() + assert actor is not None + if actor.id == user_id: + return Response("cannot deactivate yourself", status=409) + with session_scope() as db: + target = db.scalar(select(User).where(User.id == user_id)) + if target is None: + return Response(status=404) + target.active = False + return redirect("/admin/users") + + +@bp.post("/admin/users//activate") +@require_admin +def admin_users_activate(user_id: int) -> Response: + with session_scope() as db: + target = db.scalar(select(User).where(User.id == user_id)) + if target is None: + return Response(status=404) + target.active = True + return redirect("/admin/users") + + +@bp.post("/admin/users//delete") +@require_admin +def admin_users_delete(user_id: int) -> Response: + actor = current_user() + assert actor is not None + if actor.id == user_id: + return Response("cannot delete yourself", status=409) + + with session_scope() as db: + target = db.scalar(select(User).where(User.id == user_id)) + if target is None: + return Response(status=404) + + if target.admin: + other_admins = db.scalar( + select(func.count(User.id)).where(User.admin.is_(True), User.id != user_id) + ) or 0 + if other_admins == 0: + return Response("cannot delete the last admin", status=409) + + owned_servers = db.scalar( + select(func.count(Server.id)).where(Server.user_id == user_id) + ) or 0 + owned_blueprints = db.scalar( + select(func.count(BlueprintModel.id)).where(BlueprintModel.user_id == user_id) + ) or 0 + owned_overlays = db.scalar( + select(func.count(Overlay.id)).where(Overlay.user_id == user_id) + ) or 0 + if owned_servers or owned_blueprints or owned_overlays: + return Response( + f"user owns content: {owned_servers} server(s), " + f"{owned_blueprints} blueprint(s), {owned_overlays} overlay(s) — " + "delete those first", + status=409, + ) + + # Job rows have nullable user_id — keep them as audit trail with the FK nulled out. + db.execute(update(Job).where(Job.user_id == user_id).values(user_id=None)) + db.delete(target) + + return redirect("/admin/users") + + @bp.get("/admin/jobs") @require_admin def admin_jobs() -> str: diff --git a/l4d2web/templates/admin_users.html b/l4d2web/templates/admin_users.html index 678fda9..aac267f 100644 --- a/l4d2web/templates/admin_users.html +++ b/l4d2web/templates/admin_users.html @@ -6,14 +6,70 @@

Users

- + + + + + + + + + + {% for user in users %} - + + + + + + + + {% else %} - + {% endfor %}
UsernameAdminCreatedUpdated
UsernameAdminActiveCreatedUpdatedActions
{{ user.username }}{{ "yes" if user.admin else "no" }}{{ user.created_at }}{{ user.updated_at }}
{{ user.username }}{{ "yes" if user.admin else "no" }}{{ "yes" if user.active else "no" }}{{ user.created_at }}{{ user.updated_at }} + {% if user.id == g.user.id %} + you + {% else %} + {% if user.active %} +
+ + +
+ {% else %} +
+ + +
+ {% endif %} + + {% endif %} +
No users found.
No users found.
+ +{% for user in users %} +{% if user.id != g.user.id %} + + + + + +{% endif %} +{% endfor %} {% endblock %}