feat: admin settings and audit UI
This commit is contained in:
@@ -195,6 +195,44 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="tab==='settings'" class="glass rounded-2xl p-4 space-y-4">
|
||||||
|
<div class="font-bold">高级设置(越权能力)</div>
|
||||||
|
<div class="text-xs text-slate-400">仅系统管理员可见。开启会记录审计日志。</div>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||||
|
<label class="flex items-center gap-2 text-sm"><input type="checkbox" v-model="settings.advanced_impersonate" true-value="1" false-value="0"> 代理租户(Impersonate)</label>
|
||||||
|
<label class="flex items-center gap-2 text-sm"><input type="checkbox" v-model="settings.advanced_force_network" true-value="1" false-value="0"> 强制干预租户网络</label>
|
||||||
|
<label class="flex items-center gap-2 text-sm"><input type="checkbox" v-model="settings.advanced_cross_tenant" true-value="1" false-value="0"> 跨租户互通策略</label>
|
||||||
|
</div>
|
||||||
|
<button class="btn" :disabled="busy" @click="saveSettings">保存高级设置</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="tab==='audit'" class="glass rounded-2xl p-4 space-y-4">
|
||||||
|
<div class="font-bold">审计日志</div>
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<input class="ipt max-w-[180px]" v-model="auditTenant" placeholder="tenant id (可空)">
|
||||||
|
<button class="btn2" :disabled="busy" @click="loadAudit">刷新</button>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-auto">
|
||||||
|
<table class="w-full text-sm min-w-[900px]">
|
||||||
|
<thead class="text-slate-400"><tr>
|
||||||
|
<th class="p-2 text-left">ID</th><th class="p-2 text-left">Actor</th><th class="p-2 text-left">Action</th><th class="p-2 text-left">Target</th><th class="p-2 text-left">Detail</th><th class="p-2 text-left">IP</th><th class="p-2 text-left">Time</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="a in audit" :key="a.id" class="border-t border-white/5">
|
||||||
|
<td class="p-2">{{ a.id }}</td>
|
||||||
|
<td class="p-2">{{ a.actor_type }}:{{ a.actor_id }}</td>
|
||||||
|
<td class="p-2">{{ a.action }}</td>
|
||||||
|
<td class="p-2">{{ a.target_type }}:{{ a.target_id }}</td>
|
||||||
|
<td class="p-2 text-xs text-slate-400">{{ a.detail }}</td>
|
||||||
|
<td class="p-2">{{ a.ip }}</td>
|
||||||
|
<td class="p-2">{{ fmtTime(a.created_at*1000) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="!audit.length"><td class="p-4 text-center text-slate-500" colspan="7">暂无审计记录</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="tab==='tenants'" class="space-y-4">
|
<div v-if="tab==='tenants'" class="space-y-4">
|
||||||
<div class="glass rounded-xl p-4 space-y-2">
|
<div class="glass rounded-xl p-4 space-y-2">
|
||||||
<div class="font-bold">创建租户</div>
|
<div class="font-bold">创建租户</div>
|
||||||
@@ -297,7 +335,8 @@ createApp({
|
|||||||
const tab = ref('dashboard');
|
const tab = ref('dashboard');
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{id:'dashboard',name:'仪表盘'},{id:'nodes',name:'节点'},{id:'sdwan',name:'SDWAN'},{id:'p2p',name:'P2P'},
|
{id:'dashboard',name:'仪表盘'},{id:'nodes',name:'节点'},{id:'sdwan',name:'SDWAN'},{id:'p2p',name:'P2P'},
|
||||||
{id:'tenants',name:'租户'},{id:'apikeys',name:'API Key'},{id:'users',name:'用户'},{id:'enroll',name:'Enroll'}
|
{id:'tenants',name:'租户'},{id:'apikeys',name:'API Key'},{id:'users',name:'用户'},{id:'enroll',name:'Enroll'},
|
||||||
|
{id:'settings',name:'高级设置'},{id:'audit',name:'审计日志'}
|
||||||
];
|
];
|
||||||
|
|
||||||
const loggedIn = ref(false), busy = ref(false), msg = ref(''), msgType = ref('ok');
|
const loggedIn = ref(false), busy = ref(false), msg = ref(''), msgType = ref('ok');
|
||||||
@@ -315,8 +354,11 @@ createApp({
|
|||||||
const userForm = ref({ role:'operator', email:'', password:'' });
|
const userForm = ref({ role:'operator', email:'', password:'' });
|
||||||
|
|
||||||
const tokenType = ref('');
|
const tokenType = ref('');
|
||||||
|
const settings = ref({ advanced_impersonate:'0', advanced_force_network:'0', advanced_cross_tenant:'0' });
|
||||||
|
const audit = ref([]);
|
||||||
|
const auditTenant = ref('');
|
||||||
const isAdmin = computed(() => role.value === 'admin');
|
const isAdmin = computed(() => role.value === 'admin');
|
||||||
const filteredTabs = computed(() => isAdmin.value ? tabs : tabs.filter(t => !['tenants','apikeys','users','enroll'].includes(t.id)));
|
const filteredTabs = computed(() => isAdmin.value ? tabs : tabs.filter(t => !['tenants','apikeys','users','enroll','settings','audit'].includes(t.id)));
|
||||||
const filteredNodes = computed(() => {
|
const filteredNodes = computed(() => {
|
||||||
const k = (nodeKeyword.value || '').trim().toLowerCase();
|
const k = (nodeKeyword.value || '').trim().toLowerCase();
|
||||||
if (!k) return nodes.value;
|
if (!k) return nodes.value;
|
||||||
@@ -392,6 +434,10 @@ createApp({
|
|||||||
const nd = await api('/api/v1/nodes');
|
const nd = await api('/api/v1/nodes');
|
||||||
nodes.value = nd.nodes || [];
|
nodes.value = nd.nodes || [];
|
||||||
sd.value = await api('/api/v1/sdwans');
|
sd.value = await api('/api/v1/sdwans');
|
||||||
|
if (isAdmin.value) {
|
||||||
|
try { settings.value = await api('/api/v1/admin/settings'); } catch(_) {}
|
||||||
|
try { const a = await api('/api/v1/admin/audit?limit=50'); audit.value = a.logs || []; } catch(_) {}
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast(e.message || '刷新失败', 'error');
|
toast(e.message || '刷新失败', 'error');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -545,6 +591,27 @@ createApp({
|
|||||||
try { const d = await api('/api/v1/tenants/enroll'); enrolls.value = d.enrolls || []; }
|
try { const d = await api('/api/v1/tenants/enroll'); enrolls.value = d.enrolls || []; }
|
||||||
catch(e){ toast(e.message, 'error'); }
|
catch(e){ toast(e.message, 'error'); }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const saveSettings = async () => {
|
||||||
|
if (!isAdmin.value) return;
|
||||||
|
try {
|
||||||
|
for (const k of Object.keys(settings.value || {})) {
|
||||||
|
await api('/api/v1/admin/settings', { method:'POST', body: JSON.stringify({ key: k, value: String(settings.value[k]) }) });
|
||||||
|
}
|
||||||
|
toast('高级设置已保存');
|
||||||
|
settings.value = await api('/api/v1/admin/settings');
|
||||||
|
} catch (e) { toast(e.message, 'error'); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadAudit = async () => {
|
||||||
|
if (!isAdmin.value) return;
|
||||||
|
try {
|
||||||
|
const q = auditTenant.value ? `?tenant=${auditTenant.value}&limit=100` : '?limit=100';
|
||||||
|
const d = await api('/api/v1/admin/audit' + q);
|
||||||
|
audit.value = d.logs || [];
|
||||||
|
toast('审计日志已刷新');
|
||||||
|
} catch (e) { toast(e.message, 'error'); }
|
||||||
|
};
|
||||||
const createEnroll = async () => {
|
const createEnroll = async () => {
|
||||||
try {
|
try {
|
||||||
const d = await api('/api/v1/tenants/enroll', { method:'POST', body:'{}' });
|
const d = await api('/api/v1/tenants/enroll', { method:'POST', body:'{}' });
|
||||||
@@ -590,6 +657,7 @@ createApp({
|
|||||||
loginUser, loginPass, loginErr, refreshSec,
|
loginUser, loginPass, loginErr, refreshSec,
|
||||||
health, stats, nodes, nodeKeyword, filteredNodes, sd, connectForm,
|
health, stats, nodes, nodeKeyword, filteredNodes, sd, connectForm,
|
||||||
tenants, activeTenant, keys, users, enrolls, tenantForm, keyForm, userForm,
|
tenants, activeTenant, keys, users, enrolls, tenantForm, keyForm, userForm,
|
||||||
|
settings, audit, auditTenant,
|
||||||
natText, uptime, fmtTime,
|
natText, uptime, fmtTime,
|
||||||
login, logout, refreshAll, saveSDWAN, addSDWANNode, removeSDWANNode, addSubnetProxy, removeSubnetProxy, autoAssignIPs,
|
login, logout, refreshAll, saveSDWAN, addSDWANNode, removeSDWANNode, addSubnetProxy, removeSubnetProxy, autoAssignIPs,
|
||||||
kickNode, renameNode, changeNodeIP, openAppManager, pushAppConfigs, openConnect, doConnect,
|
kickNode, renameNode, changeNodeIP, openAppManager, pushAppConfigs, openConnect, doConnect,
|
||||||
@@ -597,6 +665,7 @@ createApp({
|
|||||||
createKey, loadKeys, setKeyStatus,
|
createKey, loadKeys, setKeyStatus,
|
||||||
createUser, loadUsers, setUserStatus, resetUserPassword, deleteUser,
|
createUser, loadUsers, setUserStatus, resetUserPassword, deleteUser,
|
||||||
createEnroll, loadEnrolls, setEnrollStatus, consumeEnroll,
|
createEnroll, loadEnrolls, setEnrollStatus, consumeEnroll,
|
||||||
|
saveSettings, loadAudit,
|
||||||
updateCharts
|
updateCharts
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user