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 }; };