109 lines
3.9 KiB
Markdown
109 lines
3.9 KiB
Markdown
|
|
# Audit Logging Fix Plan
|
||
|
|
_Scheduled: after hours_
|
||
|
|
|
||
|
|
## Problem
|
||
|
|
|
||
|
|
Two code paths can change a task's status without capturing the **previous** status in the audit log:
|
||
|
|
|
||
|
|
1. **`POST /api/tasks/[id]/notes`** — When a user adds a note and simultaneously changes task status, the audit entry is `TASK_NOTE_ADDED` with only `statusChange: <new>`. The old status is never recorded.
|
||
|
|
2. **`src/lib/shape-import/run-import.ts`** — SHAPE import directly calls `prisma.task.update()` and `prisma.task.create()` with `COMPLETED`/`NA` status. Zero per-task audit entries.
|
||
|
|
|
||
|
|
The notes-route gap is the most operationally urgent because users are actively using it daily. The import gap is lower risk (import is a rare admin operation).
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Fix 1 — Notes Route: Capture Old Status (Priority: High)
|
||
|
|
|
||
|
|
**File:** `src/app/api/tasks/[id]/notes/route.ts`
|
||
|
|
|
||
|
|
### What to change
|
||
|
|
|
||
|
|
The task is already fetched before the note is written (to check assignment). Add the old status to the audit log, and emit a **separate** `TASK_STATUS_CHANGED` entry when a status change accompanies the note.
|
||
|
|
|
||
|
|
```ts
|
||
|
|
// After fetching task (line ~42), old status is already available as task.status
|
||
|
|
|
||
|
|
if (status && ['COMPLETED', 'NOT_STARTED', 'IN_PROGRESS'].includes(status)) {
|
||
|
|
await prisma.task.update({ ... }) // existing logic, unchanged
|
||
|
|
|
||
|
|
// ADD: dedicated status-change audit entry
|
||
|
|
await prisma.auditLog.create({
|
||
|
|
data: {
|
||
|
|
userId,
|
||
|
|
action: 'TASK_STATUS_CHANGED',
|
||
|
|
entityType: 'Task',
|
||
|
|
entityId: id,
|
||
|
|
oldValues: { status: task.status },
|
||
|
|
newValues: { status, source: 'note' },
|
||
|
|
},
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
// Existing TASK_NOTE_ADDED entry — update to include oldStatus too
|
||
|
|
await prisma.auditLog.create({
|
||
|
|
data: {
|
||
|
|
userId,
|
||
|
|
action: 'TASK_NOTE_ADDED',
|
||
|
|
entityType: 'Task',
|
||
|
|
entityId: id,
|
||
|
|
oldValues: { status: task.status }, // ADD this
|
||
|
|
newValues: { noteId: note.id, statusChange: status ?? null },
|
||
|
|
},
|
||
|
|
})
|
||
|
|
```
|
||
|
|
|
||
|
|
### Verification after deploy
|
||
|
|
```sql
|
||
|
|
-- Confirm old status is now captured
|
||
|
|
SELECT old_values->>'status', new_values->>'status', new_values->>'source', created_at
|
||
|
|
FROM audit_logs
|
||
|
|
WHERE action = 'TASK_STATUS_CHANGED'
|
||
|
|
ORDER BY created_at DESC
|
||
|
|
LIMIT 10;
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Fix 2 — PATCH Route: Confirm old_values.status is always set (Priority: Medium)
|
||
|
|
|
||
|
|
**File:** `src/app/api/tasks/[id]/route.ts`
|
||
|
|
|
||
|
|
The `TASK_UPDATED` entries already capture `oldValues: { status: task.status }`, but only when a status field is present in the request body. Verify the task is always fetched before the update (it is — line ~30 fetches `task`). No code change needed; just confirm via spot-check.
|
||
|
|
|
||
|
|
```sql
|
||
|
|
-- Spot-check: any TASK_UPDATED entries missing old_values.status
|
||
|
|
SELECT COUNT(*) FROM audit_logs
|
||
|
|
WHERE action = 'TASK_UPDATED'
|
||
|
|
AND old_values->>'status' IS NULL;
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Fix 3 — SHAPE Import: Per-task Audit Logging (Priority: Low)
|
||
|
|
|
||
|
|
**File:** `src/lib/shape-import/run-import.ts`
|
||
|
|
|
||
|
|
This is a bulk operation run rarely by admins. The `ShapeImportRun` table already records run-level stats. Acceptable to leave per-task audit logging out for now given the volume (hundreds of tasks per run would bloat the audit table).
|
||
|
|
|
||
|
|
**Recommendation:** Add a summary `SHAPE_IMPORT_TASK_SUMMARY` audit entry per import run that records counts by status (created COMPLETED, created NOT_STARTED, updated, skipped). Already partially done via `ImportStats`.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Deployment Steps
|
||
|
|
|
||
|
|
1. Apply Fix 1 to `notes/route.ts`
|
||
|
|
2. TypeScript check: `npx tsc --noEmit`
|
||
|
|
3. Commit: `git commit -m "Fix: capture old status in task notes audit log"`
|
||
|
|
4. Push + deploy: `docker compose -f /opt/stacks/horizon/docker-compose.yml up -d --build`
|
||
|
|
5. Run verification queries above to confirm
|
||
|
|
6. Optionally apply Fix 3 summary entry to shape import
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Notes
|
||
|
|
|
||
|
|
- No DB schema changes required — `old_values` and `new_values` are already `jsonb` columns.
|
||
|
|
- No migration needed.
|
||
|
|
- Zero downtime deploy (standard Next.js container rebuild).
|
||
|
|
- Fix 1 is safe to apply during off-hours with no user impact.
|