Initial commit: Portal Auth Admin Dashboard

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-18 08:18:50 +02:00
commit 7caa62a428
20 changed files with 1347 additions and 0 deletions

43
templates/base.html Normal file
View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}Portal Auth Admin{% endblock %}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Outfit:wght@400;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<header class="site-header">
<div class="brand">
<span class="brand-icon"></span>
<span class="brand-text">Portal Auth</span>
</div>
{% if session.get('admin_username') %}
<nav class="nav">
<a href="{{ url_for('index') }}">Dashboard</a>
<a href="{{ url_for('table_view', name='users') }}">Users</a>
<a href="{{ url_for('table_view', name='sessions') }}">Sessions</a>
<a href="{{ url_for('table_view', name='auth_logs') }}">Auth logs</a>
<a href="{{ url_for('table_view', name='api_tokens') }}">API tokens</a>
<span class="user">{{ session.admin_username }}</span>
<a href="{{ url_for('logout') }}" class="logout">Log out</a>
</nav>
{% endif %}
</header>
<main class="main">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<ul class="flashes">
{% for cat, msg in messages %}
<li class="flash {{ cat }}">{{ msg }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
</body>
</html>

View File

@@ -0,0 +1,15 @@
{% extends "base.html" %}
{% block title %}Change password — Portal Auth Admin{% endblock %}
{% block content %}
<div class="form-page">
<h1>Change password: <code>{{ row.username }}</code></h1>
<form method="post" action="{{ url_for('user_password', pk=row.id) }}" class="edit-form">
<label for="password">New password</label>
<input type="password" id="password" name="password" required minlength="8" autocomplete="new-password">
<div class="form-actions">
<button type="submit">Update password</button>
<a href="{{ url_for('table_view', name='users') }}" class="btn-link">Cancel</a>
</div>
</form>
</div>
{% endblock %}

22
templates/edit_user.html Normal file
View File

@@ -0,0 +1,22 @@
{% extends "base.html" %}
{% block title %}Edit user — Portal Auth Admin{% endblock %}
{% block content %}
<div class="form-page">
<h1>Edit user: <code>{{ row.username }}</code></h1>
<form method="post" action="{{ url_for('table_edit_user', name='users', pk=row.id) }}" class="edit-form">
<label for="role">Role</label>
<select id="role" name="role">
<option value="admin" {{ 'selected' if row.role == 'admin' }}>admin</option>
<option value="support" {{ 'selected' if row.role == 'support' }}>support</option>
</select>
<label class="checkbox-label">
<input type="checkbox" name="is_active" {{ 'checked' if row.is_active }}> Active (can log in)
</label>
<div class="form-actions">
<button type="submit">Save</button>
<a href="{{ url_for('table_view', name='users') }}" class="btn-link">Cancel</a>
</div>
</form>
<p><a href="{{ url_for('user_password', pk=row.id) }}">Change password</a></p>
</div>
{% endblock %}

30
templates/index.html Normal file
View File

@@ -0,0 +1,30 @@
{% extends "base.html" %}
{% block title %}Dashboard — Portal Auth Admin{% endblock %}
{% block content %}
<div class="dashboard">
<h1>Dashboard</h1>
<p class="lead">View and manage the <code>portal_auth</code> database tables.</p>
<div class="table-cards">
<a href="{{ url_for('table_view', name='users') }}" class="card">
<span class="card-icon">users</span>
<h2>Users</h2>
<p>Accounts, roles, active flag. Edit role and password.</p>
</a>
<a href="{{ url_for('table_view', name='sessions') }}" class="card">
<span class="card-icon">sessions</span>
<h2>Sessions</h2>
<p>Active login sessions. View and revoke.</p>
</a>
<a href="{{ url_for('table_view', name='auth_logs') }}" class="card">
<span class="card-icon">auth_logs</span>
<h2>Auth logs</h2>
<p>Login, logout, failed attempts. Read-only.</p>
</a>
<a href="{{ url_for('table_view', name='api_tokens') }}" class="card">
<span class="card-icon">api_tokens</span>
<h2>API tokens</h2>
<p>Tokens for programmatic access. Activate or revoke.</p>
</a>
</div>
</div>
{% endblock %}

15
templates/login.html Normal file
View File

@@ -0,0 +1,15 @@
{% extends "base.html" %}
{% block title %}Login — Portal Auth Admin{% endblock %}
{% block content %}
<div class="login-card">
<h1>Admin login</h1>
<p class="login-hint">Only users with role <code>admin</code> can access this dashboard.</p>
<form method="post" action="{{ url_for('login') }}" class="login-form">
<label for="username">Username</label>
<input type="text" id="username" name="username" required autofocus autocomplete="username">
<label for="password">Password</label>
<input type="password" id="password" name="password" required autocomplete="current-password">
<button type="submit">Log in</button>
</form>
</div>
{% endblock %}

22
templates/new_user.html Normal file
View File

@@ -0,0 +1,22 @@
{% extends "base.html" %}
{% block title %}New user — Portal Auth Admin{% endblock %}
{% block content %}
<div class="form-page">
<h1>Create user</h1>
<form method="post" action="{{ url_for('user_new') }}" class="edit-form">
<label for="username">Username</label>
<input type="text" id="username" name="username" required value="{{ username|default('', true) }}" autocomplete="username">
<label for="password">Password</label>
<input type="password" id="password" name="password" required minlength="8" autocomplete="new-password">
<label for="role">Role</label>
<select id="role" name="role">
<option value="support" {{ 'selected' if role|default('support') == 'support' }}>support</option>
<option value="admin" {{ 'selected' if role|default('support') == 'admin' }}>admin</option>
</select>
<div class="form-actions">
<button type="submit">Create user</button>
<a href="{{ url_for('table_view', name='users') }}" class="btn-link">Cancel</a>
</div>
</form>
</div>
{% endblock %}

63
templates/table.html Normal file
View File

@@ -0,0 +1,63 @@
{% extends "base.html" %}
{% block title %}{{ table_name }} — Portal Auth Admin{% endblock %}
{% block content %}
<div class="table-page">
<div class="table-header">
<h1>Table: <code>{{ table_name }}</code></h1>
<div class="table-header-actions">
{% if table_name == 'users' %}
<a href="{{ url_for('user_new') }}" class="btn-primary">Add user</a>
{% endif %}
<a href="{{ url_for('index') }}" class="back">← Dashboard</a>
</div>
</div>
<div class="table-wrapper">
<table class="data-table">
<thead>
<tr>
{% for key in columns %}
<th>{{ key }}</th>
{% endfor %}
{% if table_name in ['users', 'sessions', 'api_tokens'] %}
<th class="actions">Actions</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for row in rows %}
<tr>
{% for key in columns %}
<td title="{{ row.get(key, '') }}">{{ (row.get(key, ''))[:80] }}{% if row.get(key, '') and (row.get(key, ''))|length > 80 %}…{% endif %}</td>
{% endfor %}
{% if table_name == 'users' and raw_rows %}
<td class="actions">
<a href="{{ url_for('table_edit_user', name='users', pk=raw_rows[loop.index0].id) }}">Edit</a>
<a href="{{ url_for('user_password', pk=raw_rows[loop.index0].id) }}">Password</a>
</td>
{% elif table_name == 'sessions' and raw_rows %}
<td class="actions">
<form method="post" action="{{ url_for('session_delete') }}" class="inline-form" onsubmit="return confirm('Revoke this session?');">
<input type="hidden" name="session_id" value="{{ raw_rows[loop.index0].session_id }}">
<button type="submit">Revoke</button>
</form>
</td>
{% elif table_name == 'api_tokens' and raw_rows %}
<td class="actions">
<form method="post" action="{{ url_for('api_token_toggle', pk=raw_rows[loop.index0].id) }}" class="inline-form">
<button type="submit">{{ 'Deactivate' if row.is_active == 'True' else 'Activate' }}</button>
</form>
<form method="post" action="{{ url_for('api_token_delete', pk=raw_rows[loop.index0].id) }}" class="inline-form" onsubmit="return confirm('Delete this token?');">
<button type="submit" class="danger">Delete</button>
</form>
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if not rows %}
<p class="empty">No rows.</p>
{% endif %}
</div>
{% endblock %}