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
+86
View File
@@ -0,0 +1,86 @@
:root {
--bg-deep: #050508;
--bg-card: rgba(12, 14, 22, 0.9);
--border: rgba(0, 212, 255, 0.15);
--border-bright: rgba(0, 212, 255, 0.4);
--accent: #00d4ff;
--accent-dim: #00a8cc;
--accent-glow: rgba(0, 212, 255, 0.25);
--text: #e8ecf4;
--text-muted: #8b95a8;
--error-bg: rgba(255, 82, 82, 0.12);
--error-border: rgba(255, 82, 82, 0.35);
--error-text: #ff9e9e;
--success-bg: rgba(0, 230, 118, 0.12);
--success-text: #00e676;
--font-mono: 'JetBrains Mono', monospace;
--font-sans: 'Outfit', system-ui, sans-serif;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--font-sans);
background: var(--bg-deep);
color: var(--text);
min-height: 100vh;
}
.bg-pattern {
position: fixed; inset: 0; z-index: 0; pointer-events: none;
background:
linear-gradient(180deg, transparent 0%, rgba(0, 212, 255, 0.02) 50%, transparent 100%),
repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0, 212, 255, 0.03) 2px, rgba(0, 212, 255, 0.03) 4px),
repeating-linear-gradient(90deg, transparent, transparent 2px, rgba(0, 212, 255, 0.03) 2px, rgba(0, 212, 255, 0.03) 4px);
}
.bg-glow {
position: fixed; width: 120%; height: 55vh; top: -10%; left: -10%; z-index: 0; pointer-events: none;
background: radial-gradient(ellipse at 50% 0%, var(--accent-glow) 0%, transparent 55%);
}
.wrap { position: relative; z-index: 1; max-width: 1000px; margin: 0 auto; padding: 1.5rem; }
.card {
background: var(--bg-card); border: 1px solid var(--border); border-radius: 14px; padding: 1.5rem; margin-bottom: 1.25rem;
}
h1, .title { font-family: var(--font-mono); color: var(--accent); }
.muted { color: var(--text-muted); }
.btn {
display: inline-block; padding: 0.75rem 1.2rem; font-family: var(--font-mono); font-size: 0.9rem;
color: var(--bg-deep); background: var(--accent); border: 1px solid var(--accent); border-radius: 10px;
text-decoration: none; cursor: pointer;
}
.btn:hover { background: var(--accent-dim); box-shadow: 0 0 20px var(--accent-glow); }
.btn-ghost {
background: transparent; color: var(--accent);
}
.btn-ghost:hover { background: rgba(0, 212, 255, 0.1); }
.btn-danger { background: transparent; color: var(--error-text); border-color: var(--error-border); }
input[type=text], input[type=password], input[type=email], textarea, input[type=file] {
width: 100%; padding: 0.85rem 1rem; color: var(--text); background: rgba(255,255,255,0.04);
border: 1px solid var(--border); border-radius: 10px; outline: none; font-family: var(--font-sans);
}
input:focus, textarea:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); }
label { display: block; font-family: var(--font-mono); font-size: 0.8rem; color: var(--text-muted); margin-bottom: 0.45rem; text-transform: uppercase; letter-spacing: 0.04em; }
.form-group { margin-bottom: 1.1rem; }
.alert { padding: 0.9rem 1rem; border-radius: 10px; margin-bottom: 1rem; }
.alert.error { background: var(--error-bg); border: 1px solid var(--error-border); color: var(--error-text); }
.alert.success { background: var(--success-bg); border: 1px solid rgba(0,230,118,0.3); color: var(--success-text); }
.nav { display: flex; gap: 0.75rem; flex-wrap: wrap; align-items: center; margin-bottom: 1.5rem; }
.nav a { color: var(--accent); text-decoration: none; font-family: var(--font-mono); font-size: 0.9rem; }
.table { width: 100%; border-collapse: collapse; }
.table th, .table td { text-align: left; padding: 0.75rem 0.5rem; border-bottom: 1px solid var(--border); font-size: 0.92rem; vertical-align: top; }
.table th { color: var(--text-muted); font-family: var(--font-mono); font-weight: 500; }
code { font-family: var(--font-mono); color: var(--accent); }
.pager { display: flex; gap: 0.75rem; margin-top: 1rem; align-items: center; }
.login-box {
position: relative; z-index: 1; width: 100%; max-width: 420px; margin: 8vh auto;
background: var(--bg-card); border: 1px solid var(--border); border-radius: 16px; padding: 2.25rem;
}
.center { text-align: center; }
.qr img { border-radius: 12px; border: 1px solid var(--border); }
pre.config {
font-family: var(--font-mono); font-size: 0.8rem; line-height: 1.5; white-space: pre-wrap; word-break: break-all;
background: rgba(0,0,0,0.35); border: 1px solid var(--border); border-radius: 10px; padding: 1rem; max-height: 320px; overflow: auto;
}
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; }
.stat { padding: 1.2rem; border: 1px solid var(--border); border-radius: 12px; background: rgba(0,0,0,0.25); }
.stat .n { font-family: var(--font-mono); font-size: 1.8rem; color: var(--accent); }
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { transition-duration: 0.01ms !important; }
}
+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}}