audit: Tier 1/3/4/5/7 batch — SSE, gates, dedup, URL escape, FK constraints
Tier 1.6: S3Backend.put now sets ServerSideEncryption=AES256 — closes the cleartext-at-rest gap for signed contracts, GDPR exports, pg_dumps. Tier 3.7: New safeUrl() helper in lib/email/shell.ts. Scheme allow-list (http/https/mailto/tel/relative only — javascript:/data:/vbscript:/file: rewritten to about:blank) + HTML-attribute escape. Retrofitted across all 7 transactional templates (crm-invite, portal-auth, document-signing, notification-digest, residential-inquiry, admin-email-change). Tier 4.2: /api/v1/alerts GET now gated on admin.view_audit_log. Tier 4.3: Documenso webhook handler emits captureErrorEvent on catch. Admin/errors no longer silent on webhook crashes. Tier 4.6: Inquiry-funnel email dedup is now case-insensitive (LOWER(value)) and stores normalized email on insert. Capital-letter resubmissions no longer spawn duplicate client+yacht+interest rows. Tier 5.6 + data-model H1: migration 0056 adds FK user_permission_overrides.user_id → user(id) cascade, same for user_port_roles.userId, plus partial unique index on user_email_changes pending rows. Tier 7.6: @types/node bumped from ^25 to ^20.19.0 — matches the runtime. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -84,11 +84,18 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
// ─── Transactional trio creation ────────────────────────────────────────
|
||||
const result = await withTransaction(async (tx) => {
|
||||
// 1. Find or create client by email (case-sensitive contact match, same
|
||||
// behavior as before the refactor).
|
||||
// 1. Find or create client by email. The inquiry-funnel audit
|
||||
// flagged that the previous exact match was case-sensitive —
|
||||
// capital-letter resubmissions spawned duplicate client+yacht+
|
||||
// interest rows. Match LOWER(value) instead so foo@x.com and
|
||||
// Foo@X.COM dedupe to the same client.
|
||||
let clientId: string;
|
||||
const normalizedEmail = data.email.trim().toLowerCase();
|
||||
const existingContact = await tx.query.clientContacts.findFirst({
|
||||
where: and(eq(clientContacts.channel, 'email'), eq(clientContacts.value, data.email)),
|
||||
where: and(
|
||||
eq(clientContacts.channel, 'email'),
|
||||
sql`LOWER(${clientContacts.value}) = ${normalizedEmail}`,
|
||||
),
|
||||
});
|
||||
if (existingContact) {
|
||||
const existingClient = await tx.query.clients.findFirst({
|
||||
@@ -320,7 +327,9 @@ async function createClientInTx(
|
||||
await tx.insert(clientContacts).values({
|
||||
clientId,
|
||||
channel: 'email',
|
||||
value: data.email,
|
||||
// Store lowercased so the case-insensitive dedup match above always
|
||||
// hits on subsequent submissions.
|
||||
value: data.email.trim().toLowerCase(),
|
||||
isPrimary: true,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user