328 lines
14 KiB
HTML
328 lines
14 KiB
HTML
<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>管理后台</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<style>
|
|
:root {
|
|
--bg: linear-gradient(135deg, #0f172a, #1e293b);
|
|
--card: #0b1224;
|
|
--accent: #22d3ee;
|
|
--accent-2: #a855f7;
|
|
--text: #e2e8f0;
|
|
--muted: #94a3b8;
|
|
--danger: #f87171;
|
|
}
|
|
body { margin:0; min-height:100vh; background:var(--bg); color:var(--text); font-family:"Segoe UI","Helvetica Neue",Arial,sans-serif; padding:24px; }
|
|
.shell { max-width: 1100px; margin:0 auto; background:var(--card); border:1px solid rgba(255,255,255,0.08); border-radius:14px; padding:24px; box-shadow:0 25px 60px rgba(0,0,0,0.45); }
|
|
h1,h2 { margin:0 0 10px; }
|
|
label { display:block; font-weight:600; color:#cbd5e1; margin:8px 0 4px; }
|
|
input, textarea { width:100%; padding:10px 12px; border-radius:10px; border:1px solid rgba(255,255,255,0.08); background:rgba(255,255,255,0.04); color:var(--text); }
|
|
button { padding:10px 14px; border:none; border-radius:10px; cursor:pointer; background:linear-gradient(135deg,var(--accent),var(--accent-2)); color:var(--text); font-weight:700; }
|
|
table { width:100%; border-collapse:collapse; margin-top:12px; }
|
|
th, td { padding:10px; border-bottom:1px solid rgba(255,255,255,0.08); text-align:left; }
|
|
.muted { color:var(--muted); }
|
|
.status { margin-top:12px; padding:10px; border-radius:10px; background:rgba(255,255,255,0.04); border:1px solid rgba(255,255,255,0.08); }
|
|
.error { color: var(--danger); }
|
|
.grid { display:grid; gap:10px; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
|
|
.section { margin-top:20px; padding:14px; border-radius:12px; border:1px solid rgba(255,255,255,0.06); }
|
|
.row { display:flex; gap:10px; flex-wrap:wrap; }
|
|
.row > div { flex:1; min-width:220px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="shell">
|
|
<h1>管理后台</h1>
|
|
<p class="muted" style="margin-top:-6px;">输入 .env 的 ZHIYUN_PASS 校验后可管理 AWS 账户与 IP 映射。</p>
|
|
|
|
<div class="status" id="auth-box">
|
|
<div class="row">
|
|
<div style="flex:2;">
|
|
<label for="auth-pass">校验密码</label>
|
|
<input id="auth-pass" type="password" placeholder="输入 ZHIYUN_PASS">
|
|
</div>
|
|
<div style="flex:1; align-self:flex-end;">
|
|
<button id="auth-btn" style="width:100%;">校验</button>
|
|
</div>
|
|
</div>
|
|
<div id="auth-msg" class="muted" style="margin-top:6px;"></div>
|
|
</div>
|
|
|
|
<div id="content" style="display:none;">
|
|
<div class="section">
|
|
<h2>AWS 账户</h2>
|
|
<div class="grid">
|
|
<div>
|
|
<label for="acc-name">name</label>
|
|
<input id="acc-name" placeholder="如 account_a">
|
|
</div>
|
|
<div>
|
|
<label for="acc-region">region</label>
|
|
<input id="acc-region" placeholder="如 us-east-1">
|
|
</div>
|
|
<div>
|
|
<label for="acc-aki">access_key_id</label>
|
|
<input id="acc-aki">
|
|
</div>
|
|
<div>
|
|
<label for="acc-sak">secret_access_key</label>
|
|
<input id="acc-sak">
|
|
</div>
|
|
<div>
|
|
<label for="acc-ami">ami_id</label>
|
|
<input id="acc-ami">
|
|
</div>
|
|
<div>
|
|
<label for="acc-subnet">subnet_id (可选)</label>
|
|
<input id="acc-subnet">
|
|
</div>
|
|
<div>
|
|
<label for="acc-sg">security_group_ids 逗号分隔 (可选)</label>
|
|
<input id="acc-sg" placeholder="sg-1,sg-2">
|
|
</div>
|
|
<div>
|
|
<label for="acc-key">key_name (可选)</label>
|
|
<input id="acc-key">
|
|
</div>
|
|
</div>
|
|
<div style="margin-top:10px; display:flex; gap:10px; flex-wrap:wrap;">
|
|
<button id="acc-save">新增/更新</button>
|
|
<button id="acc-del" style="background:#ef4444;">删除</button>
|
|
</div>
|
|
<div id="acc-msg" class="status" style="display:none;"></div>
|
|
<table id="acc-table" style="display:none;">
|
|
<thead>
|
|
<tr>
|
|
<th>name</th><th>region</th><th>ami_id</th><th>subnet_id</th><th>sg_ids</th><th>key_name</th><th>更新</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>IP-账户映射</h2>
|
|
<div class="row">
|
|
<div>
|
|
<label for="map-ip">IP 地址</label>
|
|
<input id="map-ip" placeholder="如 54.12.34.56">
|
|
</div>
|
|
<div>
|
|
<label for="map-acc">账户名</label>
|
|
<input id="map-acc" placeholder="对应 aws_accounts.name">
|
|
</div>
|
|
</div>
|
|
<div style="margin-top:10px; display:flex; gap:10px; flex-wrap:wrap;">
|
|
<button id="map-save">新增/更新</button>
|
|
<button id="map-del" style="background:#ef4444;">删除</button>
|
|
</div>
|
|
<div id="map-msg" class="status" style="display:none;"></div>
|
|
<table id="map-table" style="display:none;">
|
|
<thead>
|
|
<tr><th>IP</th><th>账户</th></tr>
|
|
</thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const authBtn = document.getElementById('auth-btn');
|
|
const authPass = document.getElementById('auth-pass');
|
|
const authMsg = document.getElementById('auth-msg');
|
|
const content = document.getElementById('content');
|
|
|
|
const accTable = document.getElementById('acc-table');
|
|
const accBody = accTable.querySelector('tbody');
|
|
const accMsg = document.getElementById('acc-msg');
|
|
const accInputs = {
|
|
name: document.getElementById('acc-name'),
|
|
region: document.getElementById('acc-region'),
|
|
aki: document.getElementById('acc-aki'),
|
|
sak: document.getElementById('acc-sak'),
|
|
ami: document.getElementById('acc-ami'),
|
|
subnet: document.getElementById('acc-subnet'),
|
|
sg: document.getElementById('acc-sg'),
|
|
key: document.getElementById('acc-key'),
|
|
};
|
|
const accSave = document.getElementById('acc-save');
|
|
const accDel = document.getElementById('acc-del');
|
|
|
|
const mapTable = document.getElementById('map-table');
|
|
const mapBody = mapTable.querySelector('tbody');
|
|
const mapMsg = document.getElementById('map-msg');
|
|
const mapIp = document.getElementById('map-ip');
|
|
const mapAcc = document.getElementById('map-acc');
|
|
const mapSave = document.getElementById('map-save');
|
|
const mapDel = document.getElementById('map-del');
|
|
|
|
function showMsg(el, text, isError=false) {
|
|
el.style.display = 'block';
|
|
el.className = 'status' + (isError ? ' error' : '');
|
|
el.textContent = text;
|
|
}
|
|
|
|
async function auth() {
|
|
authBtn.disabled = true;
|
|
authMsg.textContent = '校验中...';
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append('password', authPass.value);
|
|
const resp = await fetch('/admin/auth', { method:'POST', body: formData });
|
|
const data = await resp.json();
|
|
if (!resp.ok) throw new Error(data.error || '校验失败');
|
|
authMsg.textContent = '校验成功';
|
|
content.style.display = 'block';
|
|
await Promise.all([loadAccounts(), loadMappings()]);
|
|
} catch (err) {
|
|
authMsg.textContent = err.message;
|
|
content.style.display = 'none';
|
|
} finally {
|
|
authBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
async function loadAccounts() {
|
|
try {
|
|
const resp = await fetch('/admin/aws/list');
|
|
const data = await resp.json();
|
|
if (!resp.ok) throw new Error(data.error || '获取失败');
|
|
const items = data.items || [];
|
|
accBody.innerHTML = items.map(item => `
|
|
<tr>
|
|
<td>${item.name}</td>
|
|
<td>${item.region}</td>
|
|
<td>${item.ami_id}</td>
|
|
<td>${item.subnet_id || ''}</td>
|
|
<td>${(item.security_group_ids || []).join(', ')}</td>
|
|
<td>${item.key_name || ''}</td>
|
|
<td>${item.updated_at || ''}</td>
|
|
</tr>
|
|
`).join('');
|
|
accTable.style.display = 'table';
|
|
} catch (err) {
|
|
showMsg(accMsg, err.message, true);
|
|
}
|
|
}
|
|
|
|
async function saveAccount() {
|
|
const formData = new FormData();
|
|
formData.append('name', accInputs.name.value.trim());
|
|
formData.append('region', accInputs.region.value.trim());
|
|
formData.append('access_key_id', accInputs.aki.value.trim());
|
|
formData.append('secret_access_key', accInputs.sak.value.trim());
|
|
formData.append('ami_id', accInputs.ami.value.trim());
|
|
formData.append('subnet_id', accInputs.subnet.value.trim());
|
|
formData.append('security_group_ids', accInputs.sg.value.trim());
|
|
formData.append('key_name', accInputs.key.value.trim());
|
|
accSave.disabled = true;
|
|
try {
|
|
const resp = await fetch('/admin/aws/upsert', { method:'POST', body: formData });
|
|
const data = await resp.json();
|
|
if (!resp.ok) throw new Error(data.error || '保存失败');
|
|
showMsg(accMsg, '保存成功');
|
|
await loadAccounts();
|
|
} catch (err) {
|
|
showMsg(accMsg, err.message, true);
|
|
} finally {
|
|
accSave.disabled = false;
|
|
}
|
|
}
|
|
|
|
async function deleteAccount() {
|
|
const name = accInputs.name.value.trim();
|
|
if (!name) {
|
|
showMsg(accMsg, '删除前请填写 name', true);
|
|
return;
|
|
}
|
|
accDel.disabled = true;
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append('name', name);
|
|
const resp = await fetch('/admin/aws/delete', { method:'POST', body: formData });
|
|
const data = await resp.json();
|
|
if (!resp.ok) throw new Error(data.error || '删除失败');
|
|
showMsg(accMsg, '删除成功');
|
|
await loadAccounts();
|
|
} catch (err) {
|
|
showMsg(accMsg, err.message, true);
|
|
} finally {
|
|
accDel.disabled = false;
|
|
}
|
|
}
|
|
|
|
async function loadMappings() {
|
|
try {
|
|
const resp = await fetch('/mapping/list');
|
|
const data = await resp.json();
|
|
if (!resp.ok) throw new Error(data.error || '获取失败');
|
|
const items = data.items || [];
|
|
mapBody.innerHTML = items.map(item => `
|
|
<tr>
|
|
<td>${item.ip_address}</td>
|
|
<td>${item.account_name}</td>
|
|
</tr>
|
|
`).join('');
|
|
mapTable.style.display = 'table';
|
|
} catch (err) {
|
|
showMsg(mapMsg, err.message, true);
|
|
}
|
|
}
|
|
|
|
async function saveMapping() {
|
|
const ip = mapIp.value.trim();
|
|
const acc = mapAcc.value.trim();
|
|
if (!ip || !acc) {
|
|
showMsg(mapMsg, 'IP 和账户名不能为空', true);
|
|
return;
|
|
}
|
|
mapSave.disabled = true;
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append('ip', ip);
|
|
formData.append('account', acc);
|
|
const resp = await fetch('/mapping/upsert', { method:'POST', body: formData });
|
|
const data = await resp.json();
|
|
if (!resp.ok) throw new Error(data.error || '保存失败');
|
|
showMsg(mapMsg, '保存成功');
|
|
await loadMappings();
|
|
} catch (err) {
|
|
showMsg(mapMsg, err.message, true);
|
|
} finally {
|
|
mapSave.disabled = false;
|
|
}
|
|
}
|
|
|
|
async function deleteMapping() {
|
|
const ip = mapIp.value.trim();
|
|
if (!ip) {
|
|
showMsg(mapMsg, '删除前请填写 IP', true);
|
|
return;
|
|
}
|
|
mapDel.disabled = true;
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append('ip', ip);
|
|
const resp = await fetch('/mapping/delete', { method:'POST', body: formData });
|
|
const data = await resp.json();
|
|
if (!resp.ok) throw new Error(data.error || '删除失败');
|
|
showMsg(mapMsg, '删除成功');
|
|
await loadMappings();
|
|
} catch (err) {
|
|
showMsg(mapMsg, err.message, true);
|
|
} finally {
|
|
mapDel.disabled = false;
|
|
}
|
|
}
|
|
|
|
authBtn.addEventListener('click', auth);
|
|
accSave.addEventListener('click', saveAccount);
|
|
accDel.addEventListener('click', deleteAccount);
|
|
mapSave.addEventListener('click', saveMapping);
|
|
mapDel.addEventListener('click', deleteMapping);
|
|
</script>
|
|
</body>
|
|
</html>
|