wolf-host / admin.html
mohammed978's picture
Wolf Host panel (gateway architecture, obfuscated sources)
73c1400 verified
Raw History Blame Contribute Delete
13.1 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="icon" type="image/png" href="/logo.png" />
<title>Admin Panel · Wolf Host</title>
<style>
:root{--bg:#05060a;--muted:#99a3ad;--accent:#00d4ff;--border:rgba(255,255,255,.10);--card:rgba(255,255,255,.06);--shadow:0 18px 60px rgba(0,0,0,.55);--danger:#ff6b6b;--ok:#7CFF7C;--warn:#ffd166}
*{box-sizing:border-box}
body{
margin:0;font-family:system-ui,Segoe UI,Roboto,Arial;color:rgba(255,255,255,.92);
background:
radial-gradient(1200px 600px at 10% 10%, rgba(0,212,255,.25), transparent 60%),
radial-gradient(900px 500px at 90% 20%, rgba(123,97,255,.22), transparent 55%),
linear-gradient(180deg,#070a12,#05060a);
}
.top{display:flex;align-items:center;justify-content:space-between;padding:14px 20px;border-bottom:1px solid var(--border);background:rgba(0,0,0,.35);backdrop-filter:blur(14px);position:sticky;top:0}
.logo{font-weight:950;letter-spacing:1px;color:var(--accent);text-transform:uppercase}
.btn{padding:10px 14px;border-radius:12px;border:1px solid var(--border);background:rgba(255,255,255,.06);color:white;font-weight:900;cursor:pointer}
.btn:hover{background:rgba(255,255,255,.09)}
.btn-accent{border:0;background:linear-gradient(135deg, rgba(0,212,255,.95), rgba(123,97,255,.85));color:#00131c}
.wrap{max-width:1200px;margin:22px auto;padding:0 20px}
.grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}
@media (max-width: 900px){.grid{grid-template-columns:repeat(2,1fr)}}
@media (max-width: 520px){.grid{grid-template-columns:1fr}}
.card{background:linear-gradient(180deg, rgba(255,255,255,.08), rgba(255,255,255,.04));border:1px solid var(--border);border-radius:16px;padding:14px;box-shadow:var(--shadow);backdrop-filter:blur(16px)}
.k{color:var(--muted);font-size:12px}
.v{font-size:20px;font-weight:950;margin-top:6px}
h2{margin:22px 0 10px}
table{width:100%;border-collapse:collapse;background:linear-gradient(180deg, rgba(255,255,255,.07), rgba(255,255,255,.03));border:1px solid var(--border);border-radius:16px;overflow:hidden;box-shadow:var(--shadow);backdrop-filter:blur(14px)}
th,td{padding:12px 10px;border-bottom:1px solid rgba(255,255,255,.08);text-align:left;font-size:13px}
th{color:rgba(255,255,255,.78);text-transform:uppercase;letter-spacing:.5px;font-size:11px}
.tag{display:inline-block;padding:4px 8px;border-radius:999px;background:rgba(0,0,0,.25);border:1px solid rgba(255,255,255,.10);font-weight:900;font-size:11px}
.tag.ok{color:var(--ok)} .tag.bad{color:var(--danger)} .tag.mid{color:var(--warn)}
.actions button{margin-right:6px;margin-top:4px}
.row{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin:10px 0}
input{padding:10px;border-radius:12px;border:1px solid var(--border);background:rgba(0,0,0,.35);color:#e9f2f7;outline:none}
input:focus{border-color:rgba(0,212,255,.6);box-shadow:0 0 0 3px rgba(0,212,255,.15)}
.msg{margin-top:10px;color:var(--muted);font-size:12px}
.wrap{animation:fadein .3s ease}
@keyframes fadein{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
.card{transition:transform .15s,border-color .15s}
.card:hover{transform:translateY(-2px);border-color:rgba(0,212,255,.35)}
.v{background:linear-gradient(135deg,#fff,rgba(0,212,255,.85));-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}
tbody tr{transition:background .12s}
tbody tr:hover{background:rgba(0,212,255,.05)}
.tag.ok{box-shadow:0 0 12px rgba(124,255,124,.22)}
.tag.bad{box-shadow:0 0 12px rgba(255,107,107,.22)}
.tag.mid{box-shadow:0 0 12px rgba(255,209,102,.22)}
.tbl-wrap{overflow-x:auto;border-radius:16px}
.btn{transition:transform .12s,filter .12s,background .12s}
.btn:hover{filter:brightness(1.15)}
.btn:active{transform:scale(.96)}
</style>
</head>
<body>
<div class="top">
<div class="logo" style="display:flex;align-items:center;gap:10px"><img src="/logo.png" alt="3MH" style="width:34px;height:34px;border-radius:10px;object-fit:cover">ADMIN PANEL</div>
<div style="display:flex;gap:10px">
<button class="btn" onclick="location.href='/'">Panel</button>
<button class="btn" onclick="location.href='/docs'">Docs</button>
<button class="btn" onclick="location.href='/logout'">Logout</button>
</div>
</div>
<div class="wrap">
<div class="grid">
<div class="card"><div class="k">Servers Total</div><div class="v" id="st_total">0</div></div>
<div class="card"><div class="k">Running</div><div class="v" id="st_run">0</div></div>
<div class="card"><div class="k">Installing/Starting</div><div class="v" id="st_ins">0</div></div>
<div class="card"><div class="k">Banned Servers</div><div class="v" id="st_ban">0</div></div>
</div>
<div class="grid" style="margin-top:12px">
<div class="card"><div class="k">Users Total</div><div class="v" id="u_total">0</div></div>
<div class="card"><div class="k">Active Users</div><div class="v" id="u_active">0</div></div>
<div class="card"><div class="k">Rules</div><div class="v" style="font-size:13px">Unlimited servers<br>Fair-use auto-throttle</div></div>
</div>
<div class="row">
<button class="btn btn-accent" onclick="refreshAll()">Refresh</button>
<span class="msg" id="msg"></span>
</div>
<h2>Servers</h2>
<div class="tbl-wrap">
<table>
<thead><tr><th>Folder</th><th>Name</th><th>Owner</th><th>Main File</th><th>Status</th><th>Ban</th></tr></thead>
<tbody id="serversT"></tbody>
</table>
</div>
<h2 style="margin-top:26px">Users</h2>
<div class="row"><input id="searchUser" placeholder="Search username..." oninput="filterUsers()"></div>
<div class="tbl-wrap">
<table>
<thead><tr><th>Username</th><th>Email</th><th>Servers</th><th>Active</th><th>Actions</th></tr></thead>
<tbody id="usersT"></tbody>
</table>
</div>
<div class="msg">Administrators can ban or unban users and toggle premium status.</div>
<h2 style="margin-top:26px">API Keys</h2>
<div class="row" style="gap:8px;flex-wrap:wrap">
<input id="kLabel" placeholder="Label (e.g. ci-bot)">
<label><input type="checkbox" class="ks" value="read" checked> read</label>
<label><input type="checkbox" class="ks" value="create"> create</label>
<label><input type="checkbox" class="ks" value="files"> files</label>
<label><input type="checkbox" class="ks" value="control"> control</label>
<label><input type="checkbox" class="ks" value="admin"> admin</label>
<button class="btn btn-primary" onclick="createKey()">+ New Key</button>
</div>
<div id="kNew" class="msg" style="color:#6ee7b7;word-break:break-all"></div>
<div class="tbl-wrap">
<table>
<thead><tr><th>Label</th><th>Scopes</th><th>Active</th><th>Last used</th><th>Actions</th></tr></thead>
<tbody id="keysT"></tbody>
</table>
</div>
</div>
<script>
let usersCache = [];
let serversCache = [];
async function refreshKeys(){
const r=await fetch('/api/admin/api-keys');
const d=await r.json().catch(()=>({success:false}));
if(!d.success) return;
const t=document.getElementById('keysT');
t.innerHTML=(d.keys||[]).map(k=>`<tr>
<td>${escapeHtml(k.label)}</td>
<td>${escapeHtml((k.scopes||'[]').replace(/[\[\]"]/g,''))}</td>
<td>${k.active?'<span class="tag ok">YES</span>':'<span class="tag bad">NO</span>'}</td>
<td>${k.last_used?new Date(k.last_used*1000).toLocaleString():'-'}</td>
<td>${k.active?`<button class="btn btn-mini btn-danger" onclick="revokeKey('${k.sha||''}',${k.id})">Revoke</button>`:''}</td>
</tr>`).join('');
}
async function createKey(){
const label=document.getElementById('kLabel').value.trim()||'key';
const scopes=[...document.querySelectorAll('.ks:checked')].map(c=>c.value);
const r=await fetch('/api/admin/api-keys/create',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({label,scopes})});
const d=await r.json().catch(()=>({success:false}));
if(!d.success){ alert(d.message||'Failed'); return; }
document.getElementById('kNew').textContent='New key (shown once): '+d.key;
refreshKeys();
}
async function revokeKey(sha,id){
if(!confirm('Revoke this API key? It stops working within ~30s.')) return;
// sha (digest) is the portable identifier: auto-increment ids differ between
// the two gateway accounts' D1 databases. id kept as a legacy fallback.
const body = (sha && /^[0-9a-f]{64}$/.test(sha)) ? {sha} : {id};
await fetch('/api/admin/api-keys/revoke',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
refreshKeys();
}
function escapeHtml(s){
return (s||'')
.replaceAll('&','&amp;')
.replaceAll('<','&lt;')
.replaceAll('>','&gt;')
.replaceAll('"','&quot;')
.replaceAll("'","&#039;");
}
function tagStatus(s){
const st=(s||'').toLowerCase();
if(st==='running') return '<span class="tag ok">RUNNING</span>';
if(st==='installing'||st==='starting') return '<span class="tag mid">INSTALLING</span>';
if(st==='banned') return '<span class="tag bad">BANNED</span>';
return '<span class="tag">OFFLINE</span>';
}
async function refreshStats(){
const r=await fetch('/api/admin/quickstats');
const d=await r.json();
const s=d.stats||{};
st_total.textContent=s.servers_total||0;
st_run.textContent=s.servers_running||0;
st_ins.textContent=s.servers_installing||0;
st_ban.textContent=s.servers_banned||0;
u_total.textContent=s.users_total||0;
u_active.textContent=s.users_active||0;
}
async function refreshServers(){
const r=await fetch('/api/admin/servers');
const d=await r.json();
serversCache=d.servers||[];
const t=document.getElementById('serversT'); t.innerHTML='';
(d.servers||[]).forEach(s=>{
t.innerHTML += `
<tr>
<td>${escapeHtml(s.folder)}</td>
<td>${escapeHtml(s.title)}</td>
<td>${escapeHtml(s.owner)}</td>
<td>${escapeHtml(s.startup_file || '')}</td>
<td>${tagStatus(s.status)}</td>
<td class="actions">
${s.banned
? `<button class="btn" onclick="banServer('${s.key}', false)">Unban</button>`
: `<button class="btn" style="border-color:rgba(255,107,107,.35);color:var(--danger)" onclick="banServer('${s.key}', true)">Ban</button>`}
<button class="btn" style="border-color:rgba(255,107,107,.5);color:var(--danger)" onclick="deleteServer('${s.key}')">Delete</button>
</td>
</tr>`;
});
}
async function banServer(key, banned){
await fetch('/api/admin/server/ban',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({key,banned})});
refreshAll();
}
async function deleteServer(key){
const s=(serversCache||[]).find(x=>x.key===key) || {};
const title=s.title || s.folder || key;
if(!confirm(`Delete server "${title}" permanently?\nAll its files, logs and data will be removed.`)) return;
if(!confirm(`Are you sure about deleting "${title}"? This cannot be undone.`)) return;
const r=await fetch(`/server/delete/${encodeURIComponent(key)}`, {method:'POST'});
const d=await r.json().catch(()=>({success:false}));
if(!d.success){ alert(d.message || 'Failed to delete server'); }
refreshAll();
}
async function refreshUsers(){
const r=await fetch('/api/admin/users');
const d=await r.json();
usersCache=d.users||[];
renderUsers(usersCache);
}
function renderUsers(arr){
const t=document.getElementById('usersT'); t.innerHTML='';
arr.forEach(u=>{
t.innerHTML += `
<tr>
<td>${escapeHtml(u.username)}</td>
<td>${escapeHtml(u.email || '')}</td>
<td>${u.servers}</td>
<td>${u.active ? '<span class="tag ok">ACTIVE</span>' : '<span class="tag bad">BANNED</span>'}</td>
<td class="actions">
<button class="btn" onclick="setActive('${u.username}', ${u.active ? 'false':'true'})">${u.active ? 'Ban' : 'Unban'}</button>
</td>
</tr>`;
});
}
async function setActive(username, active){
await fetch('/api/admin/user/update',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username,active})});
refreshUsers(); refreshStats();
}
function filterUsers(){
const q=(document.getElementById('searchUser').value||'').toLowerCase().trim();
if(!q) return renderUsers(usersCache);
renderUsers(usersCache.filter(u => (u.username||'').toLowerCase().includes(q)));
}
async function refreshAll(){
document.getElementById('msg').textContent='Refreshing...';
try{
await Promise.all([refreshStats(), refreshServers(), refreshUsers(), refreshKeys()]);
}catch(e){
document.getElementById('msg').textContent='Network error — retrying...';
setTimeout(refreshAll, 4000);
return;
}
document.getElementById('msg').textContent='Done';
}
document.addEventListener('DOMContentLoaded', refreshAll);
</script>
</body>
</html>