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:
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user