feat(l4d2-web): add admin landing and system pages
This commit is contained in:
parent
feab09db07
commit
4b326736fe
5 changed files with 123 additions and 1 deletions
|
|
@ -6,7 +6,7 @@ from sqlalchemy import select
|
||||||
from l4d2web.auth import current_user, require_admin, require_login
|
from l4d2web.auth import current_user, require_admin, require_login
|
||||||
from l4d2web.db import session_scope
|
from l4d2web.db import session_scope
|
||||||
from l4d2web.models import Blueprint as BlueprintModel
|
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__)
|
bp = Blueprint("pages", __name__)
|
||||||
|
|
@ -18,6 +18,33 @@ def dashboard() -> str:
|
||||||
return render_template("dashboard.html")
|
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")
|
@bp.get("/servers")
|
||||||
@require_login
|
@require_login
|
||||||
def servers_page() -> str:
|
def servers_page() -> str:
|
||||||
|
|
|
||||||
13
l4d2web/templates/admin.html
Normal file
13
l4d2web/templates/admin.html
Normal 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 %}
|
||||||
27
l4d2web/templates/admin_jobs.html
Normal file
27
l4d2web/templates/admin_jobs.html
Normal 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 %}
|
||||||
19
l4d2web/templates/admin_users.html
Normal file
19
l4d2web/templates/admin_users.html
Normal 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 %}
|
||||||
|
|
@ -137,6 +137,42 @@ def test_servers_page_links_server_names(auth_client_with_server) -> None:
|
||||||
assert ">details<" not in text
|
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:
|
def test_blueprint_page_private_visibility(user_client_and_other_blueprint) -> None:
|
||||||
client, blueprint_id = user_client_and_other_blueprint
|
client, blueprint_id = user_client_and_other_blueprint
|
||||||
response = client.get(f"/blueprints/{blueprint_id}")
|
response = client.get(f"/blueprints/{blueprint_id}")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue