modified: web/app.py
modified: web/blueprints/auth.py modified: web/blueprints/group_admin.py modified: web/blueprints/site_admin.py new file: web/limiter.py modified: web/panel_db.py modified: web/requirements.txt new file: web/templates/429.html new file: web/templates/admin/audit_log.html modified: web/templates/admin/base.html
This commit is contained in:
31
web/templates/429.html
Normal file
31
web/templates/429.html
Normal file
@@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Too Many Requests — MCLogger</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<style>
|
||||
body { display: flex; align-items: center; justify-content: center; min-height: 100vh; background: #0d1117; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="text-center p-4" style="max-width:420px">
|
||||
<i class="bi bi-shield-exclamation text-warning" style="font-size:3rem"></i>
|
||||
<h2 class="fw-bold mt-3">Too Many Requests</h2>
|
||||
<p class="text-muted">You have submitted this form too frequently. Please wait
|
||||
{% if retry_after %}
|
||||
<strong>{{ retry_after }} second{{ 's' if retry_after != 1 }}</strong>
|
||||
{% else %}
|
||||
a moment
|
||||
{% endif %}
|
||||
before trying again.
|
||||
</p>
|
||||
<a href="javascript:history.back()" class="btn btn-outline-secondary mt-2">
|
||||
<i class="bi bi-arrow-left me-1"></i>Go back
|
||||
</a>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
157
web/templates/admin/audit_log.html
Normal file
157
web/templates/admin/audit_log.html
Normal file
@@ -0,0 +1,157 @@
|
||||
{% extends "admin/base.html" %}
|
||||
{% block title %}Audit Log{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h4 class="mb-0"><i class="bi bi-journal-text me-2"></i>Audit Log</h4>
|
||||
<span class="text-muted small">{{ total }} event{{ 's' if total != 1 }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<form method="get" action="{{ url_for('site_admin.audit_log') }}" class="card border-secondary mb-4">
|
||||
<div class="card-body py-2">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label form-label-sm mb-1">Action</label>
|
||||
<select name="action" class="form-select form-select-sm bg-dark text-white border-secondary">
|
||||
<option value="">— All actions —</option>
|
||||
{% for a in actions %}
|
||||
<option value="{{ a }}" {{ 'selected' if action_filter == a }}>{{ a }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label form-label-sm mb-1">Group</label>
|
||||
<select name="group_id" class="form-select form-select-sm bg-dark text-white border-secondary">
|
||||
<option value="">— All groups —</option>
|
||||
{% for g in all_groups %}
|
||||
<option value="{{ g.id }}" {{ 'selected' if group_filter == g.id }}>{{ g.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label form-label-sm mb-1">Actor</label>
|
||||
<input type="text" name="actor" class="form-control form-control-sm bg-dark text-white border-secondary"
|
||||
placeholder="Username…" value="{{ actor_filter }}">
|
||||
</div>
|
||||
<div class="col-md-3 d-flex gap-2">
|
||||
<button type="submit" class="btn btn-sm btn-primary w-100">
|
||||
<i class="bi bi-funnel-fill me-1"></i>Filter
|
||||
</button>
|
||||
<a href="{{ url_for('site_admin.audit_log') }}" class="btn btn-sm btn-outline-secondary w-100">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="card border-secondary">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-dark table-hover table-sm align-middle mb-0">
|
||||
<thead class="table-secondary text-dark">
|
||||
<tr>
|
||||
<th style="width:155px">Timestamp (UTC)</th>
|
||||
<th style="width:130px">Actor</th>
|
||||
<th style="width:180px">Action</th>
|
||||
<th style="width:90px">Entity</th>
|
||||
<th style="width:80px">Entity ID</th>
|
||||
<th style="width:120px">Group</th>
|
||||
<th>Details</th>
|
||||
<th style="width:110px">IP Address</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in rows %}
|
||||
<tr>
|
||||
<td class="text-muted small">{{ row.created_at | fmt_dt }}</td>
|
||||
<td>
|
||||
{% if row.actor_username %}
|
||||
<span class="text-info">{{ row.actor_username }}</span>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% set action_class = {
|
||||
'user.login': 'badge bg-success',
|
||||
'user.login_failed': 'badge bg-danger',
|
||||
'admin.login': 'badge bg-warning text-dark',
|
||||
'admin.login_failed': 'badge bg-danger',
|
||||
'invite.created': 'badge bg-primary',
|
||||
'invite.accepted': 'badge bg-success',
|
||||
'invite.revoked': 'badge bg-secondary',
|
||||
'invite.resent': 'badge bg-info text-dark',
|
||||
'member.added': 'badge bg-primary',
|
||||
'member.removed': 'badge bg-danger',
|
||||
'member.role_changed': 'badge bg-warning text-dark',
|
||||
'member.updated': 'badge bg-warning text-dark',
|
||||
'group.created': 'badge bg-success',
|
||||
'group.updated': 'badge bg-secondary',
|
||||
'group.deleted': 'badge bg-danger',
|
||||
'user.updated': 'badge bg-secondary',
|
||||
'user.deleted': 'badge bg-danger',
|
||||
'mail.settings_saved': 'badge bg-info text-dark',
|
||||
'mail.settings_deleted':'badge bg-danger',
|
||||
} %}
|
||||
<span class="{{ action_class.get(row.action, 'badge bg-secondary') }} font-monospace" style="font-size:.75em">
|
||||
{{ row.action }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-muted small">{{ row.entity_type or '—' }}</td>
|
||||
<td class="text-muted small font-monospace">{{ row.entity_id or '—' }}</td>
|
||||
<td class="small">
|
||||
{% if row.group_name %}
|
||||
<span class="badge bg-dark border border-secondary">{{ row.group_name }}</span>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="small text-muted font-monospace">
|
||||
{% if row.details %}
|
||||
{% set d = row.details if row.details is mapping else {} %}
|
||||
{% for k, v in d.items() %}
|
||||
<span class="me-2"><strong>{{ k }}:</strong> {{ v }}</span>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
—
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-muted small font-monospace">{{ row.ip_address or '—' }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="8" class="text-center text-muted py-4">
|
||||
<i class="bi bi-journal-x me-2"></i>No audit events found.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if total_pages > 1 %}
|
||||
<nav class="mt-3">
|
||||
<ul class="pagination pagination-sm justify-content-center mb-0">
|
||||
<li class="page-item {{ 'disabled' if page <= 1 }}">
|
||||
<a class="page-link" href="{{ url_for('site_admin.audit_log', page=page-1, action=action_filter, group_id=group_filter, actor=actor_filter) }}">
|
||||
<i class="bi bi-chevron-left"></i>
|
||||
</a>
|
||||
</li>
|
||||
{% for p in range([1, page-2]|max, [total_pages+1, page+3]|min) %}
|
||||
<li class="page-item {{ 'active' if p == page }}">
|
||||
<a class="page-link" href="{{ url_for('site_admin.audit_log', page=p, action=action_filter, group_id=group_filter, actor=actor_filter) }}">{{ p }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
<li class="page-item {{ 'disabled' if page >= total_pages }}">
|
||||
<a class="page-link" href="{{ url_for('site_admin.audit_log', page=page+1, action=action_filter, group_id=group_filter, actor=actor_filter) }}">
|
||||
<i class="bi bi-chevron-right"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -19,6 +19,7 @@
|
||||
<a href="{{ url_for('site_admin.groups') }}" class="nav-link text-white {{ 'fw-bold' if request.endpoint == 'site_admin.groups' }}">Groups</a>
|
||||
<a href="{{ url_for('site_admin.users') }}" class="nav-link text-white {{ 'fw-bold' if request.endpoint == 'site_admin.users' }}">Users</a>
|
||||
<a href="{{ url_for('site_admin.mail_settings') }}" class="nav-link text-white {{ 'fw-bold' if request.endpoint == 'site_admin.mail_settings' }}">Mail</a>
|
||||
<a href="{{ url_for('site_admin.audit_log') }}" class="nav-link text-white {{ 'fw-bold' if request.endpoint == 'site_admin.audit_log' }}">Audit Log</a>
|
||||
<form method="post" action="{{ url_for('auth.logout') }}" class="d-inline">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-outline-light btn-sm">
|
||||
|
||||
Reference in New Issue
Block a user