feat: audit api, sdwan persist, relay fallback updates

This commit is contained in:
2026-03-06 14:47:03 +08:00
parent e96a2e5dd9
commit 57b4dadd42
26 changed files with 991 additions and 183 deletions

View File

@@ -31,11 +31,9 @@
<h1 class="text-2xl font-black text-white mb-2">INP2P 控制台</h1>
<p class="text-slate-500 text-sm mb-6">登录后可管理节点、SDWAN、连接与租户</p>
<div class="space-y-3">
<input v-model="loginTenant" class="ipt" placeholder="Tenant ID用户登录" @keyup.enter="login">
<input v-model="loginUser" class="ipt" placeholder="用户名(如 admin" @keyup.enter="login">
<input v-model="loginUser" class="ipt" placeholder="用户名全局唯一字母≥6位" @keyup.enter="login">
<input v-model="loginPass" class="ipt" type="password" placeholder="密码" @keyup.enter="login">
<div class="text-xs text-slate-500 text-center">或使用主 Token 登录(管理员)</div>
<input v-model="loginToken" class="ipt" type="password" placeholder="Master Token" @keyup.enter="login">
<div class="text-xs text-slate-500 text-center">用户名要求仅字母、长度≥6、全局唯一</div>
<button class="btn w-full" :disabled="busy" @click="login">{{ busy ? '登录中...' : '登录' }}</button>
<div class="text-[11px] text-slate-500 text-center">Build: {{ buildVersion }}</div>
<div v-if="loginErr" class="text-red-400 text-sm">{{ loginErr }}</div>
@@ -151,6 +149,23 @@
</div>
<button class="btn2 mt-3" @click="addSDWANNode">+ 添加节点</button>
</div>
<div class="glass rounded-xl p-4">
<div class="font-bold mb-3">子网代理Subnet Proxy</div>
<div class="text-xs text-slate-400 mb-2">示例local 192.168.0.0/24 → virtual 10.0.100.0/24掩码需一致</div>
<div class="space-y-2">
<div v-for="(s,i) in sd.subnetProxies" :key="i" class="grid grid-cols-1 md:grid-cols-6 gap-2">
<select class="ipt" v-model="s.node">
<option value="">选择节点</option>
<option v-for="x in nodes" :key="'sp'+x.name" :value="x.name">{{ x.name }}</option>
</select>
<input class="ipt md:col-span-2" v-model="s.localCIDR" placeholder="192.168.0.0/24">
<input class="ipt md:col-span-2" v-model="s.virtualCIDR" placeholder="10.0.100.0/24">
<button class="btn2" @click="removeSubnetProxy(i)">删除</button>
</div>
</div>
<button class="btn2 mt-3" @click="addSubnetProxy">+ 添加代理</button>
</div>
</div>
<div v-if="tab==='p2p'" class="space-y-4">
@@ -287,11 +302,11 @@ createApp({
const loggedIn = ref(false), busy = ref(false), msg = ref(''), msgType = ref('ok');
const role = ref(''), status = ref(1);
const loginTenant = ref('1'), loginUser = ref('admin'), loginPass = ref('admin'), loginToken = ref(''), loginErr = ref('');
const loginUser = ref(''), loginPass = ref(''), loginErr = ref('');
const refreshSec = ref(15), timer = ref(null);
const health = ref({}), stats = ref({}), nodes = ref([]), nodeKeyword = ref('');
const sd = ref({ enabled:false, name:'sdwan-main', gatewayCIDR:'10.10.0.0/24', mode:'mesh', hubNode:'', mtu:1420, nodes:[], routes:['10.10.0.0/24'] });
const sd = ref({ enabled:false, name:'sdwan-main', gatewayCIDR:'10.10.0.0/24', mode:'mesh', hubNode:'', mtu:1420, nodes:[], routes:['10.10.0.0/24'], subnetProxies:[] });
const connectForm = ref({ from:'', to:'', srcPort:80, dstPort:80, appName:'manual-connect' });
const tenants = ref([]), activeTenant = ref(1), keys = ref([]), users = ref([]), enrolls = ref([]);
@@ -300,7 +315,7 @@ createApp({
const userForm = ref({ role:'operator', email:'', password:'' });
const tokenType = ref('');
const isAdmin = computed(() => role.value === 'admin' && tokenType.value !== 'session');
const isAdmin = computed(() => role.value === 'admin');
const filteredTabs = computed(() => isAdmin.value ? tabs : tabs.filter(t => !['tenants','apikeys','users','enroll'].includes(t.id)));
const filteredNodes = computed(() => {
const k = (nodeKeyword.value || '').trim().toLowerCase();
@@ -341,19 +356,14 @@ createApp({
loginErr.value = '';
busy.value = true;
try {
let d;
if ((loginToken.value || '').trim()) {
d = await fetch('/api/v1/auth/login', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ token: loginToken.value.trim() }) }).then(r=>r.json());
if (d.error) throw new Error(d.message || 'token 登录失败');
localStorage.setItem('master_t', d.token || '');
} else {
d = await fetch('/api/v1/auth/login', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ tenant: Number(loginTenant.value || 1), username: loginUser.value, password: loginPass.value }) }).then(r=>r.json());
if (d.error) throw new Error(d.message || '用户名密码登录失败');
}
const uname = (loginUser.value || '').trim();
if (!/^[A-Za-z]{6,}$/.test(uname)) throw new Error('用户名需仅字母且≥6位');
const d = await fetch('/api/v1/auth/login', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ username: uname, password: loginPass.value }) }).then(r=>r.json());
if (d.error) throw new Error(d.message || '用户名密码登录失败');
localStorage.setItem('t', d.token || '');
role.value = d.role || '';
status.value = d.status ?? 1;
tokenType.value = d.token_type || (localStorage.getItem('t') === localStorage.getItem('master_t') ? 'master' : 'apikey');
tokenType.value = d.token_type || 'session';
if (status.value !== 1) throw new Error('账号已停用');
loggedIn.value = true;
await refreshAll();
@@ -398,6 +408,8 @@ createApp({
};
const addSDWANNode = () => sd.value.nodes = [...(sd.value.nodes || []), { node:'', ip:'' }];
const removeSDWANNode = i => sd.value.nodes.splice(i, 1);
const addSubnetProxy = () => sd.value.subnetProxies = [...(sd.value.subnetProxies || []), { node:'', localCIDR:'', virtualCIDR:'' }];
const removeSubnetProxy = i => sd.value.subnetProxies.splice(i, 1);
const autoAssignIPs = () => {
const used = new Set();
(sd.value.nodes || []).forEach(n => { const p = (n.ip||'').split('.'); if (p.length===4) used.add(Number(p[3])); });
@@ -575,11 +587,11 @@ createApp({
return {
buildVersion, tab, filteredTabs, loggedIn, busy, msg, msgType, role, status, tokenType,
loginTenant, loginUser, loginPass, loginToken, loginErr, refreshSec,
loginUser, loginPass, loginErr, refreshSec,
health, stats, nodes, nodeKeyword, filteredNodes, sd, connectForm,
tenants, activeTenant, keys, users, enrolls, tenantForm, keyForm, userForm,
natText, uptime, fmtTime,
login, logout, refreshAll, saveSDWAN, addSDWANNode, removeSDWANNode, autoAssignIPs,
login, logout, refreshAll, saveSDWAN, addSDWANNode, removeSDWANNode, addSubnetProxy, removeSubnetProxy, autoAssignIPs,
kickNode, renameNode, changeNodeIP, openAppManager, pushAppConfigs, openConnect, doConnect,
createTenant, loadTenants, setTenantStatus,
createKey, loadKeys, setKeyStatus,