fix(audit): LOWs sweep — truncate auth entityId, fix legacy berthId in seed-data

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) <noreply@anthropic.com>
This commit is contained in:
Matt Ciaccio
2026-05-06 22:40:35 +02:00
parent b4fb3b2ca6
commit 0f648a924b
2 changed files with 44 additions and 15 deletions

View File

@@ -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,

View File

@@ -39,6 +39,7 @@ import {
berths,
berthReservations,
interests,
interestBerths,
documentTemplates,
} from './schema';
import {
@@ -966,11 +967,16 @@ export async function seedPortData(portId: string, portSlug: string): Promise<Se
},
];
await tx.insert(interests).values(
// 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]!,
berthId: p.berthIdx !== null ? berthRows[p.berthIdx]!.id : null,
yachtId: p.yachtIdx !== null ? yachtRows[p.yachtIdx]!.id : null,
pipelineStage: p.pipelineStage,
leadCategory: p.leadCategory,
@@ -979,7 +985,25 @@ export async function seedPortData(portId: string, portSlug: string): Promise<Se
dateLastContact: daysAgo(Math.max(0, p.daysAgoFirst - 2)),
archivedAt: p.archived ? daysAgo(p.daysAgoFirst - 30) : null,
})),
);
)
.returning({ id: interests.id });
const junctionRows: Array<typeof interestBerths.$inferInsert> = [];
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.