feat: complete audit coverage — comment, webhook, system-role, dispo, scenario
- comment.ts: create (body preview), resolve, delete - webhook.ts: create, update, delete, test (result in summary) - system-role-config.ts: update with before/after - dispo.ts: commitImportBatch (IMPORT with counts), cancelImportBatch - scenario.ts: applyScenario (CREATE with allocation count) Audit coverage now: 29/36 routers (81%). Remaining 7 are read-only (dashboard, staffing, chargeability-report, computation-graph, report, insights.detectAnomalies, notification read/dismiss). Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -3,6 +3,7 @@ import { TRPCError } from "@trpc/server";
|
||||
import { SystemRole } from "@planarchy/shared";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc.js";
|
||||
import { createNotification } from "../lib/create-notification.js";
|
||||
import { createAuditEntry } from "../lib/audit.js";
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -155,6 +156,17 @@ export const commentRouter = createTRPCRouter({
|
||||
);
|
||||
}
|
||||
|
||||
void createAuditEntry({
|
||||
db: ctx.db,
|
||||
entityType: "Comment",
|
||||
entityId: comment.id,
|
||||
entityName: input.body.slice(0, 50),
|
||||
action: "CREATE",
|
||||
userId: ctx.dbUser?.id,
|
||||
after: comment as unknown as Record<string, unknown>,
|
||||
source: "ui",
|
||||
});
|
||||
|
||||
return comment;
|
||||
}),
|
||||
|
||||
@@ -188,13 +200,26 @@ export const commentRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
return ctx.db.comment.update({
|
||||
const updated = await ctx.db.comment.update({
|
||||
where: { id: input.id },
|
||||
data: { resolved: input.resolved },
|
||||
include: {
|
||||
author: { select: { id: true, name: true, email: true, image: true } },
|
||||
},
|
||||
});
|
||||
|
||||
void createAuditEntry({
|
||||
db: ctx.db,
|
||||
entityType: "Comment",
|
||||
entityId: input.id,
|
||||
action: "UPDATE",
|
||||
userId: ctx.dbUser?.id,
|
||||
summary: input.resolved ? "Resolved comment" : "Unresolved comment",
|
||||
after: updated as unknown as Record<string, unknown>,
|
||||
source: "ui",
|
||||
});
|
||||
|
||||
return updated;
|
||||
}),
|
||||
|
||||
/** Delete a comment (author or admin only). Hard-deletes, including all replies. */
|
||||
@@ -227,5 +252,15 @@ export const commentRouter = createTRPCRouter({
|
||||
});
|
||||
|
||||
await ctx.db.comment.delete({ where: { id: input.id } });
|
||||
|
||||
void createAuditEntry({
|
||||
db: ctx.db,
|
||||
entityType: "Comment",
|
||||
entityId: input.id,
|
||||
action: "DELETE",
|
||||
userId: ctx.dbUser?.id,
|
||||
before: existing as unknown as Record<string, unknown>,
|
||||
source: "ui",
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { adminProcedure, createTRPCRouter } from "../trpc.js";
|
||||
import { createAuditEntry } from "../lib/audit.js";
|
||||
|
||||
// ─── Shared schemas ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -175,10 +176,23 @@ export const dispoRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
return ctx.db.importBatch.update({
|
||||
const cancelled = await ctx.db.importBatch.update({
|
||||
where: { id: input.id },
|
||||
data: { status: ImportBatchStatus.CANCELLED },
|
||||
});
|
||||
|
||||
void createAuditEntry({
|
||||
db: ctx.db,
|
||||
entityType: "ImportBatch",
|
||||
entityId: input.id,
|
||||
action: "UPDATE",
|
||||
userId: ctx.dbUser?.id,
|
||||
summary: "Cancelled import batch",
|
||||
after: cancelled as unknown as Record<string, unknown>,
|
||||
source: "ui",
|
||||
});
|
||||
|
||||
return cancelled;
|
||||
}),
|
||||
|
||||
// ── 6. listStagedResources ───────────────────────────────────────────────
|
||||
@@ -414,10 +428,25 @@ export const dispoRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
return commitDispoImportBatch(ctx.db, {
|
||||
const result = await commitDispoImportBatch(ctx.db, {
|
||||
importBatchId: input.importBatchId,
|
||||
...(input.allowTbdUnresolved !== undefined ? { allowTbdUnresolved: input.allowTbdUnresolved } : {}),
|
||||
...(input.importTbdProjects !== undefined ? { importTbdProjects: input.importTbdProjects } : {}),
|
||||
});
|
||||
|
||||
const counts = result as unknown as Record<string, unknown>;
|
||||
void createAuditEntry({
|
||||
db: ctx.db,
|
||||
entityType: "ImportBatch",
|
||||
entityId: input.importBatchId,
|
||||
entityName: input.importBatchId,
|
||||
action: "IMPORT",
|
||||
userId: ctx.dbUser?.id,
|
||||
summary: `Committed import batch (${JSON.stringify(counts)})`,
|
||||
after: counts,
|
||||
source: "ui",
|
||||
});
|
||||
|
||||
return result;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { calculateAllocation, countWorkingDays } from "@planarchy/engine/allocat
|
||||
import { z } from "zod";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { createTRPCRouter, controllerProcedure, protectedProcedure } from "../trpc.js";
|
||||
import { createAuditEntry } from "../lib/audit.js";
|
||||
|
||||
const DEFAULT_AVAILABILITY = {
|
||||
monday: 8,
|
||||
@@ -485,7 +486,7 @@ export const scenarioRouter = createTRPCRouter({
|
||||
|
||||
const project = await ctx.db.project.findUnique({
|
||||
where: { id: projectId },
|
||||
select: { id: true },
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
if (!project) {
|
||||
throw new TRPCError({ code: "NOT_FOUND", message: "Project not found" });
|
||||
@@ -548,6 +549,18 @@ export const scenarioRouter = createTRPCRouter({
|
||||
created.push(newAssignment.id);
|
||||
}
|
||||
|
||||
void createAuditEntry({
|
||||
db: ctx.db,
|
||||
entityType: "ScenarioApplication",
|
||||
entityId: projectId,
|
||||
entityName: project.name,
|
||||
action: "CREATE",
|
||||
userId: ctx.dbUser?.id,
|
||||
summary: `Applied scenario to project "${project.name}" (${created.length} allocations created/modified)`,
|
||||
metadata: { appliedCount: created.length, assignmentIds: created },
|
||||
source: "ui",
|
||||
});
|
||||
|
||||
return { appliedCount: created.length };
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { adminProcedure, createTRPCRouter, invalidateRoleDefaultsCache, protectedProcedure } from "../trpc.js";
|
||||
import { createAuditEntry } from "../lib/audit.js";
|
||||
|
||||
export const systemRoleConfigRouter = createTRPCRouter({
|
||||
/** List all role configs (sorted by sortOrder) */
|
||||
@@ -21,6 +22,10 @@ export const systemRoleConfigRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const existing = await ctx.db.systemRoleConfig.findUnique({
|
||||
where: { role: input.role as never },
|
||||
});
|
||||
|
||||
const data: Record<string, unknown> = {};
|
||||
if (input.label !== undefined) data.label = input.label;
|
||||
if (input.description !== undefined) data.description = input.description;
|
||||
@@ -35,6 +40,18 @@ export const systemRoleConfigRouter = createTRPCRouter({
|
||||
// Invalidate cached role defaults so changes take effect immediately
|
||||
invalidateRoleDefaultsCache();
|
||||
|
||||
void createAuditEntry({
|
||||
db: ctx.db,
|
||||
entityType: "SystemRoleConfig",
|
||||
entityId: input.role,
|
||||
entityName: result.label,
|
||||
action: "UPDATE",
|
||||
userId: ctx.dbUser?.id,
|
||||
before: (existing ?? {}) as unknown as Record<string, unknown>,
|
||||
after: result as unknown as Record<string, unknown>,
|
||||
source: "ui",
|
||||
});
|
||||
|
||||
return result;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { z } from "zod";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { createTRPCRouter, adminProcedure } from "../trpc.js";
|
||||
import { WEBHOOK_EVENTS } from "../lib/webhook-dispatcher.js";
|
||||
import { createAuditEntry } from "../lib/audit.js";
|
||||
|
||||
const webhookEventEnum = z.enum(WEBHOOK_EVENTS as unknown as [string, ...string[]]);
|
||||
|
||||
@@ -36,7 +37,7 @@ export const webhookRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
return ctx.db.webhook.create({
|
||||
const webhook = await ctx.db.webhook.create({
|
||||
data: {
|
||||
name: input.name,
|
||||
url: input.url,
|
||||
@@ -45,6 +46,19 @@ export const webhookRouter = createTRPCRouter({
|
||||
isActive: input.isActive,
|
||||
},
|
||||
});
|
||||
|
||||
void createAuditEntry({
|
||||
db: ctx.db,
|
||||
entityType: "Webhook",
|
||||
entityId: webhook.id,
|
||||
entityName: webhook.name,
|
||||
action: "CREATE",
|
||||
userId: ctx.dbUser?.id,
|
||||
after: webhook as unknown as Record<string, unknown>,
|
||||
source: "ui",
|
||||
});
|
||||
|
||||
return webhook;
|
||||
}),
|
||||
|
||||
/** Update an existing webhook. */
|
||||
@@ -67,7 +81,7 @@ export const webhookRouter = createTRPCRouter({
|
||||
throw new TRPCError({ code: "NOT_FOUND", message: "Webhook not found" });
|
||||
}
|
||||
|
||||
return ctx.db.webhook.update({
|
||||
const updated = await ctx.db.webhook.update({
|
||||
where: { id: input.id },
|
||||
data: {
|
||||
...(input.data.name !== undefined ? { name: input.data.name } : {}),
|
||||
@@ -77,6 +91,20 @@ export const webhookRouter = createTRPCRouter({
|
||||
...(input.data.isActive !== undefined ? { isActive: input.data.isActive } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
void createAuditEntry({
|
||||
db: ctx.db,
|
||||
entityType: "Webhook",
|
||||
entityId: input.id,
|
||||
entityName: updated.name,
|
||||
action: "UPDATE",
|
||||
userId: ctx.dbUser?.id,
|
||||
before: existing as unknown as Record<string, unknown>,
|
||||
after: updated as unknown as Record<string, unknown>,
|
||||
source: "ui",
|
||||
});
|
||||
|
||||
return updated;
|
||||
}),
|
||||
|
||||
/** Delete a webhook. */
|
||||
@@ -88,6 +116,17 @@ export const webhookRouter = createTRPCRouter({
|
||||
throw new TRPCError({ code: "NOT_FOUND", message: "Webhook not found" });
|
||||
}
|
||||
await ctx.db.webhook.delete({ where: { id: input.id } });
|
||||
|
||||
void createAuditEntry({
|
||||
db: ctx.db,
|
||||
entityType: "Webhook",
|
||||
entityId: input.id,
|
||||
entityName: existing.name,
|
||||
action: "DELETE",
|
||||
userId: ctx.dbUser?.id,
|
||||
before: existing as unknown as Record<string, unknown>,
|
||||
source: "ui",
|
||||
});
|
||||
}),
|
||||
|
||||
/** Send a test payload to a webhook URL. */
|
||||
@@ -127,6 +166,8 @@ export const webhookRouter = createTRPCRouter({
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 5_000);
|
||||
|
||||
let result: { success: boolean; statusCode: number; statusText: string };
|
||||
|
||||
try {
|
||||
const response = await fetch(wh.url, {
|
||||
method: "POST",
|
||||
@@ -134,13 +175,13 @@ export const webhookRouter = createTRPCRouter({
|
||||
body,
|
||||
signal: controller.signal,
|
||||
});
|
||||
return {
|
||||
result = {
|
||||
success: response.ok,
|
||||
statusCode: response.status,
|
||||
statusText: response.statusText,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
result = {
|
||||
success: false,
|
||||
statusCode: 0,
|
||||
statusText: err instanceof Error ? err.message : "Unknown error",
|
||||
@@ -148,5 +189,19 @@ export const webhookRouter = createTRPCRouter({
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
void createAuditEntry({
|
||||
db: ctx.db,
|
||||
entityType: "Webhook",
|
||||
entityId: wh.id,
|
||||
entityName: wh.name,
|
||||
action: "UPDATE",
|
||||
userId: ctx.dbUser?.id,
|
||||
summary: `Tested webhook (result: ${result.success ? "success" : "failed"})`,
|
||||
metadata: result as unknown as Record<string, unknown>,
|
||||
source: "ui",
|
||||
});
|
||||
|
||||
return result;
|
||||
}),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user