57 lines
6.3 KiB
HTML
57 lines
6.3 KiB
HTML
<!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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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>
|