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:
1 parent
6e7f639013
commit
c98d27e70c
3 files changed
+32
-27
No files matched your search
@@ -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(),
|
||||
|
||||
@@ -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
@@ -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;
|
||||
};
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user