feat(audit): add localized security audit logs (#5462)

This commit is contained in:
Calcium-Ion
2026-06-12 23:40:40 +08:00
committed by GitHub
parent 27b2b2c4b9
commit d0c4305a16
25 changed files with 1249 additions and 33 deletions
@@ -46,6 +46,7 @@ import {
hasAnyCacheTokens,
parseLogOther,
isViolationFeeLog,
renderAuditContent,
} from '../../lib/format'
import {
isDisplayableLogType,
@@ -100,6 +101,13 @@ function buildDetailSegments(
other: LogOtherData | null,
t: (key: string, opts?: Record<string, unknown>) => string
): DetailSegment[] {
// Audit (type=3) and login (type=7) logs: render localized content from the
// structured op descriptor instead of the raw (English-fallback) content.
if (log.type === 3 || log.type === 7) {
const text = renderAuditContent(other, t)
return text ? [{ text }] : []
}
if (log.type === 6) {
return [{ text: t('Async task refund') }]
}
@@ -29,6 +29,7 @@ import {
ShieldCheck,
UserCog,
Info,
LogIn,
} from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { formatBillingCurrencyFromUSD } from '@/lib/currency'
@@ -52,6 +53,7 @@ import {
isViolationFeeLog,
getFirstResponseTimeColor,
getResponseTimeColor,
renderAuditContent,
} from '../../lib/format'
import {
getLogTypeConfig,
@@ -60,6 +62,17 @@ import {
} from '../../lib/utils'
import type { LogOtherData } from '../../types'
// Maps a channel-update changed-field token (as recorded by the backend audit)
// to its i18n label key for display in the audit details.
const CHANNEL_FIELD_LABELS: Record<string, string> = {
status: 'Status',
models: 'Models',
group: 'Group',
type: 'Type',
base_url: 'Base URL',
key: 'Key',
}
function timingTextColorClass(
variant: 'success' | 'warning' | 'danger'
): string {
@@ -461,6 +474,41 @@ export function DetailsDialog(props: DetailsDialogProps) {
return `ID: ${id}`
})()
// Localized operation text rendered from the language-independent op
// descriptor (shared by audit type=3 and login type=7).
const operationText = renderAuditContent(other, t)
const auditRoute = isManage && props.isAdmin ? other?.audit_info : undefined
// Channel update records which fields changed (stable field tokens); render
// them with their localized labels for admins.
const changedFieldTokens =
isManage && props.isAdmin && Array.isArray(other?.op?.params?.changed_fields)
? (other.op.params.changed_fields as string[])
: []
const changedFieldsText = changedFieldTokens
.map((field) => t(CHANNEL_FIELD_LABELS[field] ?? field))
.join(', ')
const showManageAuditSection =
isManage && props.isAdmin && (operationText != null || auditRoute != null)
// Login audit (type=7); visible to the log owner, not admin-only.
const isLogin = props.log.type === 7
const loginAuditFields = isLogin
? ([
other?.login_method && {
label: t('Login Method'),
value: String(other.login_method),
},
props.log.ip && {
label: t('IP Address'),
value: props.log.ip,
},
other?.user_agent && {
label: t('User Agent'),
value: String(other.user_agent),
},
].filter(Boolean) as Array<{ label: string; value: string }>)
: []
const conversionChain =
other && Array.isArray(other.request_conversion)
? other.request_conversion.filter(Boolean)
@@ -749,6 +797,62 @@ export function DetailsDialog(props: DetailsDialogProps) {
/>
)}
{/* Operation audit info (type=3, admin only) */}
{showManageAuditSection && (
<DetailSection
icon={<ShieldCheck className='size-3.5' aria-hidden='true' />}
label={t('Operation Audit Info')}
>
{operationText != null && (
<DetailRow label={t('Operation')} value={operationText} />
)}
{changedFieldsText !== '' && (
<DetailRow
label={t('Changed Fields')}
value={changedFieldsText}
/>
)}
{auditRoute?.method && auditRoute?.route && (
<DetailRow
label={t('Request')}
value={`${auditRoute.method} ${auditRoute.route}`}
mono
/>
)}
{auditRoute?.status != null && (
<DetailRow
label={t('Result')}
value={
auditRoute.success
? `${t('Success')} (${auditRoute.status})`
: `${t('Failed')} (${auditRoute.status})`
}
mono
/>
)}
</DetailSection>
)}
{/* Login audit info (type=7) */}
{isLogin && loginAuditFields.length > 0 && (
<DetailSection
icon={<LogIn className='size-3.5' aria-hidden='true' />}
label={t('Login Info')}
>
{operationText != null && (
<DetailRow label={t('Operation')} value={operationText} />
)}
{loginAuditFields.map((field, idx) => (
<DetailRow
key={idx}
label={field.label}
value={field.value}
mono
/>
))}
</DetailSection>
)}
{/* Audio/WebSocket token breakdown */}
{hasAudioTokens && other && (
<DetailSection