WFDASH-WEBAPP/scripts/migrations/2026-09-25-create-audit-log.ts
Dave 7555196166 feat(audit): implement audit trail for delivery notes and orders
- Added an audit trail feature that logs every create, update, and delete action on delivery notes and orders, capturing detailed changes per field.
- Introduced the `AuditTimeline` component to display the history of changes for delivery notes and orders, enhancing traceability.
- Updated the `page.tsx` files for delivery notes and orders to fetch and display audit logs, ensuring users can view the history of modifications.
- Enhanced the `package.json` with a new migration script for setting up the audit log functionality.

This feature improves accountability and transparency in the management of delivery notes and orders.
2026-09-30 01:48:44 +02:00

115 lines
4 KiB
TypeScript

/**
* One-shot migration: create the `audit_log` collection.
*
* Run with:
*
* npx tsx scripts/migrations/2026-09-25-create-audit-log.ts
*
* One row per create / update / delete on quotes, quote_items,
* quote_attachments, orders, order_items and delivery_notes. Rows are
* written ONLY by the PB hook `pocketbase/pb_hooks/audit_log.pb.js`
* (which runs as the app, so it bypasses the API rules below). Nobody
* can create, edit or delete an audit row through the API — the
* create/update/delete rules are superuser-only (null).
*
* Read side: `src/lib/server/audit-log-service.ts` (admin client),
* surfaced on the quote / order / delivery-note detail pages.
*
* Idempotent — safe to re-run; exits early if the collection exists.
*
* Rollback: remove the hook file from pb_hooks, then delete the
* `audit_log` collection in the PB admin UI. Nothing references it.
*/
import "./_env-loader";
import PocketBase from "pocketbase";
const PB_URL = process.env.NEXT_PUBLIC_POCKETBASE_URL || "http://127.0.0.1:8090";
const ADMIN_EMAIL = process.env.PB_ADMIN_EMAIL;
const ADMIN_PASSWORD = process.env.PB_ADMIN_PASSWORD;
// Explicit `max` on every text field: PB treats `max: 0` as a 5000-char
// cap, not "unlimited", and fails the insert with a generic 400.
const text = (name: string, max: number, required = false) => ({
name,
type: "text",
required,
max,
});
async function main() {
if (!ADMIN_EMAIL || !ADMIN_PASSWORD) {
throw new Error(
"Set PB_ADMIN_EMAIL and PB_ADMIN_PASSWORD env vars (e.g. sourced from .env.local)."
);
}
const pb = new PocketBase(PB_URL);
await pb.collection("_superusers").authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD);
console.log(`[audit-log] Authenticated against ${PB_URL}`);
const existing = await pb.collections
.getFullList()
.then((all) => all.find((c) => c.name === "audit_log"));
if (existing) {
console.log("[audit-log] audit_log already exists — skipping.");
return;
}
await pb.collections.create({
name: "audit_log",
type: "base",
// Any logged-in dashboard user may read the trail; only the hook
// (running as the app) may write it.
listRule: "@request.auth.id != ''",
viewRule: "@request.auth.id != ''",
createRule: null,
updateRule: null,
deleteRule: null,
fields: [
// Which record changed.
text("collection_name", 64, true),
text("record_id", 32, true),
// Which detail page the change rolls up to: the quote for a
// quote_item, the order for an order_item, etc.
{
name: "parent_type",
type: "select",
required: true,
maxSelect: 1,
values: ["quote", "order", "delivery_note"],
},
text("parent_id", 32, true),
// create | update | delete | pdf_generated | attachment_added
text("action", 32, true),
// Human context for the row, e.g. the item description or quote number.
text("label", 500),
// [{ field, from, to }] — values truncated by the hook.
{ name: "changes", type: "json", required: false, maxSize: 200000 },
// Who: resolved PB user id + display name, or a hint for
// automations (n8n / agent) that write with the superuser key.
text("actor_id", 32),
text("actor_name", 200),
text("actor_email", 200),
// web | server | agent | mcp | n8n | system
text("source", 32),
// Extra lookup keys, space separated: "dn:DN26071401 quote:abc".
text("refs", 500),
{ name: "created", type: "autodate", onCreate: true, onUpdate: false },
],
indexes: [
"CREATE INDEX idx_audit_log_parent ON audit_log (parent_type, parent_id, created)",
"CREATE INDEX idx_audit_log_record ON audit_log (collection_name, record_id)",
],
});
console.log("[audit-log] Created audit_log collection. Migration complete.");
console.log(
"[audit-log] Next: copy pocketbase/pb_hooks/audit_log.pb.js into the PB server's pb_hooks/ directory and restart PB."
);
}
main().catch((err) => {
console.error("[audit-log] Migration failed:", err);
process.exit(1);
});