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,111 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Admin Panel{% endblock %} - MyWebdav</title>
|
||||
<link rel="stylesheet" href="/static/css/admin.css">
|
||||
<link rel="icon" type="image/png" href="/static/icons/icon-192x192.png">
|
||||
{% block extra_css %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-container">
|
||||
<header class="admin-header">
|
||||
<div class="header-left">
|
||||
<button class="hamburger-btn" id="sidebar-toggle" aria-label="Toggle sidebar">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</button>
|
||||
<div class="admin-logo">
|
||||
<span class="logo-icon">◆</span>
|
||||
<span class="logo-text">My<span class="logo-accent">Webdav</span></span>
|
||||
<span class="admin-badge">Admin</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<span class="admin-user">{{ session.username }}</span>
|
||||
<a href="/manage/logout" class="btn btn-outline">Logout</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="admin-body">
|
||||
<aside class="admin-sidebar" id="admin-sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
<a href="/manage/" class="nav-item {% if request.url.path == '/manage/' %}active{% endif %}">
|
||||
<span class="nav-icon">📊</span>
|
||||
<span class="nav-text">Dashboard</span>
|
||||
</a>
|
||||
<a href="/manage/users" class="nav-item {% if '/manage/users' in request.url.path %}active{% endif %}">
|
||||
<span class="nav-icon">👥</span>
|
||||
<span class="nav-text">Users</span>
|
||||
</a>
|
||||
<a href="/manage/payments" class="nav-item {% if '/manage/payments' in request.url.path %}active{% endif %}">
|
||||
<span class="nav-icon">💳</span>
|
||||
<span class="nav-text">Payments</span>
|
||||
</a>
|
||||
<a href="/manage/settings" class="nav-item {% if '/manage/settings' in request.url.path %}active{% endif %}">
|
||||
<span class="nav-icon">⚙️</span>
|
||||
<span class="nav-text">Settings</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<a href="/" class="nav-item">
|
||||
<span class="nav-icon">🌐</span>
|
||||
<span class="nav-text">View Site</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="sidebar-overlay" id="sidebar-overlay"></div>
|
||||
|
||||
<main class="admin-main">
|
||||
{% if request.query_params.get('success') %}
|
||||
<div class="alert alert-success">Changes saved successfully.</div>
|
||||
{% endif %}
|
||||
{% if request.query_params.get('error') %}
|
||||
<div class="alert alert-error">
|
||||
{% if request.query_params.get('error') == 'csrf' %}
|
||||
Security token expired. Please try again.
|
||||
{% elif request.query_params.get('error') == 'not_found' %}
|
||||
Item not found.
|
||||
{% else %}
|
||||
An error occurred. Please try again.
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if request.query_params.get('deleted') %}
|
||||
<div class="alert alert-success">Item deleted successfully.</div>
|
||||
{% endif %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const sidebarToggle = document.getElementById('sidebar-toggle');
|
||||
const sidebar = document.getElementById('admin-sidebar');
|
||||
const overlay = document.getElementById('sidebar-overlay');
|
||||
|
||||
function toggleSidebar() {
|
||||
sidebar.classList.toggle('open');
|
||||
overlay.classList.toggle('visible');
|
||||
document.body.classList.toggle('sidebar-open');
|
||||
}
|
||||
|
||||
sidebarToggle.addEventListener('click', toggleSidebar);
|
||||
overlay.addEventListener('click', toggleSidebar);
|
||||
|
||||
document.querySelectorAll('.nav-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
if (window.innerWidth < 768) {
|
||||
sidebar.classList.remove('open');
|
||||
overlay.classList.remove('visible');
|
||||
document.body.classList.remove('sidebar-open');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,76 @@
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block title %}Dashboard{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Dashboard</h1>
|
||||
<p class="page-subtitle">Overview of your MyWebdav instance</p>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Total Users</div>
|
||||
<div class="stat-value">{{ stats.total_users }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Active Users</div>
|
||||
<div class="stat-value success">{{ stats.active_users }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Total Storage Used</div>
|
||||
<div class="stat-value">{{ stats.total_storage | format_bytes }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Monthly Revenue</div>
|
||||
<div class="stat-value success">{{ stats.monthly_revenue | format_currency }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Pending Invoices</div>
|
||||
<div class="stat-value {% if stats.pending_invoices > 0 %}warning{% endif %}">{{ stats.pending_invoices }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Inactive Users</div>
|
||||
<div class="stat-value {% if stats.inactive_users > 0 %}danger{% endif %}">{{ stats.inactive_users }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-grid">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Quick Actions</h2>
|
||||
</div>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 12px;">
|
||||
<a href="/manage/users" class="btn btn-primary">Manage Users</a>
|
||||
<a href="/manage/payments" class="btn btn-outline">View Payments</a>
|
||||
<a href="/manage/settings" class="btn btn-outline">Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Recent Activity</h2>
|
||||
</div>
|
||||
{% if recent_activities %}
|
||||
<div class="activity-list">
|
||||
{% for activity in recent_activities %}
|
||||
<div class="activity-item">
|
||||
<div class="activity-icon">{{ activity.user[0] | upper }}</div>
|
||||
<div class="activity-content">
|
||||
<div class="activity-text">
|
||||
<strong>{{ activity.user }}</strong> {{ activity.action }}
|
||||
</div>
|
||||
<div class="activity-time">{{ activity.timestamp.strftime('%Y-%m-%d %H:%M') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">📋</div>
|
||||
<div class="empty-state-text">No recent activity</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Login - MyWebdav</title>
|
||||
<link rel="stylesheet" href="/static/css/admin.css">
|
||||
<link rel="icon" type="image/png" href="/static/icons/icon-192x192.png">
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<div class="login-box">
|
||||
<div class="login-logo">
|
||||
<span class="logo-icon">◆</span>
|
||||
<span class="logo-text">My<span class="logo-accent">Webdav</span></span>
|
||||
</div>
|
||||
<h1 class="login-title">Admin Panel</h1>
|
||||
|
||||
{% if error %}
|
||||
<div class="login-error">
|
||||
Invalid username or password.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="POST" action="/manage/login" class="login-form">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="username">Username</label>
|
||||
<input type="text" id="username" name="username" class="form-input" required autofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="password">Password</label>
|
||||
<input type="password" id="password" name="password" class="form-input" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,122 @@
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block title %}Invoice: {{ invoice.invoice_number }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">{{ invoice.invoice_number }}</h1>
|
||||
<p class="page-subtitle">Created: {{ invoice.created_at.strftime('%Y-%m-%d %H:%M') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="detail-grid">
|
||||
<div class="detail-section">
|
||||
<h3 class="detail-section-title">Invoice Details</h3>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Status</span>
|
||||
<span class="detail-value">
|
||||
{% 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 %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">User</span>
|
||||
<span class="detail-value">
|
||||
{% if user %}
|
||||
<a href="/manage/users/{{ user.id }}">{{ user.username }}</a>
|
||||
{% else %}
|
||||
Unknown
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Period</span>
|
||||
<span class="detail-value">{{ invoice.period_start.strftime('%Y-%m-%d') }} - {{ invoice.period_end.strftime('%Y-%m-%d') }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Due Date</span>
|
||||
<span class="detail-value">{% if invoice.due_date %}{{ invoice.due_date.strftime('%Y-%m-%d') }}{% else %}-{% endif %}</span>
|
||||
</div>
|
||||
{% if invoice.paid_at %}
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Paid At</span>
|
||||
<span class="detail-value">{{ invoice.paid_at.strftime('%Y-%m-%d %H:%M') }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<h3 class="detail-section-title">Totals</h3>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Subtotal</span>
|
||||
<span class="detail-value">{{ invoice.subtotal | format_currency }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Tax</span>
|
||||
<span class="detail-value">{{ invoice.tax | format_currency }}</span>
|
||||
</div>
|
||||
<div class="detail-row" style="font-size: 1.1rem; font-weight: 600;">
|
||||
<span class="detail-label">Total</span>
|
||||
<span class="detail-value">{{ invoice.total | format_currency }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top: 24px;">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Line Items</h2>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<th>Type</th>
|
||||
<th>Quantity</th>
|
||||
<th>Unit Price</th>
|
||||
<th>Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in line_items %}
|
||||
<tr>
|
||||
<td>{{ item.description }}</td>
|
||||
<td>
|
||||
<span class="badge badge-secondary">{{ item.item_type }}</span>
|
||||
</td>
|
||||
<td>{{ "%.2f" | format(item.quantity) }}</td>
|
||||
<td>{{ item.unit_price | format_currency }}</td>
|
||||
<td><strong>{{ item.amount | format_currency }}</strong></td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="5" class="empty-state">
|
||||
<div class="empty-state-text">No line items</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if invoice.status != 'paid' %}
|
||||
<div class="card" style="margin-top: 24px;">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Actions</h2>
|
||||
</div>
|
||||
<form method="POST" action="/manage/payments/{{ invoice.id }}/mark-paid" onsubmit="return confirm('Mark this invoice as paid?');">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit" class="btn btn-success">Mark as Paid</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div style="margin-top: 24px;">
|
||||
<a href="/manage/payments" class="btn btn-outline">← Back to Payments</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -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 %}
|
||||
@@ -0,0 +1,80 @@
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block title %}Settings{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Settings</h1>
|
||||
<p class="page-subtitle">Configure pricing and system settings</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Pricing Configuration</h2>
|
||||
</div>
|
||||
|
||||
{% if pricing_configs %}
|
||||
<div class="table-container">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Setting</th>
|
||||
<th>Description</th>
|
||||
<th>Value</th>
|
||||
<th>Unit</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for config in pricing_configs %}
|
||||
<tr>
|
||||
<td><strong>{{ config.config_key }}</strong></td>
|
||||
<td>{{ config.description or '-' }}</td>
|
||||
<td>
|
||||
<form method="POST" action="/manage/settings/pricing/{{ config.id }}" class="inline-form" id="form-{{ config.id }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="text" name="value" class="form-input" value="{{ config.config_value }}" style="width: 120px;">
|
||||
</form>
|
||||
</td>
|
||||
<td>{{ config.unit or '-' }}</td>
|
||||
<td>
|
||||
<button type="submit" form="form-{{ config.id }}" class="btn btn-sm btn-primary">Save</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">⚙️</div>
|
||||
<div class="empty-state-text">No pricing configuration found</div>
|
||||
<p style="color: var(--text-color-light); margin-top: 8px;">
|
||||
Run the database initialization to set up default pricing.
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top: 24px;">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">System Information</h2>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Application</span>
|
||||
<span class="detail-value">MyWebdav Cloud Storage</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Admin Panel Version</span>
|
||||
<span class="detail-value">1.0.0</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.inline-form {
|
||||
display: inline;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -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"> </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 %}
|
||||
@@ -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 %}">« Previous</a>
|
||||
{% else %}
|
||||
<span class="disabled">« 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 »</a>
|
||||
{% else %}
|
||||
<span class="disabled">Next »</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user