Add Go WireGuard panel with PostgreSQL 17 and Dokploy compose.

This commit is contained in:
2026-07-29 09:15:00 +03:00
parent 370e2455d4
commit 31ce2af622
25 changed files with 2423 additions and 0 deletions
+43
View File
@@ -0,0 +1,43 @@
{{define "admin_dashboard.html"}}
<!DOCTYPE html>
<html lang="ru">
<head>
{{template "layout_head" .}}
<title>Админка — {{siteName}}</title>
</head>
<body>
<div class="bg-pattern"></div><div class="bg-glow"></div>
<div class="wrap">
<div class="nav">
<strong class="title">Админ · {{siteName}}</strong>
<a href="/admin/wg">WireGuard</a>
<span style="flex:1"></span>
<span class="muted">{{.User.Username}}</span>
<a href="/logout">Выход</a>
</div>
<div class="stats">
<div class="stat"><div class="muted">WG конфиги</div><div class="n">{{.TotalConfigs}}</div></div>
<div class="stat"><div class="muted">Пользователи</div><div class="n">{{.TotalUsers}}</div></div>
<div class="stat"><div class="muted">Сайт</div><div class="n">{{if .SiteOnline}}ON{{else}}OFF{{end}}</div></div>
</div>
<div class="card" style="margin-top:1.25rem">
<h1 style="font-size:1.1rem;margin-bottom:1rem">Статус сайта</h1>
<form method="POST" action="/admin/status">
<div class="form-group">
<label>Режим</label>
<select name="is_online" style="width:100%;padding:0.85rem 1rem;border-radius:10px;background:rgba(255,255,255,0.04);color:var(--text);border:1px solid var(--border)">
<option value="1" {{if .SiteOnline}}selected{{end}}>Онлайн</option>
<option value="0" {{if not .SiteOnline}}selected{{end}}>Оффлайн</option>
</select>
</div>
<div class="form-group">
<label>Сообщение оффлайна</label>
<textarea name="message" rows="3">{{.SiteMessage}}</textarea>
</div>
<button class="btn" type="submit">Сохранить</button>
</form>
</div>
</div>
</body>
</html>
{{end}}
+83
View File
@@ -0,0 +1,83 @@
{{define "admin_wg.html"}}
<!DOCTYPE html>
<html lang="ru">
<head>
{{template "layout_head" .}}
<title>WireGuard — {{siteName}}</title>
</head>
<body>
<div class="bg-pattern"></div><div class="bg-glow"></div>
<div class="wrap">
<div class="nav">
<a href="/admin">← Админка</a>
<strong class="title">WireGuard .conf</strong>
<span style="flex:1"></span>
<a href="/logout">Выход</a>
</div>
{{if .Message}}
<div class="alert {{.MessageType}}">{{.Message}}{{if .NewToken}} · <code>{{.PublicURL}}{{.NewToken}}</code>{{end}}</div>
{{end}}
<div class="card">
<h1 style="font-size:1.1rem;margin-bottom:1rem">Загрузить .conf</h1>
<form method="POST" action="/admin/wg/upload" enctype="multipart/form-data">
<div class="form-group">
<label for="wg_conf">Файл WireGuard (.conf)</label>
<input id="wg_conf" type="file" name="wg_conf" accept=".conf" required>
</div>
<button class="btn" type="submit" name="submit_form" value="1">Сохранить</button>
</form>
</div>
<div class="card">
<h1 style="font-size:1.1rem;margin-bottom:1rem">Настройки скачивания</h1>
<form method="POST" action="/admin/wg/settings">
<div class="form-group" style="display:flex;gap:0.6rem;align-items:center">
<input type="checkbox" id="show_zip_full" name="show_zip_full" value="1" {{if .ShowZipFull}}checked{{end}}>
<label for="show_zip_full" style="margin:0;text-transform:none;font-family:var(--font-sans)">ZIP (conf + QR)</label>
</div>
<div class="form-group" style="display:flex;gap:0.6rem;align-items:center">
<input type="checkbox" id="show_zip_conf" name="show_zip_conf" value="1" {{if .ShowZipConf}}checked{{end}}>
<label for="show_zip_conf" style="margin:0;text-transform:none;font-family:var(--font-sans)">ZIP только conf</label>
</div>
<button class="btn btn-ghost" type="submit">Обновить настройки</button>
</form>
</div>
<div class="card">
<h1 style="font-size:1.1rem;margin-bottom:1rem">Конфиги</h1>
<table class="table">
<thead>
<tr><th>ID</th><th>Токен</th><th>Файл</th><th>Дата</th><th>Ссылка</th><th></th></tr>
</thead>
<tbody>
{{range .Configs}}
<tr>
<td>{{.ID}}</td>
<td><code>{{.Token}}</code></td>
<td>{{.OriginalFilename}}</td>
<td class="muted">{{.CreatedAt.Format "2006-01-02 15:04"}}</td>
<td><a style="color:var(--accent)" href="/wg?token={{.Token}}" target="_blank">открыть</a></td>
<td>
<form method="POST" action="/admin/wg/delete" onsubmit="return confirm('Удалить?')">
<input type="hidden" name="id" value="{{.ID}}">
<button class="btn btn-danger" type="submit">Удалить</button>
</form>
</td>
</tr>
{{else}}
<tr><td colspan="6" class="muted">Пока нет конфигов</td></tr>
{{end}}
</tbody>
</table>
<div class="pager">
{{if gt .Page 1}}<a class="btn btn-ghost" href="/admin/wg?page={{sub .Page 1}}"></a>{{end}}
<span class="muted">стр. {{.Page}} / {{.TotalPages}}</span>
{{if lt .Page .TotalPages}}<a class="btn btn-ghost" href="/admin/wg?page={{add .Page 1}}"></a>{{end}}
</div>
</div>
</div>
</body>
</html>
{{end}}
+29
View File
@@ -0,0 +1,29 @@
{{define "home.html"}}
<!DOCTYPE html>
<html lang="ru">
<head>
{{template "layout_head" .}}
<title>{{siteName}}</title>
</head>
<body>
<div class="bg-pattern"></div><div class="bg-glow"></div>
<div class="wrap">
<div class="nav">
<strong class="title">{{siteName}}</strong>
<span style="flex:1"></span>
{{if .User}}
<a href="/admin">Админка</a>
<a href="/logout">Выход</a>
{{else}}
<a href="/login">Вход</a>
{{end}}
</div>
<div class="card">
<h1>{{siteName}}</h1>
<p class="muted" style="margin:1rem 0 1.5rem">WireGuard панель на Go + PostgreSQL 17</p>
<a class="btn" href="/login">Войти в админку</a>
</div>
</div>
</body>
</html>
{{end}}
+33
View File
@@ -0,0 +1,33 @@
{{define "login.html"}}
<!DOCTYPE html>
<html lang="ru">
<head>
{{template "layout_head" .}}
<title>Вход — {{siteName}}</title>
</head>
<body>
<div class="bg-pattern"></div><div class="bg-glow"></div>
<div class="login-box">
<h1 class="center">Вход в аккаунт</h1>
<p class="muted center" style="margin:0.4rem 0 1.5rem">{{siteName}}</p>
{{if .Error}}<div class="alert error">{{.Error}}</div>{{end}}
<form method="POST" action="/login">
<div class="form-group">
<label for="username">Имя пользователя или email</label>
<input id="username" name="username" type="text" required autofocus value="{{.Username}}">
</div>
<div class="form-group">
<label for="password">Пароль</label>
<input id="password" name="password" type="password" required>
</div>
<div class="form-group" style="display:flex;gap:0.6rem;align-items:center">
<input type="checkbox" id="remember_me" name="remember_me" value="1">
<label for="remember_me" style="margin:0;text-transform:none;letter-spacing:normal;font-family:var(--font-sans)">Запомнить меня</label>
</div>
<button class="btn" style="width:100%" type="submit">Войти</button>
</form>
<p class="center muted" style="margin-top:1.2rem"><a href="/" style="color:var(--accent)">На главную</a></p>
</div>
</body>
</html>
{{end}}
+18
View File
@@ -0,0 +1,18 @@
{{define "offline.html"}}
<!DOCTYPE html>
<html lang="ru">
<head>
{{template "layout_head" .}}
<title>Недоступен — {{.SiteName}}</title>
</head>
<body>
<div class="bg-pattern"></div><div class="bg-glow"></div>
<div class="wrap" style="max-width:560px;margin-top:10vh">
<div class="card center">
<h1>Сайт временно недоступен</h1>
<p class="muted" style="margin-top:1rem">{{.Message}}</p>
</div>
</div>
</body>
</html>
{{end}}
+10
View File
@@ -0,0 +1,10 @@
{{define "layout_head"}}
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#050508">
<meta name="color-scheme" content="dark">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/app.css">
{{end}}
+41
View File
@@ -0,0 +1,41 @@
{{define "wg_public.html"}}
<!DOCTYPE html>
<html lang="ru">
<head>
{{template "layout_head" .}}
<title>{{.FileName}}</title>
</head>
<body>
<div class="bg-pattern"></div><div class="bg-glow"></div>
<div class="wrap" style="max-width:560px">
<div class="nav">
<h1 style="font-size:1.2rem">{{.FileName}}</h1>
</div>
<div class="alert success">Конфиг готов.</div>
{{if .QR}}
<div class="card center qr">
<h1 style="font-size:1rem;margin-bottom:1rem">Отсканируйте QR-код</h1>
<img src="{{.QR}}" width="300" height="300" alt="QR">
</div>
{{end}}
<div class="center" style="margin-bottom:1.25rem">
{{if .ShowZipFull}}<a class="btn" href="/wg?token={{.Token}}&download=full">ZIP (conf + QR)</a>{{end}}
{{if .ShowZipConf}}<a class="btn btn-ghost" href="/wg?token={{.Token}}&download=conf">ZIP только conf</a>{{end}}
</div>
<div class="card">
<h1 style="font-size:1rem;margin-bottom:0.8rem">Текст конфигурации</h1>
<pre class="config" id="cfg">{{.Config}}</pre>
<button class="btn btn-ghost" style="margin-top:0.8rem" type="button" id="copyBtn">Копировать</button>
</div>
</div>
<script>
document.getElementById('copyBtn')?.addEventListener('click', async function() {
const t = document.getElementById('cfg')?.textContent || '';
try { await navigator.clipboard.writeText(t); this.textContent = 'Скопировано'; }
catch(e) { this.textContent = 'Ошибка'; }
setTimeout(() => this.textContent = 'Копировать', 2000);
});
</script>
</body>
</html>
{{end}}