This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
||||
// The audit viewer (spec §9.4). It reads the platform's audit log for this deployment with the
|
||||
// deployment token, which carries `audit:read`, and shows only the events of this organization:
|
||||
// every write goes through one function that puts the organization's slug in the event's target,
|
||||
// and `isOwnedBy` is that rule read backwards (src/server/audit.ts).
|
||||
//
|
||||
// Administrators only, which the layout beside this page asks of the tenancy resolver
|
||||
// (src/server/guard.ts) before the response starts.
|
||||
|
||||
import { orgGet } from "@conusai-kit/tenancy"
|
||||
import { ConusAIClient } from "@conusai-sdk/node-sdk"
|
||||
import type { ReactElement } from "react"
|
||||
import { webRuntime } from "../../../src/server/app"
|
||||
import { isOwnedBy } from "../../../src/server/audit"
|
||||
import { pageContext } from "../../../src/server/page"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
interface AuditRow {
|
||||
id: number
|
||||
action: string
|
||||
outcome: string
|
||||
occurred_at: string
|
||||
resource_type?: string | null
|
||||
resource_id?: string | null
|
||||
}
|
||||
|
||||
/** How many of this organization's events the viewer shows, and how far back it scans for them. */
|
||||
const PAGE_LIMIT = 200
|
||||
const MAX_PAGES = 25
|
||||
|
||||
/**
|
||||
* The platform's log is deployment-wide and pages newest-first with no server-side filter, so the
|
||||
* organization's rows are collected by walking pages until the viewer's own limit is filled or the
|
||||
* log ends -- a filter over the first page alone would show nothing once 200 newer events of other
|
||||
* organizations exist.
|
||||
*/
|
||||
async function organizationEvents(org: { slug: string; id: string }): Promise<AuditRow[]> {
|
||||
const { env } = webRuntime()
|
||||
const platform = new ConusAIClient({
|
||||
baseUrl: env.deployment.CONUSAI_API_URL,
|
||||
apiKey: env.deployment.CONUSAI_API_TOKEN,
|
||||
})
|
||||
const rows: AuditRow[] = []
|
||||
let cursor: string | null | undefined
|
||||
for (let page = 0; page < MAX_PAGES && rows.length < PAGE_LIMIT; page += 1) {
|
||||
const { data } = await platform.readEvents({
|
||||
query: cursor ? { limit: PAGE_LIMIT, cursor } : { limit: PAGE_LIMIT },
|
||||
throwOnError: true,
|
||||
})
|
||||
for (const row of (data?.items ?? []) as AuditRow[]) {
|
||||
if (isOwnedBy(org, row.resource_id)) rows.push(row)
|
||||
}
|
||||
if (!data?.has_more || !data.next_cursor) break
|
||||
cursor = data.next_cursor
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
export default async function Audit({ params }: { params: Promise<{ org: string }> }): Promise<ReactElement> {
|
||||
const { org } = await params
|
||||
const { caller, i18n } = await pageContext()
|
||||
|
||||
// The layout already established membership; the id is what subscriber-written events carry.
|
||||
const organization = await caller.call(orgGet, { org })
|
||||
const { observe } = webRuntime()
|
||||
const rows = await organizationEvents({ slug: org, id: organization.orgId }).catch((error: unknown) => {
|
||||
// The platform being unreachable is not the viewer's failure to explain to a customer; it is
|
||||
// ours to know about, so it is logged, and the page says there is nothing to show.
|
||||
observe.log.warn(
|
||||
`audit viewer: the platform's log could not be read: ${error instanceof Error ? error.name : "failed"}`,
|
||||
)
|
||||
return []
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>{i18n.message("app.audit")}</h1>
|
||||
{rows.length === 0 ? (
|
||||
<p>{i18n.message("app.auditEmpty")}</p>
|
||||
) : (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{i18n.message("app.when")}</th>
|
||||
<th scope="col">{i18n.message("app.action")}</th>
|
||||
<th scope="col">{i18n.message("app.outcome")}</th>
|
||||
<th scope="col">{i18n.message("app.resource")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((row) => (
|
||||
<tr key={row.id}>
|
||||
<td>{i18n.date(new Date(row.occurred_at))}</td>
|
||||
<td>{row.action}</td>
|
||||
<td>{row.outcome}</td>
|
||||
<td>{row.resource_type ?? ""}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user