feat: add audit target management

This commit is contained in:
马登山
2026-04-28 08:06:11 +08:00
parent a3476fd390
commit 30f4a434dc
13 changed files with 724 additions and 0 deletions
+235
View File
@@ -0,0 +1,235 @@
"use client"
import * as React from "react"
import { useState, useEffect, useCallback, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { RiAddLine, RiRefreshLine, RiDeleteBin5Line } from "@remixicon/react"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Page } from "@/components/page"
import { PageHeader } from "@/components/page-header"
import { SearchInput } from "@/components/search-input"
import { DataTable } from "@/components/data-table/data-table"
import { useDataTable } from "@/hooks/use-data-table"
import { useAuditTarget } from "@/hooks/use-audit-target"
import { useModuleSwitches } from "@/hooks/use-module-switches"
import { AuditTargetNewForm } from "@/components/audit-target/new-form"
import { canManageAuditTargets } from "@/lib/audit-target-access"
import { useDialog } from "@/lib/feedback/dialog"
import { useMessage } from "@/lib/feedback/message"
import type { ColumnDef } from "@tanstack/react-table"
interface RowData {
account_id: string
service: string
status: string
source?: string
}
function isConfigSource(source: string | undefined) {
return source === "config"
}
export default function AuditTargetPage() {
const { t } = useTranslation()
const message = useMessage()
const dialog = useDialog()
const { getAuditTargetList, deleteAuditTarget } = useAuditTarget()
const { getModuleSwitches } = useModuleSwitches()
const [data, setData] = useState<RowData[]>([])
const [loading, setLoading] = useState(false)
const [searchTerm, setSearchTerm] = useState("")
const [newFormOpen, setNewFormOpen] = useState(false)
const [auditEnabled, setAuditEnabled] = useState<boolean | undefined>(undefined)
const canManageTargets = canManageAuditTargets(auditEnabled)
const loadData = useCallback(async () => {
setLoading(true)
try {
const [response, switches] = await Promise.allSettled([
getAuditTargetList(),
getModuleSwitches({ suppress403Redirect: true }),
])
if (switches.status === "fulfilled" && switches.value) {
setAuditEnabled(switches.value.audit_enabled)
}
if (response.status === "rejected") {
throw response.reason
}
const list = (response.value?.audit_endpoints ?? []) as RowData[]
setData(list)
} catch {
setData([])
} finally {
setLoading(false)
}
}, [getAuditTargetList, getModuleSwitches])
useEffect(() => {
loadData()
}, [loadData])
const filteredData = useMemo(() => {
if (!searchTerm) return data
const term = searchTerm.toLowerCase()
return data.filter(
(row) =>
row.account_id?.toLowerCase().includes(term) ||
row.service?.toLowerCase().includes(term) ||
row.source?.toLowerCase().includes(term),
)
}, [data, searchTerm])
const deleteItem = useCallback(
async (row: RowData) => {
if (!canManageTargets) {
message.warning(t("Audit is disabled. Enable audit before managing audit targets."))
return
}
try {
await deleteAuditTarget(`audit_${row.service}`, row.account_id)
message.success(t("Delete Success"))
loadData()
} catch (error) {
console.error(error)
const msg = (error as Error)?.message || t("Delete Failed")
message.error(msg)
}
},
[canManageTargets, deleteAuditTarget, loadData, message, t],
)
const confirmDelete = useCallback(
(row: RowData) => {
if (!canManageTargets) return
if (!isConfigSource(row.source)) return
dialog.error({
title: t("Warning"),
content: t("Are you sure you want to delete this audit target?"),
positiveText: t("Confirm"),
negativeText: t("Cancel"),
onPositiveClick: () => deleteItem(row),
})
},
[canManageTargets, deleteItem, dialog, t],
)
const columns: ColumnDef<RowData>[] = useMemo(
() => [
{
accessorKey: "account_id",
header: () => t("Audit Targets"),
cell: ({ row }) => <span className="font-mono text-sm">{row.original.account_id}</span>,
},
{
accessorKey: "service",
header: () => t("Type"),
cell: ({ row }) => <span>{row.original.service}</span>,
},
{
accessorKey: "status",
header: () => t("Status"),
cell: ({ row }) => (
<Badge variant={row.original.status === "online" ? "secondary" : "outline"}>
{row.original.status === "online" ? t("Online") : row.original.status || "-"}
</Badge>
),
},
{
accessorKey: "source",
header: () => t("Source"),
cell: ({ row }) => {
const source = row.original.source
if (!source) return <span>-</span>
return <Badge variant={isConfigSource(source) ? "secondary" : "outline"}>{source}</Badge>
},
},
{
id: "actions",
header: () => t("Actions"),
enableSorting: false,
enableHiding: false,
meta: { width: 90 },
cell: ({ row }) =>
isConfigSource(row.original.source) ? (
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => confirmDelete(row.original)}
disabled={!canManageTargets}
>
<RiDeleteBin5Line className="size-4" aria-hidden />
<span>{t("Delete")}</span>
</Button>
</div>
) : (
<span className="text-muted-foreground">-</span>
),
},
],
[canManageTargets, confirmDelete, t],
)
const { table } = useDataTable<RowData>({
data: filteredData,
columns,
getRowId: (row) => `${row.service}-${row.account_id}`,
})
return (
<Page>
<PageHeader
actions={
<>
<div className="w-full sm:max-w-xs">
<SearchInput
value={searchTerm}
onChange={setSearchTerm}
placeholder={t("Search")}
clearable
className="w-full"
/>
</div>
<Button variant="outline" onClick={() => setNewFormOpen(true)} disabled={!canManageTargets}>
<RiAddLine className="size-4" aria-hidden />
<span>{t("Add Audit Target")}</span>
</Button>
<Button variant="outline" onClick={loadData}>
<RiRefreshLine className="size-4" aria-hidden />
<span>{t("Refresh")}</span>
</Button>
</>
}
>
<h1 className="text-2xl font-bold">{t("Audit Targets")}</h1>
</PageHeader>
{!canManageTargets ? (
<Alert>
<AlertTitle>{t("Audit is disabled")}</AlertTitle>
<AlertDescription>{t("Enable audit in Settings before managing audit targets.")}</AlertDescription>
</Alert>
) : null}
<DataTable
table={table}
isLoading={loading}
emptyTitle={t("No Audit Targets")}
emptyDescription={t("Create an audit target to forward audit logs.")}
/>
<AuditTargetNewForm open={newFormOpen} onOpenChange={setNewFormOpen} onSuccess={loadData} />
</Page>
)
}
+234
View File
@@ -0,0 +1,234 @@
"use client"
import * as React from "react"
import Image from "next/image"
import { useTranslation } from "react-i18next"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Field, FieldContent, FieldDescription, FieldLabel } from "@/components/ui/field"
import { useAuditTarget } from "@/hooks/use-audit-target"
import {
AUDIT_TARGET_CONFIG_OPTIONS,
AUDIT_TARGET_TYPE_MAPPING,
AUDIT_TARGET_TYPE_OPTIONS,
type AuditTargetType,
} from "@/lib/audit-target-config"
import { useMessage } from "@/lib/feedback/message"
import { cn } from "@/lib/utils"
import { buildRoute } from "@/lib/routes"
interface AuditTargetNewFormProps {
open: boolean
onOpenChange: (open: boolean) => void
onSuccess?: () => void
}
export function AuditTargetNewForm({ open, onOpenChange, onSuccess }: AuditTargetNewFormProps) {
const { t } = useTranslation()
const message = useMessage()
const { updateAuditTarget } = useAuditTarget()
const [type, setType] = React.useState<AuditTargetType | "">("")
const [name, setName] = React.useState("")
const [config, setConfig] = React.useState<Record<string, string | number>>({})
const [nameError, setNameError] = React.useState("")
const [submitting, setSubmitting] = React.useState(false)
const currentConfigOptions = type ? (AUDIT_TARGET_CONFIG_OPTIONS[type] ?? []) : []
const selectedOption = AUDIT_TARGET_TYPE_OPTIONS.find((o) => o.value === type)
const resetForm = React.useCallback(() => {
setType("")
setName("")
setConfig({})
setNameError("")
}, [])
React.useEffect(() => {
if (open) {
resetForm()
}
}, [open, resetForm])
React.useEffect(() => {
if (type && AUDIT_TARGET_CONFIG_OPTIONS[type]) {
const nextConfig: Record<string, string | number> = {}
AUDIT_TARGET_CONFIG_OPTIONS[type].forEach((item) => {
nextConfig[item.name] = ""
})
setConfig(nextConfig)
}
}, [type])
const validateNameFormat = (v: string) => {
if (!v) {
setNameError(t("Please enter name"))
return
}
if (!/^[A-Za-z0-9_]+$/.test(v)) {
setNameError(t("Please enter name") + " (A-Z,0-9,_)")
} else {
setNameError("")
}
}
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const v = e.target.value
setName(v)
validateNameFormat(v)
}
const validateForm = () => {
validateNameFormat(name)
if (!type) {
message.error(t("Please select audit target type"))
return false
}
if (nameError) {
message.error(nameError)
return false
}
const hasConfig = Object.values(config).some((v) => v !== "" && v !== null && v !== undefined)
if (!hasConfig) {
message.error(t("Please fill in at least one configuration item"))
return false
}
return true
}
const handleSave = async () => {
if (!validateForm()) return
setSubmitting(true)
try {
if (!type) return
const targetType = AUDIT_TARGET_TYPE_MAPPING[type]
const keyValues = Object.entries(config).map(([key, value]) => ({
key,
value: String(value ?? ""),
}))
await updateAuditTarget(targetType, name, { key_values: keyValues })
message.success(t("Audit target created successfully"))
onSuccess?.()
onOpenChange(false)
resetForm()
} catch (error) {
console.error(error)
const msg = (error as Error)?.message || t("Failed to create audit target")
message.error(msg)
} finally {
setSubmitting(false)
}
}
const handleCancel = () => {
onOpenChange(false)
resetForm()
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-h-[80vh] overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>{type ? t("Add {type} Audit Target", { type }) : t("Add Audit Target")}</DialogTitle>
</DialogHeader>
<div className="space-y-6">
{!type ? (
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
{AUDIT_TARGET_TYPE_OPTIONS.map((option) => (
<button
key={option.value}
type="button"
onClick={() => setType(option.value)}
className={cn("cursor-pointer border border-border/70 text-left transition hover:border-primary")}
>
<div className="flex items-center gap-3 p-4">
<Image
src={buildRoute(option.icon)}
alt=""
width={40}
height={40}
className="size-10 shrink-0 object-contain"
/>
<div>
<p className="text-base font-semibold">{t(option.labelKey)}</p>
<p className="text-sm text-muted-foreground">{t(option.descKey)}</p>
</div>
</div>
</button>
))}
</div>
) : (
<div className="space-y-4">
<button
type="button"
onClick={() => setType("")}
className="flex w-full cursor-pointer items-center gap-3 border p-4 text-left transition hover:border-primary"
>
{selectedOption && (
<Image
src={buildRoute(selectedOption.icon)}
alt=""
width={40}
height={40}
className="size-10 shrink-0 object-contain"
/>
)}
<div className="flex flex-col">
<span className="text-sm text-muted-foreground">{t("Selected Type")}</span>
<span className="text-base font-semibold">{type}</span>
</div>
</button>
<div className="grid gap-4">
<Field>
<FieldLabel htmlFor="audit-target-name">{t("Name")} (A-Z,0-9,_)</FieldLabel>
<FieldContent>
<Input
id="audit-target-name"
value={name}
onChange={handleNameChange}
placeholder={t("Please enter name")}
autoComplete="off"
/>
</FieldContent>
{nameError && <FieldDescription className="text-destructive">{nameError}</FieldDescription>}
</Field>
{currentConfigOptions.map((cfg) => (
<Field key={cfg.name}>
<FieldLabel htmlFor={`audit-config-${cfg.name}`}>{t(cfg.label)}</FieldLabel>
<FieldContent>
<Input
id={`audit-config-${cfg.name}`}
value={String(config[cfg.name] ?? "")}
onChange={(e) =>
setConfig((prev) => ({
...prev,
[cfg.name]: cfg.type === "number" ? Number(e.target.value) || 0 : e.target.value,
}))
}
type={cfg.type === "password" ? "password" : "text"}
autoComplete="off"
placeholder={`${t("Please enter")} ${t(cfg.label).toLowerCase()}`}
/>
</FieldContent>
</Field>
))}
</div>
</div>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={handleCancel}>
{t("Cancel")}
</Button>
<Button onClick={handleSave} disabled={!type || submitting}>
{submitting ? t("Saving...") : t("Save")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
+5
View File
@@ -80,6 +80,11 @@ export default [
to: "/events-target",
icon: "ri:bookmark-3-line",
},
{
label: "Audit Targets",
to: "/audit-target",
icon: "ri:file-shield-2-line",
},
{
label: "SSE Settings",
to: "/sse",
+39
View File
@@ -0,0 +1,39 @@
"use client"
import { useCallback } from "react"
import { useApi } from "@/contexts/api-context"
export function useAuditTarget() {
const api = useApi()
const getAuditTargetList = useCallback(async () => {
return api.get("/audit/target/list") as Promise<{
audit_endpoints?: Array<{
account_id: string
service: string
status: string
source?: string
}>
}>
}, [api])
const updateAuditTarget = useCallback(
async (targetType: string, targetName: string, targetData: Record<string, unknown>) => {
return api.put(`/audit/target/${targetType}/${targetName}`, targetData)
},
[api],
)
const deleteAuditTarget = useCallback(
async (targetType: string, targetName: string) => {
return api.delete(`/audit/target/${targetType}/${targetName}/reset`)
},
[api],
)
return {
getAuditTargetList,
updateAuditTarget,
deleteAuditTarget,
}
}
+3
View File
@@ -0,0 +1,3 @@
export function canManageAuditTargets(auditEnabled) {
return auditEnabled !== false
}
+3
View File
@@ -0,0 +1,3 @@
export function canManageAuditTargets(auditEnabled: boolean | undefined) {
return auditEnabled !== false
}
+112
View File
@@ -0,0 +1,112 @@
export const AUDIT_TARGET_CONFIG_OPTIONS = {
Kafka: [
{ label: "KAFKA_BROKERS", name: "brokers", type: "text" },
{ label: "KAFKA_TOPIC", name: "topic", type: "text" },
{ label: "KAFKA_ACKS", name: "acks", type: "text" },
{ label: "KAFKA_TLS_ENABLE", name: "tls_enable", type: "text" },
{ label: "KAFKA_TLS_CA", name: "tls_ca", type: "text" },
{ label: "KAFKA_TLS_CLIENT_CERT", name: "tls_client_cert", type: "text" },
{ label: "KAFKA_TLS_CLIENT_KEY", name: "tls_client_key", type: "text" },
{ label: "KAFKA_QUEUE_DIR", name: "queue_dir", type: "text" },
{ label: "KAFKA_QUEUE_LIMIT", name: "queue_limit", type: "number" },
{ label: "COMMENT_KEY", name: "comment", type: "text" },
],
MQTT: [
{ label: "MQTT_BROKER", name: "broker", type: "text" },
{ label: "MQTT_TOPIC", name: "topic", type: "text" },
{ label: "MQTT_QOS", name: "qos", type: "number" },
{ label: "MQTT_USERNAME", name: "username", type: "text" },
{ label: "MQTT_PASSWORD", name: "password", type: "password" },
{ label: "MQTT_RECONNECT_INTERVAL", name: "reconnect_interval", type: "number" },
{ label: "MQTT_KEEP_ALIVE_INTERVAL", name: "keep_alive_interval", type: "number" },
{ label: "MQTT_QUEUE_DIR", name: "queue_dir", type: "text" },
{ label: "MQTT_QUEUE_LIMIT", name: "queue_limit", type: "number" },
{ label: "MQTT_TLS_POLICY", name: "tls_policy", type: "text" },
{ label: "MQTT_TLS_CA", name: "tls_ca", type: "text" },
{ label: "MQTT_TLS_CLIENT_CERT", name: "tls_client_cert", type: "text" },
{ label: "MQTT_TLS_CLIENT_KEY", name: "tls_client_key", type: "text" },
{ label: "MQTT_TLS_TRUST_LEAF_AS_CA", name: "tls_trust_leaf_as_ca", type: "text" },
{ label: "MQTT_WS_PATH_ALLOWLIST", name: "ws_path_allowlist", type: "text" },
{ label: "COMMENT_KEY", name: "comment", type: "text" },
],
Webhook: [
{ label: "WEBHOOK_ENDPOINT", name: "endpoint", type: "text" },
{ label: "WEBHOOK_AUTH_TOKEN", name: "auth_token", type: "text" },
{ label: "WEBHOOK_QUEUE_LIMIT", name: "queue_limit", type: "number" },
{ label: "WEBHOOK_QUEUE_DIR", name: "queue_dir", type: "text" },
{ label: "WEBHOOK_CLIENT_CERT", name: "client_cert", type: "text" },
{ label: "WEBHOOK_CLIENT_KEY", name: "client_key", type: "text" },
{ label: "WEBHOOK_CLIENT_CA", name: "client_ca", type: "text" },
{ label: "WEBHOOK_SKIP_TLS_VERIFY", name: "skip_tls_verify", type: "text" },
{ label: "COMMENT_KEY", name: "comment", type: "text" },
],
NATS: [
{ label: "NATS_ADDRESS", name: "address", type: "text" },
{ label: "NATS_SUBJECT", name: "subject", type: "text" },
{ label: "NATS_USERNAME", name: "username", type: "text" },
{ label: "NATS_PASSWORD", name: "password", type: "password" },
{ label: "NATS_TOKEN", name: "token", type: "text" },
{ label: "NATS_CREDENTIALS_FILE", name: "credentials_file", type: "text" },
{ label: "NATS_TLS_CA", name: "tls_ca", type: "text" },
{ label: "NATS_TLS_CLIENT_CERT", name: "tls_client_cert", type: "text" },
{ label: "NATS_TLS_CLIENT_KEY", name: "tls_client_key", type: "text" },
{ label: "NATS_TLS_REQUIRED", name: "tls_required", type: "text" },
{ label: "NATS_QUEUE_DIR", name: "queue_dir", type: "text" },
{ label: "NATS_QUEUE_LIMIT", name: "queue_limit", type: "number" },
{ label: "COMMENT_KEY", name: "comment", type: "text" },
],
Pulsar: [
{ label: "PULSAR_BROKER", name: "broker", type: "text" },
{ label: "PULSAR_TOPIC", name: "topic", type: "text" },
{ label: "PULSAR_AUTH_TOKEN", name: "auth_token", type: "text" },
{ label: "PULSAR_USERNAME", name: "username", type: "text" },
{ label: "PULSAR_PASSWORD", name: "password", type: "password" },
{ label: "PULSAR_TLS_CA", name: "tls_ca", type: "text" },
{ label: "PULSAR_TLS_ALLOW_INSECURE", name: "tls_allow_insecure", type: "text" },
{ label: "PULSAR_TLS_HOSTNAME_VERIFICATION", name: "tls_hostname_verification", type: "text" },
{ label: "PULSAR_QUEUE_DIR", name: "queue_dir", type: "text" },
{ label: "PULSAR_QUEUE_LIMIT", name: "queue_limit", type: "number" },
{ label: "COMMENT_KEY", name: "comment", type: "text" },
],
}
export const AUDIT_TARGET_TYPE_OPTIONS = [
{
labelKey: "Kafka",
value: "Kafka",
icon: "/svg/kafka.svg",
descKey: "Send audit logs via Kafka",
},
{
labelKey: "MQTT",
value: "MQTT",
icon: "/svg/mqtt.svg",
descKey: "Send audit logs via MQTT broker",
},
{
labelKey: "Webhook",
value: "Webhook",
icon: "/svg/webhooks.svg",
descKey: "Send audit logs to HTTP endpoints",
},
{
labelKey: "NATS",
value: "NATS",
icon: "/svg/nats.svg",
descKey: "Send audit logs via NATS",
},
{
labelKey: "Pulsar",
value: "Pulsar",
icon: "/svg/pulsar.svg",
descKey: "Send audit logs via Pulsar",
},
]
export const AUDIT_TARGET_TYPE_MAPPING = {
Kafka: "audit_kafka",
MQTT: "audit_mqtt",
Webhook: "audit_webhook",
NATS: "audit_nats",
Pulsar: "audit_pulsar",
}
+27
View File
@@ -0,0 +1,27 @@
import {
AUDIT_TARGET_CONFIG_OPTIONS as JS_AUDIT_TARGET_CONFIG_OPTIONS,
AUDIT_TARGET_TYPE_MAPPING as JS_AUDIT_TARGET_TYPE_MAPPING,
AUDIT_TARGET_TYPE_OPTIONS as JS_AUDIT_TARGET_TYPE_OPTIONS,
} from "./audit-target-config.js"
export type AuditTargetType = "Kafka" | "MQTT" | "Webhook" | "NATS" | "Pulsar"
export type AuditTargetConfigField = {
label: string
name: string
type: "text" | "password" | "number"
}
export const AUDIT_TARGET_CONFIG_OPTIONS = JS_AUDIT_TARGET_CONFIG_OPTIONS as Record<
AuditTargetType,
AuditTargetConfigField[]
>
export const AUDIT_TARGET_TYPE_OPTIONS = JS_AUDIT_TARGET_TYPE_OPTIONS as Array<{
labelKey: string
value: AuditTargetType
icon: string
descKey: string
}>
export const AUDIT_TARGET_TYPE_MAPPING = JS_AUDIT_TARGET_TYPE_MAPPING as Record<AuditTargetType, string>
+1
View File
@@ -36,6 +36,7 @@ export const PAGE_PERMISSIONS: Record<string, ConsoleScope[]> = {
"/lifecycle": [CONSOLE_SCOPES.VIEW_BUCKET_LIFECYCLE],
"/tiers": [CONSOLE_SCOPES.VIEW_TIERED_STORAGE],
"/events-target": [CONSOLE_SCOPES.VIEW_EVENT_DESTINATIONS],
"/audit-target": [CONSOLE_SCOPES.VIEW_EVENT_DESTINATIONS],
"/sse": [CONSOLE_SCOPES.VIEW_SSE_SETTINGS],
"/oidc": [CONSOLE_SCOPES.VIEW_OIDC_SETTINGS],
"/settings": [CONSOLE_SCOPES.VIEW_SETTINGS],
+1
View File
@@ -19,6 +19,7 @@ export const MENU_CONTROLLED_DASHBOARD_ROUTES: readonly string[] = [
"/status",
"/tiers",
"/events-target",
"/audit-target",
"/sse",
"/oidc",
"/settings",
+2
View File
@@ -11,6 +11,7 @@ import {
RiBarChartBoxLine,
RiStackLine,
RiBookmark3Line,
RiFileShield2Line,
RiSecurePaymentLine,
RiSettingsLine,
RiFingerprintLine,
@@ -34,6 +35,7 @@ const iconMap: Record<string, ComponentType<{ className?: string }>> = {
"ri:bar-chart-box-line": RiBarChartBoxLine,
"ri:stack-line": RiStackLine,
"ri:bookmark-3-line": RiBookmark3Line,
"ri:file-shield-2-line": RiFileShield2Line,
"ri:secure-payment-line": RiSecurePaymentLine,
"ri:settings-line": RiSettingsLine,
"ri:fingerprint-line": RiFingerprintLine,
+10
View File
@@ -0,0 +1,10 @@
import test from "node:test"
import assert from "node:assert/strict"
import { canManageAuditTargets } from "../../lib/audit-target-access.js"
test("canManageAuditTargets blocks changes only when audit is disabled", () => {
assert.equal(canManageAuditTargets(false), false)
assert.equal(canManageAuditTargets(true), true)
assert.equal(canManageAuditTargets(undefined), true)
})
+52
View File
@@ -0,0 +1,52 @@
import assert from "node:assert/strict"
import test from "node:test"
import {
AUDIT_TARGET_CONFIG_OPTIONS,
AUDIT_TARGET_TYPE_MAPPING,
AUDIT_TARGET_TYPE_OPTIONS,
} from "../../lib/audit-target-config.js"
test("audit target config exposes fields matching backend audit contracts", () => {
assert.deepEqual(
AUDIT_TARGET_CONFIG_OPTIONS.Kafka.map((field) => field.name),
[
"brokers",
"topic",
"acks",
"tls_enable",
"tls_ca",
"tls_client_cert",
"tls_client_key",
"queue_dir",
"queue_limit",
"comment",
],
)
assert.deepEqual(
AUDIT_TARGET_CONFIG_OPTIONS.Webhook.map((field) => field.name),
[
"endpoint",
"auth_token",
"queue_limit",
"queue_dir",
"client_cert",
"client_key",
"client_ca",
"skip_tls_verify",
"comment",
],
)
})
test("audit target type metadata maps console types to audit subsystems", () => {
const kafkaOption = AUDIT_TARGET_TYPE_OPTIONS.find((option) => option.value === "Kafka")
assert.ok(kafkaOption)
assert.equal(kafkaOption.icon, "/svg/kafka.svg")
assert.equal(AUDIT_TARGET_TYPE_MAPPING.Kafka, "audit_kafka")
assert.equal(AUDIT_TARGET_TYPE_MAPPING.MQTT, "audit_mqtt")
assert.equal(AUDIT_TARGET_TYPE_MAPPING.Webhook, "audit_webhook")
assert.equal(AUDIT_TARGET_TYPE_MAPPING.NATS, "audit_nats")
assert.equal(AUDIT_TARGET_TYPE_MAPPING.Pulsar, "audit_pulsar")
})