vpn/backend/admin.html
2026-09-23 01:19:36 +00:00

57 lines
6.3 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>VPN App — управление</title>
<style>
:root { color-scheme: light dark; font: 15px system-ui,sans-serif; }
body { max-width: 1100px; margin: 0 auto; padding: 24px; background: #f7f7fb; color: #202124; }
@media (prefers-color-scheme: dark) { body { background: #17181c; color: #eee; } }
section { background: color-mix(in srgb, Canvas 90%, transparent); border-radius: 18px; padding: 18px; margin: 16px 0; box-shadow: 0 2px 10px #0001; }
input,textarea,button { box-sizing: border-box; border: 1px solid #8885; border-radius: 10px; padding: 9px; margin: 4px; font: inherit; }
input,textarea { width: min(100%, 420px); } textarea { min-height: 80px; vertical-align: top; }
button { cursor: pointer; } table { width: 100%; border-collapse: collapse; } th,td { padding: 8px; border-bottom: 1px solid #8884; text-align: left; vertical-align: top; }
code { word-break: break-all; } .muted { opacity: .68; } .danger { color: #c62828; } #status { white-space: pre-wrap; }
</style>
</head>
<body>
<h1>VPN App · управление</h1>
<section>
<label>Админ-токен <input id="token" type="password" autocomplete="off"></label>
<button onclick="loadUsers()">Войти / обновить</button>
<div id="status" class="muted"></div>
</section>
<section>
<h2>Новый пользователь</h2>
<form onsubmit="createUser(event)">
<input name="name" placeholder="Имя" required>
<input name="price" type="number" min="0" placeholder="Руб./месяц" required>
<input name="day" type="number" min="1" max="31" placeholder="День оплаты" required>
<input name="vless" placeholder="VLESS subscription URL">
<input name="account" placeholder="Скрытое имя аккаунта Amnezia">
<input name="freeturn" placeholder="Ключ FreeTurn">
<button>Создать</button>
</form>
</section>
<section>
<h2>Пользователи</h2>
<div id="users" class="muted">Введите токен и нажмите «Войти / обновить».</div>
</section>
<script>
const token = document.querySelector('#token'); token.value = sessionStorage.getItem('vpnapp-admin-token') || '';
const statusEl = document.querySelector('#status');
function auth() { const value = token.value.trim(); sessionStorage.setItem('vpnapp-admin-token', value); return {'Authorization': 'Bearer ' + value, 'Content-Type':'application/json'}; }
async function api(path, options={}) { options.headers = {...auth(), ...(options.headers||{})}; const r=await fetch(path,options); const data=await r.json().catch(()=>({})); if(!r.ok) throw new Error(data.detail||r.statusText); return data; }
function esc(s) { return String(s??'').replace(/[&<>"']/g, c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); }
async function loadUsers() { try { const data=await api('/admin/api/users'); renderUsers(data); statusEl.textContent='Готово'; } catch(e) { statusEl.textContent='Ошибка: '+e.message; } }
async function createUser(e) { e.preventDefault(); const f=new FormData(e.target); try { await api('/admin/api/users',{method:'POST',body:JSON.stringify({name:f.get('name'),monthly_price_rub:Number(f.get('price')),payment_day:Number(f.get('day')),vless_subscription_url:f.get('vless'),amnezia_account_name:f.get('account'),freeturn_key:f.get('freeturn')})}); e.target.reset(); await loadUsers(); } catch(x) { statusEl.textContent='Ошибка: '+x.message; } }
async function removeUser(id) { if(!confirm('Удалить пользователя и его устройства?')) return; await api('/admin/api/users/'+id,{method:'DELETE'}); loadUsers(); }
async function addDevice(id) { try { const d=await api('/admin/api/users/'+id+'/devices',{method:'POST'}); alert('Сохраните enrollment URL:\n'+d.enrollment_url+'\n\nSubscription URL:\n'+d.subscription_url+'\n\nТокен показывается только сейчас.'); loadUsers(); } catch(e) { statusEl.textContent='Ошибка: '+e.message; } }
async function removeDevice(id) { if(!confirm('Удалить устройство?')) return; await api('/admin/api/devices/'+id,{method:'DELETE'}); loadUsers(); }
async function editServer(userId) { const name=prompt('Название сервера:'); if(!name) return; const config=prompt('Конфиг AmneziaWG 3.1 для сервера:'); if(!config) return; try { await api('/admin/api/users/'+userId+'/servers',{method:'POST',body:JSON.stringify({server_name:name,amnezia_config:config})}); await loadUsers(); } catch(e) { statusEl.textContent='Ошибка: '+e.message; } }
async function removeServer(userId,name) { if(!confirm('Удалить конфиг сервера?')) return; try { await api('/admin/api/users/'+userId+'/servers/'+encodeURIComponent(name),{method:'DELETE'}); await loadUsers(); } catch(e) { statusEl.textContent='Ошибка: '+e.message; } }
function renderUsers(users) { if(!users.length){document.querySelector('#users').textContent='Пользователей пока нет.';return;} document.querySelector('#users').innerHTML='<table><tr><th>Пользователь</th><th>Оплата</th><th>Устройства</th><th>Amnezia-серверы</th><th></th></tr>'+users.map(u=>`<tr><td><strong>${esc(u.name)}</strong><br><span class="muted">${esc(u.id)}</span></td><td>${u.monthly_price_rub} ₽ / мес<br>день ${u.payment_day}</td><td>${u.devices.map(d=>`<div><code>${esc(d.id)}</code><br><span class="muted">последнее: ${esc(d.last_seen_at||'—')}<br>UA: ${esc(d.last_user_agent||'—')}</span><br><button onclick="removeDevice('${esc(d.id)}')">Удалить устройство</button></div>`).join('<hr>')||'—'}<br><button onclick="addDevice('${esc(u.id)}')">Добавить устройство / ссылку</button></td><td>${u.server_configs.map(s=>`<div><code>${esc(s.server_name)}</code> <button onclick="removeServer('${esc(u.id)}','${esc(s.server_name)}')">×</button></div>`).join('')||'—'}<br><button onclick="editServer('${esc(u.id)}')">Добавить/изменить конфиг</button></td><td><button class="danger" onclick="removeUser('${esc(u.id)}')">Удалить пользователя</button></td></tr>`).join('')+'</table>'; }
</script>
</body>
</html>