feat: 展示真实运维审计日志
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
import type {
|
import type {
|
||||||
AgentCard,
|
AgentCard,
|
||||||
AgentInvocation,
|
AgentInvocation,
|
||||||
|
AuditLogEntry,
|
||||||
CreateAgentInvocationPayload,
|
CreateAgentInvocationPayload,
|
||||||
CreateDeployRunPayload,
|
CreateDeployRunPayload,
|
||||||
DashboardSummary,
|
DashboardSummary,
|
||||||
@@ -267,6 +268,12 @@ export const devopsApi = {
|
|||||||
return toJenkinsSyncSummary(data, projectList)
|
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[]> {
|
async getAgentCards(): Promise<AgentCard[]> {
|
||||||
return agentCards
|
return agentCards
|
||||||
},
|
},
|
||||||
@@ -416,6 +423,22 @@ function sanitizeIntegrationNote(note?: string): string | undefined {
|
|||||||
.replace(/key=[^&\s]+/g, 'key=[已隐藏]')
|
.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[] {
|
function toProjectConfigs(backendProjects: BackendProject[]): ProjectConfig[] {
|
||||||
return backendProjects.map((project) => {
|
return backendProjects.map((project) => {
|
||||||
const repo = parseRepository(project.repositoryUrl, project.key)
|
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 {
|
function toPipelineStep(step: BackendDeployRunStep): PipelineStep {
|
||||||
const bpmnNodeId = step.bpmnNodeId || step.id || 'unknown-step'
|
const bpmnNodeId = step.bpmnNodeId || step.id || 'unknown-step'
|
||||||
const name = step.name || bpmnNodeId
|
const name = step.name || bpmnNodeId
|
||||||
|
|||||||
+16
-1
@@ -3,6 +3,7 @@ import { defineStore } from 'pinia'
|
|||||||
import { devopsApi } from '../services/devopsApi'
|
import { devopsApi } from '../services/devopsApi'
|
||||||
import type {
|
import type {
|
||||||
AgentCard,
|
AgentCard,
|
||||||
|
AuditLogEntry,
|
||||||
CreateDeployRunPayload,
|
CreateDeployRunPayload,
|
||||||
IntegrationStatus,
|
IntegrationStatus,
|
||||||
IntegrationConfigStatus,
|
IntegrationConfigStatus,
|
||||||
@@ -24,6 +25,7 @@ export const usePlatformStore = defineStore('platform', () => {
|
|||||||
const integrations = ref<IntegrationStatus[]>([])
|
const integrations = ref<IntegrationStatus[]>([])
|
||||||
const agentCards = ref<AgentCard[]>([])
|
const agentCards = ref<AgentCard[]>([])
|
||||||
const settings = ref<SystemSetting[]>([])
|
const settings = ref<SystemSetting[]>([])
|
||||||
|
const auditLogs = ref<AuditLogEntry[]>([])
|
||||||
const integrationConfig = ref<IntegrationConfigStatus>({
|
const integrationConfig = ref<IntegrationConfigStatus>({
|
||||||
checkedAt: '',
|
checkedAt: '',
|
||||||
integrations: [],
|
integrations: [],
|
||||||
@@ -46,7 +48,12 @@ export const usePlatformStore = defineStore('platform', () => {
|
|||||||
agentCards.value = summary.agentCards
|
agentCards.value = summary.agentCards
|
||||||
settings.value = summary.settings
|
settings.value = summary.settings
|
||||||
integrationConfig.value = summary.integrationConfig
|
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()
|
dataSource.value = devopsApi.getLastDataSource()
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
@@ -98,6 +105,12 @@ export const usePlatformStore = defineStore('platform', () => {
|
|||||||
return refs
|
return refs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadAuditLogs() {
|
||||||
|
auditLogs.value = await devopsApi.getAuditLogs()
|
||||||
|
dataSource.value = devopsApi.getLastDataSource()
|
||||||
|
return auditLogs.value
|
||||||
|
}
|
||||||
|
|
||||||
function upsertRun(run: ReleaseRun) {
|
function upsertRun(run: ReleaseRun) {
|
||||||
runs.value = [run, ...runs.value.filter((item) => item.id !== run.id)]
|
runs.value = [run, ...runs.value.filter((item) => item.id !== run.id)]
|
||||||
}
|
}
|
||||||
@@ -110,6 +123,7 @@ export const usePlatformStore = defineStore('platform', () => {
|
|||||||
integrations,
|
integrations,
|
||||||
agentCards,
|
agentCards,
|
||||||
settings,
|
settings,
|
||||||
|
auditLogs,
|
||||||
integrationConfig,
|
integrationConfig,
|
||||||
releaseProcessXml,
|
releaseProcessXml,
|
||||||
projectRefs,
|
projectRefs,
|
||||||
@@ -122,5 +136,6 @@ export const usePlatformStore = defineStore('platform', () => {
|
|||||||
syncJenkinsRun,
|
syncJenkinsRun,
|
||||||
syncJenkinsRuns,
|
syncJenkinsRuns,
|
||||||
loadProjectRefs,
|
loadProjectRefs,
|
||||||
|
loadAuditLogs,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -165,6 +165,22 @@ export interface JenkinsSyncSummary {
|
|||||||
failedRuns: JenkinsSyncError[]
|
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 {
|
export interface AgentCard {
|
||||||
mode: AgentMode
|
mode: AgentMode
|
||||||
title: string
|
title: string
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Refresh } from '@element-plus/icons-vue'
|
||||||
import StatusPill from '../components/StatusPill.vue'
|
import StatusPill from '../components/StatusPill.vue'
|
||||||
import { usePlatformStore } from '../stores/platform'
|
import { usePlatformStore } from '../stores/platform'
|
||||||
|
import type { AuditLogEntry } from '../types/devops'
|
||||||
import type { IntegrationConfigState } from '../types/devops'
|
import type { IntegrationConfigState } from '../types/devops'
|
||||||
|
|
||||||
const platform = usePlatformStore()
|
const platform = usePlatformStore()
|
||||||
@@ -43,6 +45,26 @@ function formatCheckedAt(value: string): string {
|
|||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
}).format(date)
|
}).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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -158,6 +180,45 @@ function formatCheckedAt(value: string): string {
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</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">
|
<section class="panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<h2>前端环境变量</h2>
|
<h2>前端环境变量</h2>
|
||||||
@@ -279,6 +340,17 @@ function formatCheckedAt(value: string): string {
|
|||||||
line-height: 24px;
|
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) {
|
@media (max-width: 1080px) {
|
||||||
.variable-grid {
|
.variable-grid {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* 前端开发服务器配置;本地 /api 代理到真实 DevOps API,避免引入 mock 数据。
|
||||||
|
*/
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
@@ -6,5 +9,12 @@ export default defineConfig({
|
|||||||
plugins: [vue()],
|
plugins: [vue()],
|
||||||
server: {
|
server: {
|
||||||
port: 4301,
|
port: 4301,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: process.env.DEVOPS_API_PROXY_TARGET ?? 'https://devops.mrzhan.top',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user