feat: 展示真实运维审计日志
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
import type {
|
||||
AgentCard,
|
||||
AgentInvocation,
|
||||
AuditLogEntry,
|
||||
CreateAgentInvocationPayload,
|
||||
CreateDeployRunPayload,
|
||||
DashboardSummary,
|
||||
@@ -267,6 +268,12 @@ export const devopsApi = {
|
||||
return toJenkinsSyncSummary(data, projectList)
|
||||
},
|
||||
|
||||
async getAuditLogs(): Promise<AuditLogEntry[]> {
|
||||
const { data } = await http.get<AuditLogEntry[]>('/audit-logs')
|
||||
lastDataSource = 'backend'
|
||||
return Array.isArray(data) ? data.map(sanitizeAuditLogEntry) : []
|
||||
},
|
||||
|
||||
async getAgentCards(): Promise<AgentCard[]> {
|
||||
return agentCards
|
||||
},
|
||||
@@ -416,6 +423,22 @@ function sanitizeIntegrationNote(note?: string): string | undefined {
|
||||
.replace(/key=[^&\s]+/g, 'key=[已隐藏]')
|
||||
}
|
||||
|
||||
function safeText(value: unknown, fallback: string): string {
|
||||
return typeof value === 'string' && value.trim() ? value : fallback
|
||||
}
|
||||
|
||||
function optionalSafeText(value: unknown): string | undefined {
|
||||
return typeof value === 'string' && value.trim() ? value : undefined
|
||||
}
|
||||
|
||||
function safeRecord(value: unknown): Record<string, unknown> | undefined {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return value as Record<string, unknown>
|
||||
}
|
||||
|
||||
function toProjectConfigs(backendProjects: BackendProject[]): ProjectConfig[] {
|
||||
return backendProjects.map((project) => {
|
||||
const repo = parseRepository(project.repositoryUrl, project.key)
|
||||
@@ -498,6 +521,24 @@ function toJenkinsSyncSummary(
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeAuditLogEntry(record: AuditLogEntry): AuditLogEntry {
|
||||
return {
|
||||
id: safeText(record.id, 'audit-unknown'),
|
||||
action: safeText(record.action, 'UNKNOWN_ACTION'),
|
||||
resourceType: safeText(record.resourceType, 'unknown'),
|
||||
resourceId: optionalSafeText(record.resourceId),
|
||||
actorId: optionalSafeText(record.actorId),
|
||||
actorName: optionalSafeText(record.actorName),
|
||||
requestId: optionalSafeText(record.requestId),
|
||||
sourceIp: optionalSafeText(record.sourceIp),
|
||||
userAgent: optionalSafeText(record.userAgent),
|
||||
before: safeRecord(record.before),
|
||||
after: safeRecord(record.after),
|
||||
parameterDigest: safeRecord(record.parameterDigest),
|
||||
createdAt: safeText(record.createdAt, ''),
|
||||
}
|
||||
}
|
||||
|
||||
function toPipelineStep(step: BackendDeployRunStep): PipelineStep {
|
||||
const bpmnNodeId = step.bpmnNodeId || step.id || 'unknown-step'
|
||||
const name = step.name || bpmnNodeId
|
||||
|
||||
+16
-1
@@ -3,6 +3,7 @@ import { defineStore } from 'pinia'
|
||||
import { devopsApi } from '../services/devopsApi'
|
||||
import type {
|
||||
AgentCard,
|
||||
AuditLogEntry,
|
||||
CreateDeployRunPayload,
|
||||
IntegrationStatus,
|
||||
IntegrationConfigStatus,
|
||||
@@ -24,6 +25,7 @@ export const usePlatformStore = defineStore('platform', () => {
|
||||
const integrations = ref<IntegrationStatus[]>([])
|
||||
const agentCards = ref<AgentCard[]>([])
|
||||
const settings = ref<SystemSetting[]>([])
|
||||
const auditLogs = ref<AuditLogEntry[]>([])
|
||||
const integrationConfig = ref<IntegrationConfigStatus>({
|
||||
checkedAt: '',
|
||||
integrations: [],
|
||||
@@ -46,7 +48,12 @@ export const usePlatformStore = defineStore('platform', () => {
|
||||
agentCards.value = summary.agentCards
|
||||
settings.value = summary.settings
|
||||
integrationConfig.value = summary.integrationConfig
|
||||
releaseProcessXml.value = await devopsApi.getReleaseProcessXml()
|
||||
const [processXml, recentAuditLogs] = await Promise.all([
|
||||
devopsApi.getReleaseProcessXml(),
|
||||
devopsApi.getAuditLogs(),
|
||||
])
|
||||
releaseProcessXml.value = processXml
|
||||
auditLogs.value = recentAuditLogs
|
||||
dataSource.value = devopsApi.getLastDataSource()
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -98,6 +105,12 @@ export const usePlatformStore = defineStore('platform', () => {
|
||||
return refs
|
||||
}
|
||||
|
||||
async function loadAuditLogs() {
|
||||
auditLogs.value = await devopsApi.getAuditLogs()
|
||||
dataSource.value = devopsApi.getLastDataSource()
|
||||
return auditLogs.value
|
||||
}
|
||||
|
||||
function upsertRun(run: ReleaseRun) {
|
||||
runs.value = [run, ...runs.value.filter((item) => item.id !== run.id)]
|
||||
}
|
||||
@@ -110,6 +123,7 @@ export const usePlatformStore = defineStore('platform', () => {
|
||||
integrations,
|
||||
agentCards,
|
||||
settings,
|
||||
auditLogs,
|
||||
integrationConfig,
|
||||
releaseProcessXml,
|
||||
projectRefs,
|
||||
@@ -122,5 +136,6 @@ export const usePlatformStore = defineStore('platform', () => {
|
||||
syncJenkinsRun,
|
||||
syncJenkinsRuns,
|
||||
loadProjectRefs,
|
||||
loadAuditLogs,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -165,6 +165,22 @@ export interface JenkinsSyncSummary {
|
||||
failedRuns: JenkinsSyncError[]
|
||||
}
|
||||
|
||||
export interface AuditLogEntry {
|
||||
id: string
|
||||
action: string
|
||||
resourceType: string
|
||||
resourceId?: string
|
||||
actorId?: string
|
||||
actorName?: string
|
||||
requestId?: string
|
||||
sourceIp?: string
|
||||
userAgent?: string
|
||||
before?: Record<string, unknown>
|
||||
after?: Record<string, unknown>
|
||||
parameterDigest?: Record<string, unknown>
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface AgentCard {
|
||||
mode: AgentMode
|
||||
title: string
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { Refresh } from '@element-plus/icons-vue'
|
||||
import StatusPill from '../components/StatusPill.vue'
|
||||
import { usePlatformStore } from '../stores/platform'
|
||||
import type { AuditLogEntry } from '../types/devops'
|
||||
import type { IntegrationConfigState } from '../types/devops'
|
||||
|
||||
const platform = usePlatformStore()
|
||||
@@ -43,6 +45,26 @@ function formatCheckedAt(value: string): string {
|
||||
minute: '2-digit',
|
||||
}).format(date)
|
||||
}
|
||||
|
||||
function auditActor(record: AuditLogEntry): string {
|
||||
return record.actorName || record.actorId || '-'
|
||||
}
|
||||
|
||||
function auditResource(record: AuditLogEntry): string {
|
||||
return record.resourceId
|
||||
? `${record.resourceType} / ${record.resourceId}`
|
||||
: record.resourceType
|
||||
}
|
||||
|
||||
function auditDigest(record: AuditLogEntry): string {
|
||||
const payload = record.after ?? record.parameterDigest ?? record.before
|
||||
|
||||
if (!payload) {
|
||||
return '-'
|
||||
}
|
||||
|
||||
return JSON.stringify(payload)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -158,6 +180,45 @@ function formatCheckedAt(value: string): string {
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<h2>最近运维审计</h2>
|
||||
<ElButton :icon="Refresh" size="small" @click="platform.loadAuditLogs()">
|
||||
刷新
|
||||
</ElButton>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<ElTable :data="platform.auditLogs" border>
|
||||
<ElTableColumn label="时间" min-width="150">
|
||||
<template #default="{ row }">
|
||||
{{ formatCheckedAt(row.createdAt) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="action" label="动作" min-width="220" />
|
||||
<ElTableColumn label="资源" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<span class="mono">{{ auditResource(row) }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="操作者" min-width="130">
|
||||
<template #default="{ row }">
|
||||
{{ auditActor(row) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="脱敏摘要" min-width="280">
|
||||
<template #default="{ row }">
|
||||
<span class="audit-digest">{{ auditDigest(row) }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
<ElEmpty
|
||||
v-if="platform.auditLogs.length === 0"
|
||||
description="暂无审计记录"
|
||||
:image-size="72"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<h2>前端环境变量</h2>
|
||||
@@ -279,6 +340,17 @@ function formatCheckedAt(value: string): string {
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.audit-digest {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
color: #4b5565;
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', monospace;
|
||||
font-size: 12px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 1080px) {
|
||||
.variable-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
Reference in New Issue
Block a user