api: Audit-Log verdrahtet (auditedProcedure an kritische Mutationen)

withAudit war als tRPC-Wrapper definiert, aber an keine Procedure gehaengt
(audit_event blieb leer). Jetzt eine echte tRPC-v11-Middleware-Procedure
(auditedProcedure), die nach erfolgreicher Mutation best-effort einen
audit_event schreibt (Fehler beim Audit kippen die Mutation nicht).

Verdrahtet an die folgenreichen Zustandsuebergaenge:
- blocker.resolve
- project.archive, project.setPhase, project.addPhaseGate

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Raffael SheikhandClaude Opus 4.8 committed 2026-06-07 12:06:13 +02:00
1 parent 6e7f639013
commit c98d27e70c
3 files changed
+32 -27

No files matched your search

+2 -2
View File
@@ -2,7 +2,7 @@ import { z } from 'zod';
import { TRPCError } from '@trpc/server';
import { eq, isNull, desc } from 'drizzle-orm';
import { blocker, project, user } from '@cockpit/db';
import { router, protectedProcedure } from '../trpc.js';
import { router, protectedProcedure, auditedProcedure } from '../trpc.js';
export const blockerRouter = router({
listOpen: protectedProcedure.query(async ({ ctx }) => {
@@ -52,7 +52,7 @@ export const blockerRouter = router({
return created;
}),
resolve: protectedProcedure
resolve: auditedProcedure('blocker.resolve')
.input(
z.object({
blockerId: z.string().uuid(),
+4 -4
View File
@@ -11,7 +11,7 @@ import {
type Project,
} from '@cockpit/db';
import { ProjectSchemas } from '@cockpit/shared';
import { router, protectedProcedure } from '../trpc.js';
import { router, protectedProcedure, auditedProcedure } from '../trpc.js';
const PHASE_ORDER = ['definition', 'planung', 'umsetzung', 'abschluss'] as const;
function nextPhase(p: (typeof PHASE_ORDER)[number]): (typeof PHASE_ORDER)[number] {
@@ -116,7 +116,7 @@ export const projectRouter = router({
return result;
}),
archive: protectedProcedure
archive: auditedProcedure('project.archive')
.input(ProjectSchemas.archiveProjectInput)
.mutation(async ({ ctx, input }) => {
const p = (
@@ -145,7 +145,7 @@ export const projectRouter = router({
}),
// Phase direkt setzen (Leitfaden Kap. 2).
setPhase: protectedProcedure
setPhase: auditedProcedure('project.setPhase')
.input(ProjectSchemas.setPhaseInput)
.mutation(async ({ ctx, input }) => {
const [updated] = await ctx.db
@@ -158,7 +158,7 @@ export const projectRouter = router({
}),
// Gate-Review am Phasenende (Leitfaden Kap. 5), optional Folgephase freigeben.
addPhaseGate: protectedProcedure
addPhaseGate: auditedProcedure('project.addPhaseGate')
.input(ProjectSchemas.addPhaseGateInput)
.mutation(async ({ ctx, input }) => {
const [gate] = await ctx.db
+26 -21
View File
@@ -3,8 +3,9 @@
import './types.js';
import { initTRPC, TRPCError } from '@trpc/server';
import type { FastifyRequest, FastifyReply } from 'fastify';
import type { User } from '@cockpit/db';
import { auditEvent, type User } from '@cockpit/db';
import { getDb } from './db.js';
import { logger } from './lib/logger.js';
export interface Context {
user: User | null;
@@ -45,25 +46,29 @@ export const protectedProcedure = t.procedure.use(async ({ ctx, next }) => {
return next({ ctx: { ...ctx, user: ctx.user } });
});
// Audit-Wrapper: schreibt nach erfolgreicher Mutation einen audit_event.
// Genutzt als zusätzliche Middleware auf Mutations.
export function withAudit<TInput, TResult extends Record<string, unknown> | unknown>(
action: string,
) {
return async function audit(opts: {
ctx: Context;
input: TInput;
next: () => Promise<{ ok: true; data: TResult } | TResult>;
}) {
const result = await opts.next();
if (!opts.ctx.user) return result;
const { auditEvent } = await import('@cockpit/db');
await opts.ctx.db.insert(auditEvent).values({
actorUserId: opts.ctx.user.id,
action,
entityKind: action.split('.')[0] ?? 'unknown',
payload: { input: opts.input as unknown },
});
/**
* Auditierte Procedure: wie `protectedProcedure`, schreibt aber nach erfolgreicher
* Ausführung einen `audit_event`. Nutzung:
* auditedProcedure('blocker.resolve').input(schema).mutation(...)
* Das Audit ist best-effort — ein Fehler beim Schreiben darf die eigentliche
* Mutation nicht scheitern lassen.
*/
export function auditedProcedure(action: string) {
return protectedProcedure.use(async ({ ctx, next, getRawInput }) => {
const result = await next();
if (result.ok) {
try {
const rawInput = await getRawInput();
await ctx.db.insert(auditEvent).values({
actorUserId: ctx.user.id,
action,
entityKind: action.split('.')[0] ?? 'unknown',
payload: { input: (rawInput ?? null) as unknown },
});
} catch (err) {
logger.warn({ err, action }, 'audit_event konnte nicht geschrieben werden');
}
}
return result;
};
});
}