feat(gui): security hardening, UI overhaul, light theme
- CSRF protection on all POST forms (session token) - ensure_schema() moved to module-level, removed from before_request - gunicorn now binds to 127.0.0.1 only, runs as unprivileged user wgadmin - nginx reverse proxy with HTTPS (Let's Encrypt, wg.4mont.ru) - HTTP → HTTPS redirect before Basic Auth prompt - Auth moved to nginx level (auth_basic), wg-peerctl called via sudo - ufw firewall: only 22/80/443/51820 open - fail2ban: SSH + nginx (5 attempts → 1h ban) - Add Enable/Disable toggle buttons in peer table - Add .conf file download route - Light theme: white background, blue accent, subtle shadows - Modern sidebar layout, styled badges, responsive forms Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+83
-37
@@ -1,41 +1,87 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<h2>Клиенты</h2>
|
||||
<p>Интерфейс: <b>{{ meta.get('WG_INTERFACE','wg0') }}</b> | Сеть: <b>{{ meta.get('WG_NETWORK','-') }}</b> | Endpoint: <b>{{ meta.get('SERVER_PUBLIC_IP','-') }}:{{ meta.get('WG_PORT','-') }}</b></p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Имя</th><th>Статус</th><th>IP</th><th>Роуты</th><th>AllowedIPs</th><th>Endpoint</th><th>Handshake</th><th>RX</th><th>TX</th><th>PubKey</th><th>Действие</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for p in peers %}
|
||||
<tr>
|
||||
<td>{{ p.name }}</td>
|
||||
<td><span class="badge {{ p.status }}">{{ p.status }}</span></td>
|
||||
<td>{{ p.client_address }}</td>
|
||||
<td>{{ p.routes }}</td>
|
||||
<td>{{ p.allowed_ips }}</td>
|
||||
<td>{{ p.endpoint }}</td>
|
||||
<td>{{ p.latest_handshake }}</td>
|
||||
<td>{{ p.rx }}</td>
|
||||
<td>{{ p.tx }}</td>
|
||||
<td class="mono">{{ p.public_key }}</td>
|
||||
<td>
|
||||
{% if p.id %}
|
||||
<a href="{{ url_for('peer_view', peer_id=p.id) }}">QR/Config</a>
|
||||
<form method="post" action="{{ url_for('peer_delete', peer_id=p.id) }}" style="display:inline" onsubmit="return confirm('Удалить peer?')">
|
||||
<button type="submit" class="danger">Удалить</button>
|
||||
</form>
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Клиенты</h2>
|
||||
<p class="meta-line">
|
||||
<span class="meta-tag">{{ meta.get('WG_INTERFACE','wg0') }}</span>
|
||||
<span class="meta-tag">{{ meta.get('WG_NETWORK','-') }}</span>
|
||||
<span class="meta-tag">{{ meta.get('SERVER_PUBLIC_IP','-') }}:{{ meta.get('WG_PORT','-') }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<a href="{{ url_for('new_peer') }}" class="btn btn-primary">+ Добавить peer</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Имя</th>
|
||||
<th>Статус</th>
|
||||
<th>IP</th>
|
||||
<th>Роуты</th>
|
||||
<th>Endpoint</th>
|
||||
<th>Handshake</th>
|
||||
<th>RX / TX</th>
|
||||
<th>Публичный ключ</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for p in peers %}
|
||||
<tr class="{{ 'row-disabled' if not p.enabled else '' }}">
|
||||
<td><span class="peer-name">{{ p.name }}</span></td>
|
||||
<td>
|
||||
<span class="badge {{ p.status }}">
|
||||
<i class="dot"></i>{{ p.status }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="mono-sm">{{ p.client_address }}</td>
|
||||
<td class="mono-sm text-muted">{{ p.routes }}</td>
|
||||
<td class="mono-sm text-muted">{{ p.endpoint }}</td>
|
||||
<td class="text-muted">{{ p.latest_handshake }}</td>
|
||||
<td class="text-muted">{{ p.rx }} / {{ p.tx }}</td>
|
||||
<td><span class="pubkey" title="{{ p.public_key }}">{{ p.public_key[:20] }}…</span></td>
|
||||
<td>
|
||||
<div class="actions">
|
||||
{% if p.id %}
|
||||
<a href="{{ url_for('peer_view', peer_id=p.id) }}" class="btn btn-sm">QR</a>
|
||||
<a href="{{ url_for('peer_download', peer_id=p.id) }}" class="btn btn-sm">↓ .conf</a>
|
||||
|
||||
{% if p.enabled %}
|
||||
<form method="post" action="{{ url_for('peer_disable', peer_id=p.id) }}" style="display:inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-warn">Откл</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form method="post" action="{{ url_for('peer_enable', peer_id=p.id) }}" style="display:inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-ok">Вкл</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="{{ url_for('peer_delete', peer_id=p.id) }}" style="display:inline" onsubmit="return confirm('Удалить peer {{ p.name }}?')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-danger">Удалить</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form method="post" action="{{ url_for('peer_delete_by_key') }}" style="display:inline" onsubmit="return confirm('Удалить peer?')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="public_key" value="{{ p.public_key }}" />
|
||||
<button type="submit" class="btn btn-sm btn-danger">Удалить</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<form method="post" action="{{ url_for('peer_delete_by_key') }}" style="display:inline" onsubmit="return confirm('Удалить peer?')">
|
||||
<input type="hidden" name="public_key" value="{{ p.public_key }}" />
|
||||
<button type="submit" class="danger">Удалить</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<tr>
|
||||
<td colspan="9" class="empty">Пиров нет. <a href="{{ url_for('new_peer') }}">Добавить первый</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user