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 email = parsed?.user?.email ?? parsed?.email ?? args.attemptedEmail ?? null;
|
||||||
const ok = args.status >= 200 && args.status < 300;
|
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({
|
void createAuditLog({
|
||||||
userId,
|
userId,
|
||||||
portId: null,
|
portId: null,
|
||||||
action: 'login',
|
action: 'login',
|
||||||
entityType: 'session',
|
entityType: 'session',
|
||||||
entityId: userId ?? args.attemptedEmail ?? 'unknown',
|
entityId: userId ?? safeAttempted ?? 'unknown',
|
||||||
metadata: {
|
metadata: {
|
||||||
ok,
|
ok,
|
||||||
status: args.status,
|
status: args.status,
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import {
|
|||||||
berths,
|
berths,
|
||||||
berthReservations,
|
berthReservations,
|
||||||
interests,
|
interests,
|
||||||
|
interestBerths,
|
||||||
documentTemplates,
|
documentTemplates,
|
||||||
} from './schema';
|
} from './schema';
|
||||||
import {
|
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) => ({
|
interestPlan.map((p) => ({
|
||||||
portId,
|
portId,
|
||||||
clientId: clientIds[p.clientIdx]!,
|
clientId: clientIds[p.clientIdx]!,
|
||||||
berthId: p.berthIdx !== null ? berthRows[p.berthIdx]!.id : null,
|
|
||||||
yachtId: p.yachtIdx !== null ? yachtRows[p.yachtIdx]!.id : null,
|
yachtId: p.yachtIdx !== null ? yachtRows[p.yachtIdx]!.id : null,
|
||||||
pipelineStage: p.pipelineStage,
|
pipelineStage: p.pipelineStage,
|
||||||
leadCategory: p.leadCategory,
|
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)),
|
dateLastContact: daysAgo(Math.max(0, p.daysAgoFirst - 2)),
|
||||||
archivedAt: p.archived ? daysAgo(p.daysAgoFirst - 30) : null,
|
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 ────────────────────────────────────────────────────
|
// ── 8. Reservations ────────────────────────────────────────────────────
|
||||||
// 5 active on DISTINCT berths (partial unique index idx_br_active), 2 ended, 1 cancelled.
|
// 5 active on DISTINCT berths (partial unique index idx_br_active), 2 ended, 1 cancelled.
|
||||||
|
|||||||
Reference in New Issue
Block a user