feat: add admin panel with session auth, user management, and billing dashboard

Implement a full-featured admin backend at `/manage/` with login authentication using
signed cookies and constant-time credential verification. The panel includes a dashboard
showing total/active users, storage usage, monthly revenue, and pending invoices, plus
user management, payment overview, and pricing configuration pages. New environment
variables `ADMIN_USERNAME`, `ADMIN_PASSWORD`, `ADMIN_SESSION_SECRET`, and
`ADMIN_SESSION_EXPIRE_HOURS` control access. The admin router is registered in main.py
and all new templates, auth module, and router files are added.
This commit is contained in:
2026-01-05 20:50:41 +00:00
parent a8e13cb3c5
commit b8d88f5807
16 changed files with 2316 additions and 2 deletions
+185
View File
@@ -0,0 +1,185 @@
{% extends "admin/base.html" %}
{% block title %}User: {{ user.username }}{% endblock %}
{% block content %}
<div class="page-header">
<h1 class="page-title">{{ user.username }}</h1>
<p class="page-subtitle">User ID: {{ user.id }} | Created: {{ user.created_at.strftime('%Y-%m-%d %H:%M') }}</p>
</div>
<div class="detail-grid">
<div class="detail-section">
<h3 class="detail-section-title">Storage Usage</h3>
<div class="detail-row">
<span class="detail-label">Used</span>
<span class="detail-value">{{ user.used_storage_bytes | format_bytes }}</span>
</div>
<div class="detail-row">
<span class="detail-label">Quota</span>
<span class="detail-value">{{ user.storage_quota_bytes | format_bytes }}</span>
</div>
<div style="margin-top: 12px;">
<div class="progress-bar">
<div class="progress-fill {% if usage_percent > 90 %}danger{% elif usage_percent > 75 %}warning{% endif %}" style="width: {{ usage_percent }}%"></div>
</div>
<div style="text-align: center; margin-top: 8px; font-size: 0.85rem; color: var(--text-color-light);">
{{ "%.1f" | format(usage_percent) }}% used
</div>
</div>
</div>
<div class="detail-section">
<h3 class="detail-section-title">Account Status</h3>
<div class="detail-row">
<span class="detail-label">Status</span>
<span class="detail-value">
{% if user.is_active %}
<span class="badge badge-success">Active</span>
{% else %}
<span class="badge badge-danger">Inactive</span>
{% endif %}
</span>
</div>
<div class="detail-row">
<span class="detail-label">Role</span>
<span class="detail-value">
{% if user.is_superuser %}
<span class="badge badge-info">Administrator</span>
{% else %}
<span class="badge badge-secondary">User</span>
{% endif %}
</span>
</div>
<div class="detail-row">
<span class="detail-label">2FA</span>
<span class="detail-value">
{% if user.is_2fa_enabled %}
<span class="badge badge-success">Enabled</span>
{% else %}
<span class="badge badge-secondary">Disabled</span>
{% endif %}
</span>
</div>
<div class="detail-row">
<span class="detail-label">Plan</span>
<span class="detail-value">{{ user.plan_type }}</span>
</div>
</div>
</div>
<div class="card" style="margin-top: 24px;">
<div class="card-header">
<h2 class="card-title">Edit User</h2>
</div>
<form method="POST" action="/manage/users/{{ user.id }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="form-row">
<div class="form-group">
<label class="form-label" for="username">Username</label>
<input type="text" id="username" name="username" class="form-input" value="{{ user.username }}" required>
</div>
<div class="form-group">
<label class="form-label" for="email">Email</label>
<input type="email" id="email" name="email" class="form-input" value="{{ user.email }}" required>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label" for="password">New Password (leave empty to keep current)</label>
<input type="password" id="password" name="password" class="form-input" placeholder="Enter new password...">
</div>
<div class="form-group">
<label class="form-label" for="storage_quota_gb">Storage Quota (GB)</label>
<input type="number" id="storage_quota_gb" name="storage_quota_gb" class="form-input"
value="{{ (user.storage_quota_bytes / 1073741824) | round(2) }}" step="0.1" min="0" required>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label" for="plan_type">Plan Type</label>
<select id="plan_type" name="plan_type" class="form-select">
<option value="free" {% if user.plan_type == 'free' %}selected{% endif %}>Free</option>
<option value="basic" {% if user.plan_type == 'basic' %}selected{% endif %}>Basic</option>
<option value="premium" {% if user.plan_type == 'premium' %}selected{% endif %}>Premium</option>
<option value="enterprise" {% if user.plan_type == 'enterprise' %}selected{% endif %}>Enterprise</option>
</select>
</div>
<div class="form-group">
<label class="form-label">&nbsp;</label>
<div style="display: flex; gap: 24px; padding-top: 8px;">
<label class="form-checkbox">
<input type="checkbox" name="is_active" value="true" {% if user.is_active %}checked{% endif %}>
<span>Active</span>
</label>
<label class="form-checkbox">
<input type="checkbox" name="is_superuser" value="true" {% if user.is_superuser %}checked{% endif %}>
<span>Administrator</span>
</label>
</div>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save Changes</button>
<a href="/manage/users" class="btn btn-outline">Cancel</a>
</div>
</form>
</div>
{% if invoices %}
<div class="card" style="margin-top: 24px;">
<div class="card-header">
<h2 class="card-title">Recent Invoices</h2>
<a href="/manage/payments?user_id={{ user.id }}" class="btn btn-sm btn-outline">View All</a>
</div>
<div class="table-container">
<table class="data-table">
<thead>
<tr>
<th>Invoice #</th>
<th>Period</th>
<th>Total</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for invoice in invoices %}
<tr>
<td><a href="/manage/payments/{{ invoice.id }}">{{ invoice.invoice_number }}</a></td>
<td>{{ invoice.period_start.strftime('%Y-%m-%d') }} - {{ invoice.period_end.strftime('%Y-%m-%d') }}</td>
<td>{{ invoice.total | format_currency }}</td>
<td>
{% if invoice.status == 'paid' %}
<span class="badge badge-success">Paid</span>
{% elif invoice.status == 'open' %}
<span class="badge badge-warning">Open</span>
{% else %}
<span class="badge badge-secondary">{{ invoice.status }}</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<div class="card" style="margin-top: 24px; border-color: var(--danger-color);">
<div class="card-header">
<h2 class="card-title" style="color: var(--danger-color);">Danger Zone</h2>
</div>
<p style="margin-bottom: 16px; color: var(--text-color-light);">
Deleting a user is permanent and cannot be undone. All files and data associated with this user will be lost.
</p>
<form method="POST" action="/manage/users/{{ user.id }}/delete" onsubmit="return confirm('Are you sure you want to delete this user? This action cannot be undone.');">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit" class="btn btn-danger">Delete User</button>
</form>
</div>
{% endblock %}
+101
View File
@@ -0,0 +1,101 @@
{% extends "admin/base.html" %}
{% block title %}Users{% endblock %}
{% block content %}
<div class="page-header">
<h1 class="page-title">Users</h1>
<p class="page-subtitle">Manage all registered users</p>
</div>
<div class="card">
<form method="GET" action="/manage/users" class="search-bar">
<input type="text" name="search" class="form-input" placeholder="Search by username or email..." value="{{ search }}">
<select name="status" class="form-select">
<option value="">All Status</option>
<option value="active" {% if status == 'active' %}selected{% endif %}>Active</option>
<option value="inactive" {% if status == 'inactive' %}selected{% endif %}>Inactive</option>
<option value="superuser" {% if status == 'superuser' %}selected{% endif %}>Superuser</option>
</select>
<button type="submit" class="btn btn-primary">Search</button>
{% if search or status %}
<a href="/manage/users" class="btn btn-outline">Clear</a>
{% endif %}
</form>
<div class="table-container">
<table class="data-table">
<thead>
<tr>
<th>User</th>
<th>Email</th>
<th>Storage</th>
<th>Plan</th>
<th>Status</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>
<strong>{{ user.username }}</strong>
{% if user.is_superuser %}
<span class="badge badge-info">Admin</span>
{% endif %}
</td>
<td>{{ user.email }}</td>
<td>
{{ user.used_storage_bytes | format_bytes }} / {{ user.storage_quota_bytes | format_bytes }}
</td>
<td>{{ user.plan_type }}</td>
<td>
{% if user.is_active %}
<span class="badge badge-success">Active</span>
{% else %}
<span class="badge badge-danger">Inactive</span>
{% endif %}
</td>
<td>{{ user.created_at.strftime('%Y-%m-%d') }}</td>
<td class="actions">
<a href="/manage/users/{{ user.id }}" class="btn btn-sm btn-outline">Edit</a>
<form method="POST" action="/manage/users/{{ user.id }}/toggle-active" style="display: inline;">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit" class="btn btn-sm {% if user.is_active %}btn-secondary{% else %}btn-success{% endif %}">
{% if user.is_active %}Deactivate{% else %}Activate{% endif %}
</button>
</form>
</td>
</tr>
{% else %}
<tr>
<td colspan="7" class="empty-state">
<div class="empty-state-icon">👥</div>
<div class="empty-state-text">No users found</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if total_pages > 1 %}
<div class="pagination">
{% if page > 1 %}
<a href="/manage/users?page={{ page - 1 }}{% if search %}&search={{ search }}{% endif %}{% if status %}&status={{ status }}{% endif %}">&laquo; Previous</a>
{% else %}
<span class="disabled">&laquo; Previous</span>
{% endif %}
<span>Page {{ page }} of {{ total_pages }}</span>
{% if page < total_pages %}
<a href="/manage/users?page={{ page + 1 }}{% if search %}&search={{ search }}{% endif %}{% if status %}&status={{ status }}{% endif %}">Next &raquo;</a>
{% else %}
<span class="disabled">Next &raquo;</span>
{% endif %}
</div>
{% endif %}
</div>
{% endblock %}