feat(audit): extend AuditAction enum + audit logging on alerts + expense dedup

- AuditAction gains password_change, portal_invite/activate/reset
  variants, send, view. AuditLogParams.ipAddress/userAgent now optional
  so background jobs and internal helpers can log without faking values.
- alerts.service.dismissAlert/acknowledgeAlert now write
  action='update' rows with metadata.kind so the audit log differentiates
  the two state changes.
- expense-dedup.service.clearDuplicate/mergeDuplicate accept userId
  and write action='update'/'merge' rows respectively. Routes pass
  ctx.userId.

Audit gaps surfaced by audit-pass-#2: 6 services bypassed audit_logs
entirely. This commit closes 2 of them; portal-auth lands in a later
commit alongside the email-template-override work that already touches
the same file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Ciaccio
2026-05-06 14:57:24 +02:00
parent 1fb3aa3aeb
commit 1b78eadd36
5 changed files with 62 additions and 10 deletions

View File

@@ -9,7 +9,7 @@ export const POST = withAuth(
try {
const id = params.id;
if (!id) throw new ValidationError('id is required');
await clearDuplicate(id, ctx.portId);
await clearDuplicate(id, ctx.portId, ctx.userId);
return NextResponse.json({ ok: true });
} catch (error) {
return errorResponse(error);

View File

@@ -17,7 +17,7 @@ export const POST = withAuth(
const sourceId = params.id;
if (!sourceId) throw new ValidationError('id is required');
const body = await parseBody(req, mergeSchema);
await mergeDuplicate(sourceId, body.targetId, ctx.portId);
await mergeDuplicate(sourceId, body.targetId, ctx.portId, ctx.userId);
return NextResponse.json({ ok: true });
} catch (error) {
return errorResponse(error);