All checks were successful
CI / Lint, typecheck, test (push) Successful in 3m14s
CI / Build container images (push) Has been skipped
CD / Build and push images (push) Successful in 3m45s
CD / Deploy to Test (push) Successful in 10s
CD / Smoke tests against Test (push) Successful in 1m11s
CD / Promote to Int (push) Successful in 11s
CI / Auth e2e pack (push) Successful in 5m20s
CI / Import/export fidelity gate (push) Successful in 45s
New /admin/system panel (operations.md §Maintenance jobs): the maintenance job list shows every registered job with truthful last-run data (new Job.lastDurationMs recorded by the scheduler) and a manual trigger that respects the run-mutex and is itself audit-logged; a backup card mirrors the sidecar's status.json including the freshness verdict; an audit-log viewer filters by actor, action, and time range with pagination; and a storage overview lists the largest ponds. Auth events and admin actions (grants, members, user/quota admin, plugins, settings, setup) now land in a new audit_log table through a central AuditService — which keeps emitting the established stdout log line — while content activity stays log-only by design. All endpoints are Site-Admin-only; covered by API DB tests and a Playwright pack in CI. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
58 lines
1.9 KiB
TypeScript
58 lines
1.9 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import { Prisma } from '@prisma/client';
|
|
import { PinoLogger } from 'nestjs-pino';
|
|
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
|
|
export interface AuditEvent {
|
|
/** Stable dot-namespaced id, e.g. `grant.created` — the UI translates it. */
|
|
action: string;
|
|
/** The acting user; null/undefined for anonymous events. */
|
|
actorId?: string | null;
|
|
targetType?: string;
|
|
targetId?: string;
|
|
/** Small, structured context — never secrets, tokens, or page content. */
|
|
details?: Record<string, unknown>;
|
|
}
|
|
|
|
/**
|
|
* The persistent audit trail (issue #86, security.md §Logging): auth events
|
|
* and admin actions land as queryable rows for the Site-Admin viewer AND as
|
|
* the established `audit: …` stdout line. Content activity (pages, files,
|
|
* exports, labels) intentionally stays log-only — the trail answers "who
|
|
* changed access/configuration", not "who edited what".
|
|
*
|
|
* Recording must never break the audited operation: failures are logged and
|
|
* swallowed (the stdout line still fired, so nothing is silently lost).
|
|
*/
|
|
@Injectable()
|
|
export class AuditService {
|
|
constructor(
|
|
private readonly prisma: PrismaService,
|
|
private readonly logger: PinoLogger,
|
|
) {
|
|
this.logger.setContext(AuditService.name);
|
|
}
|
|
|
|
async record(event: AuditEvent): Promise<void> {
|
|
const { action, actorId, targetType, targetId, details } = event;
|
|
this.logger.info(
|
|
{ actor: actorId ?? null, targetType, targetId, ...details },
|
|
`audit: ${action}`,
|
|
);
|
|
try {
|
|
await this.prisma.auditEntry.create({
|
|
data: {
|
|
action,
|
|
actorId: actorId ?? null,
|
|
targetType,
|
|
targetId,
|
|
details: details ? (details as Prisma.InputJsonObject) : undefined,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
this.logger.error({ action, err: error }, 'audit entry could not be persisted');
|
|
}
|
|
}
|
|
}
|