feat(l4d2-web): add admin landing and system pages

This commit is contained in:
mwiegand 2026-05-06 12:09:36 +02:00
parent feab09db07
commit 4b326736fe
No known key found for this signature in database
5 changed files with 123 additions and 1 deletions

View file

@ -6,7 +6,7 @@ from sqlalchemy import select
from l4d2web.auth import current_user, require_admin, require_login
from l4d2web.db import session_scope
from l4d2web.models import Blueprint as BlueprintModel
from l4d2web.models import BlueprintOverlay, Job, Overlay, Server
from l4d2web.models import BlueprintOverlay, Job, Overlay, Server, User
bp = Blueprint("pages", __name__)
@ -18,6 +18,33 @@ def dashboard() -> str:
return render_template("dashboard.html")
@bp.get("/admin")
@require_admin
def admin_home() -> str:
return render_template("admin.html")
@bp.get("/admin/users")
@require_admin
def admin_users() -> str:
with session_scope() as db:
users = db.scalars(select(User).order_by(User.username)).all()
return render_template("admin_users.html", users=users)
@bp.get("/admin/jobs")
@require_admin
def admin_jobs() -> str:
with session_scope() as db:
rows = db.execute(
select(Job, User, Server)
.join(User, User.id == Job.user_id)
.outerjoin(Server, Server.id == Job.server_id)
.order_by(Job.created_at.desc())
).all()
return render_template("admin_jobs.html", rows=rows)
@bp.get("/servers")
@require_login
def servers_page() -> str:

View file

@ -0,0 +1,13 @@
{% extends "base.html" %}
{% block title %}Admin | left4me{% endblock %}
{% block content %}
<section class="panel">
<h1>Admin</h1>
<ul class="link-list">
<li><a href="/admin/users">Users</a></li>
<li><a href="/admin/jobs">Jobs</a></li>
</ul>
</section>
{% endblock %}

View file

@ -0,0 +1,27 @@
{% extends "base.html" %}
{% block title %}Admin Jobs | left4me{% endblock %}
{% block content %}
<section class="panel">
<h1>Jobs</h1>
<table class="table">
<thead><tr><th>ID</th><th>Operation</th><th>State</th><th>User</th><th>Server</th><th>Created</th><th>Finished</th></tr></thead>
<tbody>
{% for job, user, server in rows %}
<tr>
<td>{{ job.id }}</td>
<td>{{ job.operation }}</td>
<td>{{ job.state }}</td>
<td>{{ user.username }}</td>
<td>{% if server %}<a href="/servers/{{ server.id }}">{{ server.name }}</a>{% else %}-{% endif %}</td>
<td>{{ job.created_at }}</td>
<td>{{ job.finished_at or "-" }}</td>
</tr>
{% else %}
<tr><td colspan="7" class="muted">No jobs found.</td></tr>
{% endfor %}
</tbody>
</table>
</section>
{% endblock %}

View file

@ -0,0 +1,19 @@
{% extends "base.html" %}
{% block title %}Admin Users | left4me{% endblock %}
{% block content %}
<section class="panel">
<h1>Users</h1>
<table class="table">
<thead><tr><th>Username</th><th>Admin</th><th>Created</th><th>Updated</th></tr></thead>
<tbody>
{% for user in users %}
<tr><td>{{ user.username }}</td><td>{{ "yes" if user.admin else "no" }}</td><td>{{ user.created_at }}</td><td>{{ user.updated_at }}</td></tr>
{% else %}
<tr><td colspan="4" class="muted">No users found.</td></tr>
{% endfor %}
</tbody>
</table>
</section>
{% endblock %}

View file

@ -137,6 +137,42 @@ def test_servers_page_links_server_names(auth_client_with_server) -> None:
assert ">details<" not in text
def test_non_admin_does_not_see_admin_nav(auth_client_with_server) -> None:
response = auth_client_with_server.get("/dashboard")
text = response.get_data(as_text=True)
assert response.status_code == 200
assert 'href="/admin"' not in text
def test_admin_can_use_admin_pages(tmp_path, monkeypatch) -> None:
db_url = f"sqlite:///{tmp_path/'admin-pages.db'}"
monkeypatch.setenv("DATABASE_URL", db_url)
app = create_app({"TESTING": True, "DATABASE_URL": db_url, "SECRET_KEY": "test"})
init_db()
with session_scope() as session:
admin = User(username="admin", password_digest=hash_password("secret"), admin=True)
session.add(admin)
session.flush()
admin_id = admin.id
client = app.test_client()
with client.session_transaction() as sess:
sess["user_id"] = admin_id
assert client.get("/admin").status_code == 200
assert client.get("/admin/users").status_code == 200
assert client.get("/admin/jobs").status_code == 200
assert 'href="/admin"' in client.get("/dashboard").get_data(as_text=True)
def test_non_admin_cannot_open_admin_pages(auth_client_with_server) -> None:
assert auth_client_with_server.get("/admin").status_code == 403
assert auth_client_with_server.get("/admin/users").status_code == 403
assert auth_client_with_server.get("/admin/jobs").status_code == 403
def test_blueprint_page_private_visibility(user_client_and_other_blueprint) -> None:
client, blueprint_id = user_client_and_other_blueprint
response = client.get(f"/blueprints/{blueprint_id}")