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:
@@ -0,0 +1,112 @@
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block title %}Payments{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Payments</h1>
|
||||
<p class="page-subtitle">Overview of all invoices and payments</p>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Total Revenue</div>
|
||||
<div class="stat-value success">{{ summary.total_revenue | format_currency }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Pending Invoices</div>
|
||||
<div class="stat-value {% if summary.pending_count > 0 %}warning{% endif %}">{{ summary.pending_count }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Pending Amount</div>
|
||||
<div class="stat-value {% if summary.pending_amount > 0 %}warning{% endif %}">{{ summary.pending_amount | format_currency }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<form method="GET" action="/manage/payments" class="search-bar">
|
||||
<select name="status" class="form-select">
|
||||
<option value="">All Status</option>
|
||||
<option value="draft" {% if status_filter == 'draft' %}selected{% endif %}>Draft</option>
|
||||
<option value="open" {% if status_filter == 'open' %}selected{% endif %}>Open</option>
|
||||
<option value="paid" {% if status_filter == 'paid' %}selected{% endif %}>Paid</option>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-primary">Filter</button>
|
||||
{% if status_filter or user_id_filter %}
|
||||
<a href="/manage/payments" class="btn btn-outline">Clear</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
<div class="table-container">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Invoice #</th>
|
||||
<th>User</th>
|
||||
<th>Period</th>
|
||||
<th>Subtotal</th>
|
||||
<th>Tax</th>
|
||||
<th>Total</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in invoices %}
|
||||
<tr>
|
||||
<td><a href="/manage/payments/{{ item.invoice.id }}">{{ item.invoice.invoice_number }}</a></td>
|
||||
<td>
|
||||
{% if item.user %}
|
||||
<a href="/manage/users/{{ item.user.id }}">{{ item.user.username }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">Unknown</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ item.invoice.period_start.strftime('%Y-%m-%d') }} - {{ item.invoice.period_end.strftime('%Y-%m-%d') }}</td>
|
||||
<td>{{ item.invoice.subtotal | format_currency }}</td>
|
||||
<td>{{ item.invoice.tax | format_currency }}</td>
|
||||
<td><strong>{{ item.invoice.total | format_currency }}</strong></td>
|
||||
<td>
|
||||
{% if item.invoice.status == 'paid' %}
|
||||
<span class="badge badge-success">Paid</span>
|
||||
{% elif item.invoice.status == 'open' %}
|
||||
<span class="badge badge-warning">Open</span>
|
||||
{% else %}
|
||||
<span class="badge badge-secondary">{{ item.invoice.status }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="actions">
|
||||
<a href="/manage/payments/{{ item.invoice.id }}" class="btn btn-sm btn-outline">View</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="8" class="empty-state">
|
||||
<div class="empty-state-icon">💳</div>
|
||||
<div class="empty-state-text">No invoices found</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if total_pages > 1 %}
|
||||
<div class="pagination">
|
||||
{% if page > 1 %}
|
||||
<a href="/manage/payments?page={{ page - 1 }}{% if status_filter %}&status={{ status_filter }}{% endif %}">« Previous</a>
|
||||
{% else %}
|
||||
<span class="disabled">« Previous</span>
|
||||
{% endif %}
|
||||
|
||||
<span>Page {{ page }} of {{ total_pages }}</span>
|
||||
|
||||
{% if page < total_pages %}
|
||||
<a href="/manage/payments?page={{ page + 1 }}{% if status_filter %}&status={{ status_filter }}{% endif %}">Next »</a>
|
||||
{% else %}
|
||||
<span class="disabled">Next »</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user