From 0f648a924b91ee59787d2ad4ffe6d2946a0243db Mon Sep 17 00:00:00 2001 From: Matt Ciaccio Date: Wed, 6 May 2026 22:40:35 +0200 Subject: [PATCH] =?UTF-8?q?fix(audit):=20LOWs=20sweep=20=E2=80=94=20trunca?= =?UTF-8?q?te=20auth=20entityId,=20fix=20legacy=20berthId=20in=20seed-data?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L3: failed-login audit's entityId could carry an unbounded attempted-email value (the form lets you type anything). Truncate to 256 chars before using as entityId; full original still in metadata for forensic context. L2: seed-data.ts (the realistic fixture) inserted interests with berthId — that column was dropped in migration 0029 and the realistic seed would fail at insert on a fresh DB. Now inserts via the interestBerths junction (mirrors the synthetic seed's pattern). L1 (no-op): next-in-line notification already gets the 5-min cooldownMs default from createNotification, so retries are idempotent without extra code. Verified. L5 (no-op): import worker comment already explains the stub state adequately; no code change. 1175/1175 vitest passing. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/api/auth/[...all]/route.ts | 7 +++- src/lib/db/seed-data.ts | 52 ++++++++++++++++++++++-------- 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/src/app/api/auth/[...all]/route.ts b/src/app/api/auth/[...all]/route.ts index b79e685..1c1ac22 100644 --- a/src/app/api/auth/[...all]/route.ts +++ b/src/app/api/auth/[...all]/route.ts @@ -50,12 +50,17 @@ function logSignIn(args: { const email = parsed?.user?.email ?? parsed?.email ?? args.attemptedEmail ?? null; const ok = args.status >= 200 && args.status < 300; + // entityId is text/unbounded but indexed; truncate the attempted- + // email fallback to keep the row predictably sized when the form + // sends a giant value. The audit metadata still carries the full + // original attempted email for forensic context. + const safeAttempted = (args.attemptedEmail ?? '').slice(0, 256); void createAuditLog({ userId, portId: null, action: 'login', entityType: 'session', - entityId: userId ?? args.attemptedEmail ?? 'unknown', + entityId: userId ?? safeAttempted ?? 'unknown', metadata: { ok, status: args.status, diff --git a/src/lib/db/seed-data.ts b/src/lib/db/seed-data.ts index 097625c..217acf4 100644 --- a/src/lib/db/seed-data.ts +++ b/src/lib/db/seed-data.ts @@ -39,6 +39,7 @@ import { berths, berthReservations, interests, + interestBerths, documentTemplates, } from './schema'; import { @@ -966,20 +967,43 @@ export async function seedPortData(portId: string, portSlug: string): Promise ({ - portId, - clientId: clientIds[p.clientIdx]!, - berthId: p.berthIdx !== null ? berthRows[p.berthIdx]!.id : null, - yachtId: p.yachtIdx !== null ? yachtRows[p.yachtIdx]!.id : null, - pipelineStage: p.pipelineStage, - leadCategory: p.leadCategory, - source: p.source, - dateFirstContact: daysAgo(p.daysAgoFirst), - dateLastContact: daysAgo(Math.max(0, p.daysAgoFirst - 2)), - archivedAt: p.archived ? daysAgo(p.daysAgoFirst - 30) : null, - })), - ); + // Insert interests WITHOUT berthId (column was dropped in + // migration 0029); berth links go through the interest_berths + // junction below. Returning the rows so we can wire up the + // junction with the right interestId per row. + const insertedInterests = await tx + .insert(interests) + .values( + interestPlan.map((p) => ({ + portId, + clientId: clientIds[p.clientIdx]!, + yachtId: p.yachtIdx !== null ? yachtRows[p.yachtIdx]!.id : null, + pipelineStage: p.pipelineStage, + leadCategory: p.leadCategory, + source: p.source, + dateFirstContact: daysAgo(p.daysAgoFirst), + dateLastContact: daysAgo(Math.max(0, p.daysAgoFirst - 2)), + archivedAt: p.archived ? daysAgo(p.daysAgoFirst - 30) : null, + })), + ) + .returning({ id: interests.id }); + + const junctionRows: Array = []; + interestPlan.forEach((p, i) => { + if (p.berthIdx === null) return; + const interestId = insertedInterests[i]?.id; + if (!interestId) return; + junctionRows.push({ + interestId, + berthId: berthRows[p.berthIdx]!.id, + isPrimary: true, + isSpecificInterest: true, + isInEoiBundle: false, + }); + }); + if (junctionRows.length > 0) { + await tx.insert(interestBerths).values(junctionRows); + } // ── 8. Reservations ──────────────────────────────────────────────────── // 5 active on DISTINCT berths (partial unique index idx_br_active), 2 ended, 1 cancelled.