Initial commit: Portal Auth Admin Dashboard
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
43
templates/base.html
Normal file
43
templates/base.html
Normal 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>
|
||||
15
templates/edit_password.html
Normal file
15
templates/edit_password.html
Normal 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
22
templates/edit_user.html
Normal 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
30
templates/index.html
Normal 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
15
templates/login.html
Normal 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
22
templates/new_user.html
Normal 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
63
templates/table.html
Normal 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 %}
|
||||
Reference in New Issue
Block a user