first commit

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
test2
2026-07-25 20:36:00 +03:00
co-authored by Cursor
commit bbff3cb10b
35 changed files with 2054 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
{% extends "admin/layout.html" %}
{% block admin_title %}{{ client.name }}{% endblock %}
{% block admin_content %}
<div class="admin-top">
<div>
<h1>{{ client.name }}</h1>
<div class="muted">{{ client.server.name }} · {{ client.server.protocol }} · {{ client.address }}</div>
</div>
<div class="actions">
<a class="btn btn-sm btn-ghost" href="/admin/clients">Назад</a>
<a class="btn btn-sm btn-accent" href="/admin/clients/{{ client.id }}/config">Скачать .conf</a>
</div>
</div>
<div class="detail-grid">
<div class="panel">
<div class="panel-head"><strong>Конфиг</strong></div>
<div class="panel-body">
<pre class="mono">{{ config }}</pre>
</div>
</div>
<div class="panel">
<div class="panel-head"><strong>QR‑код</strong></div>
<div class="panel-body">
<img class="qr" src="/admin/clients/{{ client.id }}/qr" alt="QR config" />
<p class="muted" style="margin-top:0.8rem">Отсканируйте в клиенте WireGuard / AmneziaWG.</p>
{% if client.notes %}
<p><strong>Заметка:</strong> {{ client.notes }}</p>
{% endif %}
</div>
</div>
</div>
{% endblock %}
+87
View File
@@ -0,0 +1,87 @@
{% extends "admin/layout.html" %}
{% block admin_title %}Клиенты{% endblock %}
{% block admin_content %}
<div class="admin-top">
<h1>Клиенты</h1>
<div class="actions">
<a class="btn btn-sm btn-ghost" href="/admin/clients">Все</a>
<a class="btn btn-sm btn-ghost" href="/admin/clients?protocol=wireguard">WireGuard</a>
<a class="btn btn-sm btn-ghost" href="/admin/clients?protocol=awg2">AWG 2.0</a>
</div>
</div>
{% if flash %}
{% if flash.startswith('error:') %}
<div class="flash flash-error">{{ flash[6:] }}</div>
{% elif flash == 'created' %}
<div class="flash">Клиент создан</div>
{% elif flash == 'deleted' %}
<div class="flash">Клиент удалён</div>
{% endif %}
{% endif %}
<div class="panel" style="margin-bottom:1rem">
<div class="panel-head"><strong>Новый доступ</strong></div>
<div class="panel-body">
<form method="post" action="/admin/clients" class="form-grid">
<label>Имя
<input name="name" placeholder="phone / laptop" required />
</label>
<label>Сервер / протокол
<select name="server_id" required>
{% for s in servers %}
<option value="{{ s.id }}">{{ s.name }} ({{ s.protocol }})</option>
{% endfor %}
</select>
</label>
<button class="btn btn-accent" type="submit">Создать</button>
<label style="grid-column: 1 / -1">Заметка
<input name="notes" placeholder="опционально" />
</label>
</form>
</div>
</div>
<div class="panel">
<table>
<thead>
<tr>
<th>Имя</th>
<th>Протокол</th>
<th>IP</th>
<th>Статус</th>
<th>Создан</th>
<th></th>
</tr>
</thead>
<tbody>
{% for c in clients %}
<tr>
<td><a href="/admin/clients/{{ c.id }}">{{ c.name }}</a></td>
<td><span class="badge badge-proto">{{ c.server.protocol }}</span></td>
<td>{{ c.address }}</td>
<td>
{% if c.is_enabled %}
<span class="badge badge-ok">on</span>
{% else %}
<span class="badge badge-off">off</span>
{% endif %}
</td>
<td class="muted">{{ c.created_at.strftime('%Y-%m-%d %H:%M') if c.created_at else '—' }}</td>
<td class="actions">
<a class="btn btn-sm btn-ghost" href="/admin/clients/{{ c.id }}">Открыть</a>
<form method="post" action="/admin/clients/{{ c.id }}/toggle" class="inline-form">
<button class="btn btn-sm btn-ghost" type="submit">{% if c.is_enabled %}Выкл{% else %}Вкл{% endif %}</button>
</form>
<form method="post" action="/admin/clients/{{ c.id }}/delete" class="inline-form" onsubmit="return confirm('Удалить клиента?')">
<button class="btn btn-sm btn-danger" type="submit">Удалить</button>
</form>
</td>
</tr>
{% else %}
<tr><td colspan="6" class="muted">Клиентов пока нет</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
+59
View File
@@ -0,0 +1,59 @@
{% extends "admin/layout.html" %}
{% block admin_title %}Дашборд{% endblock %}
{% block admin_content %}
<div class="admin-top">
<h1>Дашборд</h1>
</div>
<div class="stats">
<div class="stat">
<div class="label">Серверы</div>
<div class="value">{{ servers|length }}</div>
</div>
<div class="stat">
<div class="label">Клиенты</div>
<div class="value">{{ clients_total }}</div>
</div>
<div class="stat">
<div class="label">Активные</div>
<div class="value">{{ clients_active }}</div>
</div>
</div>
<div class="panel">
<div class="panel-head">
<strong>Протоколы</strong>
<a class="btn btn-sm btn-accent" href="/admin/clients">Управлять клиентами</a>
</div>
<table>
<thead>
<tr>
<th>Имя</th>
<th>Протокол</th>
<th>Endpoint</th>
<th>Подсеть</th>
<th>Статус</th>
</tr>
</thead>
<tbody>
{% for s in servers %}
<tr>
<td>{{ s.name }}</td>
<td><span class="badge badge-proto">{{ s.protocol }}</span></td>
<td class="mono" style="background:transparent;color:inherit;padding:0">{{ s.public_host }}:{{ s.public_port }}</td>
<td>{{ s.subnet }}</td>
<td>
{% if s.is_enabled %}
<span class="badge badge-ok">enabled</span>
{% else %}
<span class="badge badge-off">disabled</span>
{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="5" class="muted">Серверы ещё не созданы</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
+27
View File
@@ -0,0 +1,27 @@
{% extends "base.html" %}
{% block title %}{% block admin_title %}Админка{% endblock %} — {{ app_name }}{% endblock %}
{% block body %}
<div class="admin-shell">
<aside class="sidebar">
<a class="brand" href="/admin">
<span class="brand-mark">V</span>
<span>{{ app_name }}</span>
</a>
<nav>
<a href="/admin" class="{% if request.url.path == '/admin' %}active{% endif %}">Дашборд</a>
<a href="/admin/servers" class="{% if '/servers' in request.url.path %}active{% endif %}">Серверы</a>
<a href="/admin/clients" class="{% if '/clients' in request.url.path %}active{% endif %}">Клиенты</a>
<a href="/" target="_blank">Сайт</a>
</nav>
<div class="meta">
<div>{{ admin.username }}</div>
<form class="inline-form" method="post" action="/admin/logout" style="margin-top:0.6rem">
<button class="btn btn-ghost btn-sm" type="submit" style="color:#d7dee5;border-color:#2a343c">Выйти</button>
</form>
</div>
</aside>
<main class="admin-main">
{% block admin_content %}{% endblock %}
</main>
</div>
{% endblock %}
+24
View File
@@ -0,0 +1,24 @@
{% extends "base.html" %}
{% block title %}Вход — {{ app_name }}{% endblock %}
{% block body %}
<div class="login-wrap">
<div class="login-card">
<div class="brand">
<span class="brand-mark">V</span>
<span>{{ app_name }}</span>
</div>
<h1>Вход в админку</h1>
<p>Логин и пароль задаются в переменных окружения.</p>
{% if error %}<div class="error">{{ error }}</div>{% endif %}
<form method="post" action="/admin/login" class="stack">
<label>Логин
<input name="username" autocomplete="username" required />
</label>
<label>Пароль
<input type="password" name="password" autocomplete="current-password" required />
</label>
<button class="btn btn-primary" type="submit">Войти</button>
</form>
</div>
</div>
{% endblock %}
+39
View File
@@ -0,0 +1,39 @@
{% extends "admin/layout.html" %}
{% block admin_title %}Серверы{% endblock %}
{% block admin_content %}
<div class="admin-top">
<h1>Серверы</h1>
</div>
{% for s in servers %}
<div class="panel" style="margin-bottom:1rem">
<div class="panel-head">
<strong>{{ s.name }} <span class="badge badge-proto">{{ s.protocol }}</span></strong>
<form method="post" action="/admin/servers/{{ s.id }}/sync" class="inline-form">
<button class="btn btn-sm btn-ghost" type="submit">Синхронизировать conf</button>
</form>
</div>
<div class="panel-body">
<p class="muted" style="margin-top:0">Интерфейс <code>{{ s.interface_name }}</code> · подсеть <code>{{ s.subnet }}</code> · клиентов: {{ s.clients|length }}</p>
<p><code>Public key:</code> <span class="muted">{{ s.server_public_key }}</span></p>
<form method="post" action="/admin/servers/{{ s.id }}" class="form-grid">
<label>Public host
<input name="public_host" value="{{ s.public_host }}" required />
</label>
<label>Port
<input type="number" name="public_port" value="{{ s.public_port }}" required />
</label>
<button class="btn btn-accent" type="submit">Сохранить</button>
<label style="grid-column: 1 / -1">DNS
<input name="dns" value="{{ s.dns }}" />
</label>
</form>
</div>
</div>
{% endfor %}
<p class="muted">
Конфиги пишутся в volume: <code>/data/wireguard</code> и <code>/data/amneziawg</code>.
Подключите их к WG/AWG контейнерам на том же хосте.
</p>
{% endblock %}