635 lines
29 KiB
JavaScript
635 lines
29 KiB
JavaScript
window.INP2P = window.INP2P || {};
|
||
|
||
window.INP2P.createActions = function createActions(state, deps) {
|
||
const { api, loginByPassword, Vue } = deps;
|
||
|
||
const refreshAll = async () => {
|
||
if (!state.loggedIn.value) return;
|
||
state.globalBusy.value = true;
|
||
try {
|
||
const [h, s] = await Promise.all([api('/api/v1/health'), api('/api/v1/stats')]);
|
||
state.health.value = (h && typeof h === 'object') ? h : {};
|
||
state.stats.value = (s && typeof s === 'object') ? s : {};
|
||
|
||
const nd = await api('/api/v1/nodes');
|
||
state.nodes.value = Array.isArray(nd?.nodes) ? nd.nodes : [];
|
||
|
||
try {
|
||
const sdRes = await api('/api/v1/sdwans');
|
||
state.sd.value = (sdRes && typeof sdRes === 'object') ? sdRes : { nodes: [], routes: [], subnetProxies: [] };
|
||
if (!Array.isArray(state.sd.value.nodes)) state.sd.value.nodes = [];
|
||
if (!Array.isArray(state.sd.value.routes)) state.sd.value.routes = [];
|
||
if (!Array.isArray(state.sd.value.subnetProxies)) state.sd.value.subnetProxies = [];
|
||
|
||
// tenant subnet locked by backend: keep UI aligned
|
||
if (!state.isAdmin.value && state.subnet.value) {
|
||
state.sd.value.gatewayCIDR = state.subnet.value;
|
||
if (!state.sd.value.routes.length) {
|
||
state.sd.value.routes = [state.subnet.value];
|
||
}
|
||
}
|
||
} catch (e) {
|
||
state.toast('SDWAN 加载失败:' + (state.humanizeError ? state.humanizeError(e.message || '') : (e.message || '')), 'error');
|
||
}
|
||
|
||
if (state.isAdmin.value) {
|
||
try {
|
||
const settingsRes = await api('/api/v1/admin/settings');
|
||
state.settings.value = (settingsRes && typeof settingsRes === 'object') ? settingsRes : state.settings.value;
|
||
} catch(_) {}
|
||
try {
|
||
const a = await api('/api/v1/admin/audit?limit=50');
|
||
state.audit.value = Array.isArray(a?.logs) ? a.logs : [];
|
||
} catch(_) {}
|
||
}
|
||
} catch (e) {
|
||
state.toast(state.humanizeError ? state.humanizeError(e.message || '刷新失败') : (e.message || '刷新失败'), 'error');
|
||
} finally {
|
||
state.globalBusy.value = false;
|
||
}
|
||
};
|
||
|
||
const stopTimer = () => {
|
||
if (state.timer.value) {
|
||
clearInterval(state.timer.value);
|
||
state.timer.value = null;
|
||
}
|
||
};
|
||
|
||
const startTimer = () => {
|
||
stopTimer();
|
||
if (!state.loggedIn.value) return;
|
||
const sec = Math.max(5, Number(state.refreshSec.value || 15));
|
||
state.timer.value = setInterval(refreshAll, sec * 1000);
|
||
};
|
||
|
||
const login = async () => {
|
||
state.loginErr.value = '';
|
||
state.loginBusy.value = true;
|
||
try {
|
||
const uname = (state.loginUser.value || '').trim();
|
||
const uerr = state.validateUsername ? state.validateUsername(uname) : '';
|
||
if (uerr) throw new Error(uerr);
|
||
const d = await loginByPassword(uname, state.loginPass.value);
|
||
if (d.error) throw new Error(d.message || '用户名密码登录失败');
|
||
localStorage.setItem('t', d.token || '');
|
||
state.role.value = d.role || '';
|
||
state.status.value = d.status ?? 1;
|
||
state.tokenType.value = d.token_type || 'session';
|
||
state.tenantId.value = d.tenant_id || 0;
|
||
state.subnet.value = d.subnet || '';
|
||
if (state.status.value !== 1) throw new Error('账号已停用');
|
||
state.loggedIn.value = true;
|
||
await refreshAll();
|
||
state.toast('登录成功');
|
||
} catch (e) {
|
||
state.loginErr.value = e.message || '登录失败';
|
||
} finally {
|
||
state.loginBusy.value = false;
|
||
}
|
||
};
|
||
|
||
const logout = () => {
|
||
localStorage.removeItem('t');
|
||
localStorage.removeItem('master_t');
|
||
state.loggedIn.value = false;
|
||
state.role.value = '';
|
||
state.tokenType.value = '';
|
||
state.tenantId.value = 0;
|
||
state.subnet.value = '';
|
||
stopTimer();
|
||
};
|
||
|
||
const addSDWANNode = () => state.sd.value.nodes = [...(state.sd.value.nodes || []), { node:'', ip:'' }];
|
||
const removeSDWANNode = i => state.sd.value.nodes.splice(i, 1);
|
||
const addSubnetProxy = () => state.sd.value.subnetProxies = [...(state.sd.value.subnetProxies || []), { node:'', localCIDR:'', virtualCIDR:'' }];
|
||
const removeSubnetProxy = i => state.sd.value.subnetProxies.splice(i, 1);
|
||
const autoAssignIPs = () => {
|
||
const used = new Set();
|
||
(state.sd.value.nodes || []).forEach(n => { const p = (n.ip||'').split('.'); if (p.length===4) used.add(Number(p[3])); });
|
||
let k = 2;
|
||
for (const n of state.sd.value.nodes || []) {
|
||
if (!n.ip) {
|
||
while (used.has(k) && k < 254) k++;
|
||
n.ip = `10.10.0.${k}`;
|
||
used.add(k);
|
||
k++;
|
||
}
|
||
}
|
||
state.toast('已自动分配缺失 IP');
|
||
};
|
||
|
||
const saveSDWAN = async () => {
|
||
const verr = state.validateSDWAN ? state.validateSDWAN(state.sd.value) : '';
|
||
if (verr) {
|
||
state.toast(verr, 'error');
|
||
state.logEvent && state.logEvent('error', { tab:'sdwan', action:'save', message:verr });
|
||
return;
|
||
}
|
||
state.beginTab('sdwan');
|
||
try {
|
||
await api('/api/v1/sdwan/edit', { method:'POST', body: JSON.stringify(state.sd.value) });
|
||
state.toast('SDWAN 保存成功');
|
||
await refreshAll();
|
||
state.endTabOk('sdwan', !(state.sd.value?.nodes||[]).length);
|
||
} catch (e) { state.toast(state.humanizeError ? state.humanizeError(e.message) : (e.message||'操作失败'), 'error'); state.endTabErr('sdwan'); state.logEvent && state.logEvent('error', { tab:'sdwan', action:'save', message:e.message||'' }); }
|
||
};
|
||
|
||
const kickNode = async (node) => {
|
||
if (!confirm(`高风险操作:踢下线节点 ${node}\n这会中断该节点现有连接。确认继续?`)) return;
|
||
try { await api('/api/v1/nodes/kick', { method:'POST', body: JSON.stringify({ node }) }); state.toast('已发送踢下线'); refreshAll(); }
|
||
catch(e){ state.toast(state.humanizeError ? state.humanizeError(e.message) : (e.message||'操作失败'), 'error'); }
|
||
};
|
||
|
||
const openNodeEdit = (node, mode='alias') => {
|
||
if (!node || !node.nodeUUID) return state.toast('该节点尚无UUID,稍后重连后再试', 'error');
|
||
state.nodeEditNode.value = node;
|
||
state.nodeEditMode.value = mode;
|
||
state.nodeEditTitle.value = mode === 'alias' ? '编辑设备昵称' : '编辑设备虚拟IP';
|
||
state.nodeEditValue.value = mode === 'alias' ? (node.alias || '') : (node.virtualIP || '');
|
||
state.nodeEditVisible.value = true;
|
||
};
|
||
|
||
const closeNodeEdit = () => {
|
||
state.nodeEditVisible.value = false;
|
||
state.nodeEditNode.value = null;
|
||
state.nodeEditValue.value = '';
|
||
};
|
||
|
||
const submitNodeEdit = async () => {
|
||
const node = state.nodeEditNode.value;
|
||
if (!node || !node.nodeUUID) return;
|
||
const val = String(state.nodeEditValue.value || '').trim();
|
||
try {
|
||
if (state.nodeEditMode.value === 'alias') {
|
||
await api('/api/v1/nodes/alias', { method:'POST', body: JSON.stringify({ node_uuid: node.nodeUUID, alias: val }) });
|
||
state.toast('昵称已更新');
|
||
} else {
|
||
if (!val) return state.toast('虚拟IP不能为空', 'error');
|
||
await api('/api/v1/nodes/ip', { method:'POST', body: JSON.stringify({ node_uuid: node.nodeUUID, virtual_ip: val }) });
|
||
state.toast('IP已更新,节点将按新IP重连');
|
||
}
|
||
closeNodeEdit();
|
||
refreshAll();
|
||
} catch(e){ state.toast(state.humanizeError ? state.humanizeError(e.message) : (e.message||'操作失败'), 'error'); }
|
||
};
|
||
|
||
const renameNode = async (node) => openNodeEdit(node, 'alias');
|
||
const changeNodeIP = async (node) => openNodeEdit(node, 'ip');
|
||
|
||
const appPushNode = Vue.ref('');
|
||
const appPushRaw = Vue.ref('[{"appName":"demo","protocol":"tcp","srcPort":8080,"peerNode":"","dstHost":"127.0.0.1","dstPort":80,"enabled":1}]');
|
||
let switchTabRef = null;
|
||
const setSwitchTab = fn => { switchTabRef = fn; };
|
||
const openAppManager = (node) => { appPushNode.value = node; state.toast(`已选中 ${node},请在控制台执行推配置`); if (switchTabRef) switchTabRef('p2p'); };
|
||
const openConnect = (from) => { state.connectForm.value.from = from; if (switchTabRef) switchTabRef('p2p'); };
|
||
|
||
const pushAppConfigs = async () => {
|
||
if (!appPushNode.value) return state.toast('请先选择节点', 'error');
|
||
let apps;
|
||
try { apps = JSON.parse(appPushRaw.value); } catch(_) { return state.toast('配置 JSON 格式错误', 'error'); }
|
||
state.setTabBusy('p2p', true);
|
||
try { await api('/api/v1/nodes/apps', { method:'POST', body: JSON.stringify({ node: appPushNode.value, apps }) }); state.toast('配置已推送'); }
|
||
catch(e){ state.toast(state.humanizeError ? state.humanizeError(e.message) : (e.message||'操作失败'), 'error'); }
|
||
finally { state.setTabBusy('p2p', false); }
|
||
};
|
||
|
||
const doConnect = async () => {
|
||
const req = { ...state.connectForm.value };
|
||
if (!req.from || !req.to) return state.toast('请选择 from/to 节点', 'error');
|
||
state.setTabBusy('p2p', true);
|
||
try {
|
||
await api('/api/v1/connect', { method:'POST', body: JSON.stringify(req) });
|
||
state.toast('连接请求已发送');
|
||
} catch(e){ state.toast(state.humanizeError ? state.humanizeError(e.message) : (e.message||'操作失败'), 'error'); }
|
||
finally { state.setTabBusy('p2p', false); }
|
||
};
|
||
|
||
const loadTenants = async () => {
|
||
if (!state.isAdmin.value) { state.tenants.value = []; state.tabState.value = { ...state.tabState.value, tenants:'empty' }; return; }
|
||
state.beginTab('tenants');
|
||
try {
|
||
const d = await api('/api/v1/admin/tenants');
|
||
state.tenants.value = d.tenants || [];
|
||
state.endTabOk('tenants', !state.tenants.value.length);
|
||
} catch(e){ state.toast(state.humanizeError ? state.humanizeError(e.message) : (e.message||'操作失败'), 'error'); state.endTabErr('tenants'); }
|
||
};
|
||
|
||
const createTenant = async () => {
|
||
const terr = state.validateRequired ? state.validateRequired(state.tenantForm.value.name, '租户名') : (!state.tenantForm.value.name ? '租户名不能为空' : '');
|
||
if (terr) return state.toast(terr, 'error');
|
||
state.beginTab('tenants');
|
||
try {
|
||
const tenantName = state.tenantForm.value.name;
|
||
await api('/api/v1/admin/tenants', { method:'POST', body: JSON.stringify(state.tenantForm.value) });
|
||
state.tenantForm.value = { name:'', admin_password:'', operator_password:'' };
|
||
state.toast('租户创建成功'); state.opSummary.value = `已创建租户:${tenantName}`;
|
||
await loadTenants();
|
||
} catch(e){ state.toast(state.humanizeError ? state.humanizeError(e.message) : (e.message||'操作失败'), 'error'); state.endTabErr('tenants'); }
|
||
};
|
||
|
||
const setTenantStatus = async (id, st) => {
|
||
if (!confirm(`确认将租户 ${id} ${st===1?'启用':'停用'}?`)) return;
|
||
try { await api(`/api/v1/admin/tenants/${id}?status=${st}`, { method:'POST', body:'{}' }); state.toast('租户状态已更新'); state.opSummary.value = `租户 ${id} 已${st===1?'启用':'停用'}`; loadTenants(); }
|
||
catch(e){ state.toast(state.humanizeError ? state.humanizeError(e.message) : (e.message||'操作失败'), 'error'); }
|
||
};
|
||
|
||
const reassignTenantSubnet = async (id, currentSubnet='') => {
|
||
if (!state.isAdmin.value) return;
|
||
const next = prompt('输入新的租户子网(仅 10.200.x.0/24)', currentSubnet || '');
|
||
if (next === null) return;
|
||
const subnet = String(next || '').trim();
|
||
if (!subnet) return state.toast('子网不能为空', 'error');
|
||
try {
|
||
await api(`/api/v1/admin/tenants/${id}/subnet`, { method:'POST', body: JSON.stringify({ subnet }) });
|
||
state.toast('租户子网已重分配');
|
||
await loadTenants();
|
||
try { await refreshAll(); } catch(_) {}
|
||
} catch(e){ state.toast(e.message || '重分配失败', 'error'); }
|
||
};
|
||
|
||
const loadKeys = async () => {
|
||
if (!state.activeTenant.value) return;
|
||
state.beginTab('tenants');
|
||
try {
|
||
const d = await api(`/api/v1/admin/tenants/${state.activeTenant.value}/keys`);
|
||
state.keys.value = d.keys || [];
|
||
state.selectedKeyIds.value = [];
|
||
if (!state.keepFailedPool.value) state.bulkFailedKeyIds.value = [];
|
||
state.endTabOk('tenants', !state.keys.value.length);
|
||
} catch(e){ state.toast(state.humanizeError ? state.humanizeError(e.message) : (e.message||'操作失败'), 'error'); state.endTabErr('tenants'); }
|
||
};
|
||
|
||
const createKey = async () => {
|
||
if (!state.activeTenant.value) return state.toast('请先填写 Tenant ID', 'error');
|
||
const scopeErr = state.validateRequired ? state.validateRequired(state.keyForm.value.scope, 'scope') : '';
|
||
if (scopeErr) return state.toast(scopeErr, 'error');
|
||
state.beginTab('tenants');
|
||
try {
|
||
const d = await api(`/api/v1/admin/tenants/${state.activeTenant.value}/keys`, { method:'POST', body: JSON.stringify(state.keyForm.value) });
|
||
state.toast(`API Key 创建成功: ${d.api_key || ''}`);
|
||
await loadKeys();
|
||
} catch(e){ state.toast(state.humanizeError ? state.humanizeError(e.message) : (e.message||'操作失败'), 'error'); state.endTabErr('tenants'); }
|
||
};
|
||
|
||
const setKeyStatus = async (id, st) => {
|
||
if (!confirm(`确认将 Key ${id} ${st===1?'启用':'停用'}?`)) return;
|
||
try { await api(`/api/v1/admin/tenants/${state.activeTenant.value}/keys/${id}?status=${st}`, { method:'POST', body:'{}' }); state.toast('Key 状态已更新'); state.opSummary.value = `Key ${id} 已${st===1?'启用':'停用'}`; loadKeys(); }
|
||
catch(e){ state.toast(state.humanizeError ? state.humanizeError(e.message) : (e.message||'操作失败'), 'error'); }
|
||
};
|
||
|
||
const loadUsers = async () => {
|
||
if (!state.activeTenant.value) return;
|
||
state.beginTab('tenants');
|
||
try {
|
||
const d = await api(`/api/v1/admin/users?tenant=${state.activeTenant.value}`);
|
||
state.users.value = d.users || [];
|
||
state.selectedUserIds.value = [];
|
||
if (!state.keepFailedPool.value) state.bulkFailedUserIds.value = [];
|
||
state.endTabOk('tenants', !state.users.value.length);
|
||
} catch(e){ state.toast(state.humanizeError ? state.humanizeError(e.message) : (e.message||'操作失败'), 'error'); state.endTabErr('tenants'); }
|
||
};
|
||
|
||
const createUser = async () => {
|
||
if (!state.activeTenant.value) return state.toast('请先填写 Tenant ID', 'error');
|
||
const e1 = state.validateRequired ? state.validateRequired(state.userForm.value.email, 'email/username') : '';
|
||
const e2 = state.validateRequired ? state.validateRequired(state.userForm.value.password, 'password') : '';
|
||
if (e1 || e2) return state.toast(e1 || e2, 'error');
|
||
state.beginTab('tenants');
|
||
try {
|
||
const createdUser = state.userForm.value.email;
|
||
await api('/api/v1/admin/users', { method:'POST', body: JSON.stringify({ tenant: Number(state.activeTenant.value), ...state.userForm.value }) });
|
||
state.userForm.value = { role:'operator', email:'', password:'' };
|
||
state.toast('用户创建成功'); state.opSummary.value = `已创建用户:${createdUser}`;
|
||
loadUsers();
|
||
} catch(e){ state.toast(state.humanizeError ? state.humanizeError(e.message) : (e.message||'操作失败'), 'error'); state.endTabErr('tenants'); }
|
||
};
|
||
|
||
const setUserStatus = async (id, st) => {
|
||
if (!confirm(`确认将用户 ${id} ${st===1?'启用':'停用'}?`)) return;
|
||
try { await api(`/api/v1/admin/users/${id}?status=${st}`, { method:'POST', body:'{}' }); state.toast('用户状态已更新'); state.opSummary.value = `用户 ${id} 已${st===1?'启用':'停用'}`; loadUsers(); }
|
||
catch(e){ state.toast(state.humanizeError ? state.humanizeError(e.message) : (e.message||'操作失败'), 'error'); }
|
||
};
|
||
|
||
const resetUserPassword = async (id) => {
|
||
const p = prompt('请输入新密码(至少6位)');
|
||
if (!p) return;
|
||
try { await api(`/api/v1/admin/users/${id}/password`, { method:'POST', body: JSON.stringify({ password: p }) }); state.toast('密码已重置'); }
|
||
catch(e){ state.toast(state.humanizeError ? state.humanizeError(e.message) : (e.message||'操作失败'), 'error'); }
|
||
};
|
||
|
||
const deleteUser = async (id) => {
|
||
if (!confirm('高风险操作:删除用户后不可恢复。\n确认继续?')) return;
|
||
try { await api(`/api/v1/admin/users/${id}/delete`, { method:'POST', body:'{}' }); state.toast('用户已删除'); state.opSummary.value = `用户 ${id} 已删除` ; loadUsers(); }
|
||
catch(e){ state.toast(state.humanizeError ? state.humanizeError(e.message) : (e.message||'操作失败'), 'error'); }
|
||
};
|
||
|
||
const loadEnrolls = async () => {
|
||
state.beginTab('tenants');
|
||
try {
|
||
const d = await api('/api/v1/tenants/enroll');
|
||
state.enrolls.value = d.enrolls || [];
|
||
state.selectedEnrollIds.value = [];
|
||
if (!state.keepFailedPool.value) state.bulkFailedEnrollIds.value = [];
|
||
state.endTabOk('tenants', !state.enrolls.value.length);
|
||
} catch(e){ state.toast(state.humanizeError ? state.humanizeError(e.message) : (e.message||'操作失败'), 'error'); state.endTabErr('tenants'); }
|
||
};
|
||
|
||
const createEnroll = async () => {
|
||
state.beginTab('tenants');
|
||
try {
|
||
const d = await api('/api/v1/tenants/enroll', { method:'POST', body:'{}' });
|
||
state.toast(`enroll_code: ${d.enroll_code || ''}`); state.opSummary.value = `已生成 enroll code(租户 ${state.activeTenant.value})`;
|
||
loadEnrolls();
|
||
} catch(e){ state.toast(state.humanizeError ? state.humanizeError(e.message) : (e.message||'操作失败'), 'error'); state.endTabErr('tenants'); }
|
||
};
|
||
|
||
const setEnrollStatus = async (id, st) => {
|
||
try { await api(`/api/v1/enroll/consume/${id}?status=${st}`, { method:'POST', body:'{}' }); state.toast('enroll 状态已更新'); state.opSummary.value = `enroll ${id} 状态已更新为 ${st}`; loadEnrolls(); }
|
||
catch(e){ state.toast(state.humanizeError ? state.humanizeError(e.message) : (e.message||'操作失败'), 'error'); }
|
||
};
|
||
|
||
const consumeEnroll = async () => {
|
||
state.toast('consumeEnroll 为客户端配网流程,控制台当前不直接调用');
|
||
};
|
||
|
||
|
||
const toggleAllKeys = () => {
|
||
const ids = (state.filteredKeys.value || []).map(k => Number(k.id)).filter(Boolean);
|
||
if (!ids.length) return;
|
||
if ((state.selectedKeyIds.value || []).length === ids.length) state.selectedKeyIds.value = [];
|
||
else state.selectedKeyIds.value = ids;
|
||
};
|
||
|
||
const toggleAllUsers = () => {
|
||
const ids = (state.filteredUsers.value || []).map(u => Number(u.id)).filter(Boolean);
|
||
if (!ids.length) return;
|
||
if ((state.selectedUserIds.value || []).length === ids.length) state.selectedUserIds.value = [];
|
||
else state.selectedUserIds.value = ids;
|
||
};
|
||
|
||
const toggleAllEnrolls = () => {
|
||
const ids = (state.filteredEnrolls.value || []).map(e => Number(e.id)).filter(Boolean);
|
||
if (!ids.length) return;
|
||
if ((state.selectedEnrollIds.value || []).length === ids.length) state.selectedEnrollIds.value = [];
|
||
else state.selectedEnrollIds.value = ids;
|
||
};
|
||
|
||
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
||
const runWithRetry = async (fn, retries = 1) => {
|
||
let lastErr = null;
|
||
for (let i = 0; i <= retries; i++) {
|
||
try { return await fn(); }
|
||
catch (e) { lastErr = e; if (i < retries) await sleep(120); }
|
||
}
|
||
throw lastErr || new Error('unknown error');
|
||
};
|
||
|
||
const runBulk = async ({ ids, confirmText, onEach, afterReload, summaryLabel, setFailedIds }) => {
|
||
const list = (ids || []).slice();
|
||
if (!list.length) return false;
|
||
if (!confirm(confirmText)) return false;
|
||
|
||
state.setTabBusy('tenants', true);
|
||
let ok = 0, fail = 0;
|
||
const failedIds = [];
|
||
let cursor = 0;
|
||
const workers = Array.from({ length: Math.min(6, list.length) }, async () => {
|
||
while (cursor < list.length) {
|
||
const idx = cursor++;
|
||
const id = list[idx];
|
||
try {
|
||
await runWithRetry(() => onEach(id), 1);
|
||
ok++;
|
||
} catch (_) {
|
||
fail++;
|
||
failedIds.push(id);
|
||
}
|
||
}
|
||
});
|
||
|
||
await Promise.all(workers);
|
||
state.toast(`${summaryLabel}完成:成功 ${ok},失败 ${fail}${fail ? '(失败ID可重试)' : ''}`);
|
||
if (setFailedIds) setFailedIds(failedIds);
|
||
const failedShow = failedIds.slice(0, 8).join(',');
|
||
state.opSummary.value = `${summaryLabel}:成功 ${ok} / 失败 ${fail}${failedShow ? `|失败ID: ${failedShow}${failedIds.length>8?'...':''}` : ''}`;
|
||
try { await afterReload(); } finally { state.setTabBusy('tenants', false); }
|
||
return true;
|
||
};
|
||
|
||
const bulkSetKeyStatus = async (st) => {
|
||
const ids = (state.selectedKeyIds.value || []).slice();
|
||
if (!ids.length) return state.toast('请先选择至少 1 个 Key', 'error');
|
||
if (!state.activeTenant.value) return state.toast('请先填写 Tenant ID', 'error');
|
||
await runBulk({
|
||
ids,
|
||
confirmText: `批量操作确认:将 ${ids.length} 个 Key ${st===1?'启用':'停用'},是否继续?`,
|
||
onEach: (id) => api(`/api/v1/admin/tenants/${state.activeTenant.value}/keys/${id}?status=${st}`, { method:'POST', body:'{}' }),
|
||
afterReload: loadKeys,
|
||
summaryLabel: `Key 批量${st===1?'启用':'停用'}`,
|
||
setFailedIds: (arr) => { state.bulkFailedKeyIds.value = arr; }
|
||
});
|
||
};
|
||
|
||
const bulkSetUserStatus = async (st) => {
|
||
const ids = (state.selectedUserIds.value || []).slice();
|
||
if (!ids.length) return state.toast('请先选择至少 1 个用户', 'error');
|
||
await runBulk({
|
||
ids,
|
||
confirmText: `批量操作确认:将 ${ids.length} 个用户 ${st===1?'启用':'停用'},是否继续?`,
|
||
onEach: (id) => api(`/api/v1/admin/users/${id}?status=${st}`, { method:'POST', body:'{}' }),
|
||
afterReload: loadUsers,
|
||
summaryLabel: `用户批量${st===1?'启用':'停用'}`,
|
||
setFailedIds: (arr) => { state.bulkFailedUserIds.value = arr; }
|
||
});
|
||
};
|
||
|
||
const bulkSetEnrollStatus = async (st) => {
|
||
const ids = (state.selectedEnrollIds.value || []).slice();
|
||
if (!ids.length) return state.toast('请先选择至少 1 个 enroll', 'error');
|
||
await runBulk({
|
||
ids,
|
||
confirmText: `批量操作确认:将 ${ids.length} 个 enroll 置为状态 ${st},是否继续?`,
|
||
onEach: (id) => api(`/api/v1/enroll/consume/${id}?status=${st}`, { method:'POST', body:'{}' }),
|
||
afterReload: loadEnrolls,
|
||
summaryLabel: `enroll 批量状态变更`,
|
||
setFailedIds: (arr) => { state.bulkFailedEnrollIds.value = arr; }
|
||
});
|
||
};
|
||
|
||
|
||
const retryFailedKeyOps = async (st=0) => {
|
||
const ids = (state.bulkFailedKeyIds.value || []).slice();
|
||
if (!ids.length) return state.toast('没有可重试的 Key 失败项', 'error');
|
||
await runBulk({
|
||
ids,
|
||
confirmText: `重试确认:重试 ${ids.length} 个失败 Key 操作,是否继续?`,
|
||
onEach: (id) => api(`/api/v1/admin/tenants/${state.activeTenant.value}/keys/${id}?status=${st}`, { method:'POST', body:'{}' }),
|
||
afterReload: loadKeys,
|
||
summaryLabel: 'Key 失败项重试',
|
||
setFailedIds: (arr) => { state.bulkFailedKeyIds.value = arr; }
|
||
});
|
||
};
|
||
|
||
const retryFailedUserOps = async (st=0) => {
|
||
const ids = (state.bulkFailedUserIds.value || []).slice();
|
||
if (!ids.length) return state.toast('没有可重试的用户失败项', 'error');
|
||
await runBulk({
|
||
ids,
|
||
confirmText: `重试确认:重试 ${ids.length} 个失败用户操作,是否继续?`,
|
||
onEach: (id) => api(`/api/v1/admin/users/${id}?status=${st}`, { method:'POST', body:'{}' }),
|
||
afterReload: loadUsers,
|
||
summaryLabel: '用户失败项重试',
|
||
setFailedIds: (arr) => { state.bulkFailedUserIds.value = arr; }
|
||
});
|
||
};
|
||
|
||
const retryFailedEnrollOps = async (st=0) => {
|
||
const ids = (state.bulkFailedEnrollIds.value || []).slice();
|
||
if (!ids.length) return state.toast('没有可重试的 enroll 失败项', 'error');
|
||
await runBulk({
|
||
ids,
|
||
confirmText: `重试确认:重试 ${ids.length} 个失败 enroll 操作,是否继续?`,
|
||
onEach: (id) => api(`/api/v1/enroll/consume/${id}?status=${st}`, { method:'POST', body:'{}' }),
|
||
afterReload: loadEnrolls,
|
||
summaryLabel: 'enroll 失败项重试',
|
||
setFailedIds: (arr) => { state.bulkFailedEnrollIds.value = arr; }
|
||
});
|
||
};
|
||
|
||
const clearFailedPool = (scope='all') => {
|
||
if (scope === 'all' || scope === 'key') state.bulkFailedKeyIds.value = [];
|
||
if (scope === 'all' || scope === 'user') state.bulkFailedUserIds.value = [];
|
||
if (scope === 'all' || scope === 'enroll') state.bulkFailedEnrollIds.value = [];
|
||
state.toast(scope==='all' ? '全部失败池已清空' : '失败池已清空');
|
||
};
|
||
|
||
const saveSettings = async () => {
|
||
|
||
if (!state.isAdmin.value) return;
|
||
state.beginTab('settings');
|
||
try {
|
||
for (const k of Object.keys(state.settings.value || {})) {
|
||
await api('/api/v1/admin/settings', { method:'POST', body: JSON.stringify({ key: k, value: String(state.settings.value[k]) }) });
|
||
}
|
||
state.toast('高级设置已保存'); state.opSummary.value = '高级设置已保存';
|
||
state.settings.value = await api('/api/v1/admin/settings');
|
||
state.endTabOk('settings');
|
||
} catch (e) { state.toast(state.humanizeError ? state.humanizeError(e.message) : (e.message||'操作失败'), 'error'); state.endTabErr('settings'); }
|
||
};
|
||
|
||
const loadAudit = async () => {
|
||
if (!state.isAdmin.value) return;
|
||
state.beginTab('audit');
|
||
try {
|
||
const p = new URLSearchParams();
|
||
const f = state.auditFilters.value || {};
|
||
if (state.auditTenant.value) p.set('tenant', String(state.auditTenant.value));
|
||
if (f.action) p.set('action', String(f.action));
|
||
if (f.actor_type) p.set('actor_type', String(f.actor_type));
|
||
if (f.actor_id) p.set('actor_id', String(f.actor_id));
|
||
if (f.target_type) p.set('target_type', String(f.target_type));
|
||
if (f.target_id) p.set('target_id', String(f.target_id));
|
||
if (f.from) p.set('from', String(f.from));
|
||
if (f.to) p.set('to', String(f.to));
|
||
p.set('limit', String(Number(f.limit || 100)));
|
||
p.set('offset', String(Number(f.offset || 0)));
|
||
|
||
const d = await api('/api/v1/admin/audit?' + p.toString());
|
||
state.audit.value = d.logs || [];
|
||
state.endTabOk('audit', !state.audit.value.length);
|
||
state.toast('审计日志已刷新');
|
||
} catch (e) { state.toast(state.humanizeError ? state.humanizeError(e.message) : (e.message||'操作失败'), 'error'); state.endTabErr('audit'); }
|
||
};
|
||
|
||
|
||
const exportRowsAsCsv = (rows, tag='audit') => {
|
||
const esc = (v) => '"' + String(v ?? '').replace(/"/g, '""') + '"';
|
||
const head = ['id','actor_type','actor_id','action','target_type','target_id','detail','ip','created_at'];
|
||
const lines = [head.map(esc).join(',')];
|
||
for (const a of (rows || [])) {
|
||
lines.push([
|
||
a.id, a.actor_type, a.actor_id, a.action, a.target_type, a.target_id, a.detail, a.ip, a.created_at
|
||
].map(esc).join(','));
|
||
}
|
||
const csv = '' + lines.join('\n');
|
||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||
const url = URL.createObjectURL(blob);
|
||
const t = new Date();
|
||
const pad = (n) => String(n).padStart(2, '0');
|
||
const fn = `${tag}-${t.getFullYear()}${pad(t.getMonth()+1)}${pad(t.getDate())}-${pad(t.getHours())}${pad(t.getMinutes())}${pad(t.getSeconds())}.csv`;
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = fn;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
a.remove();
|
||
URL.revokeObjectURL(url);
|
||
};
|
||
|
||
const exportAuditCsv = () => {
|
||
if (!state.isAdmin.value) return;
|
||
const rows = state.audit.value || [];
|
||
if (!rows.length) return state.toast('当前无审计数据可导出', 'error');
|
||
exportRowsAsCsv(rows, 'audit');
|
||
state.toast('审计 CSV 已导出');
|
||
state.opSummary.value = `已导出审计 CSV(${rows.length} 条)`;
|
||
};
|
||
|
||
const exportAuditFailuresCsv = () => {
|
||
if (!state.isAdmin.value) return;
|
||
const rows = (state.audit.value || []).filter(a => /denied|failed|error|kick|delete/i.test(String(a.action||'')));
|
||
if (!rows.length) return state.toast('当前无失败/高风险审计可导出', 'error');
|
||
exportRowsAsCsv(rows, 'audit-failures');
|
||
state.toast('失败事件 CSV 已导出');
|
||
state.opSummary.value = `已导出失败/高风险审计 CSV(${rows.length} 条)`;
|
||
};
|
||
|
||
const applyAuditPreset = async (preset) => {
|
||
const f = { action:'', actor_type:'', actor_id:'', target_type:'', target_id:'', from:'', to:'', limit:100, offset:0 };
|
||
state.auditTenant.value = '';
|
||
if (preset === 'login_failed') f.action = 'auth_login_failed';
|
||
else if (preset === 'denied') f.action = 'enroll_consume_denied';
|
||
else if (preset === 'connect_denied') f.action = 'connect_request_denied';
|
||
else if (preset === 'all') {
|
||
// keep default
|
||
}
|
||
state.auditPreset.value = preset || 'all';
|
||
localStorage.setItem('inp2p_audit_preset', state.auditPreset.value);
|
||
state.auditFilters.value = f;
|
||
await loadAudit();
|
||
};
|
||
|
||
const clearAuditFilters = async () => {
|
||
|
||
state.auditFilters.value = { action:'', actor_type:'', actor_id:'', target_type:'', target_id:'', from:'', to:'', limit:100, offset:0 };
|
||
state.auditTenant.value = '';
|
||
await loadAudit();
|
||
};
|
||
|
||
const updateCharts = () => {};
|
||
|
||
const actions = {
|
||
refreshAll, startTimer, stopTimer,
|
||
login, logout,
|
||
saveSDWAN, addSDWANNode, removeSDWANNode, addSubnetProxy, removeSubnetProxy, autoAssignIPs,
|
||
kickNode, renameNode, changeNodeIP, openNodeEdit, closeNodeEdit, submitNodeEdit,
|
||
openAppManager, pushAppConfigs, openConnect, doConnect,
|
||
createTenant, loadTenants, setTenantStatus, reassignTenantSubnet,
|
||
createKey, loadKeys, setKeyStatus,
|
||
createUser, loadUsers, setUserStatus, resetUserPassword, deleteUser,
|
||
createEnroll, loadEnrolls, setEnrollStatus, consumeEnroll,
|
||
saveSettings, loadAudit, clearAuditFilters, applyAuditPreset, exportAuditCsv, exportAuditFailuresCsv,
|
||
toggleAllKeys, toggleAllUsers, toggleAllEnrolls,
|
||
bulkSetKeyStatus, bulkSetUserStatus, bulkSetEnrollStatus,
|
||
retryFailedKeyOps, retryFailedUserOps, retryFailedEnrollOps, clearFailedPool,
|
||
updateCharts
|
||
};
|
||
|
||
return {
|
||
actions,
|
||
refs: { appPushNode, appPushRaw },
|
||
setSwitchTab
|
||
};
|
||
};
|