Fixes issues raised by the whole-branch review before merge: - Critical: the previous migration hardcoded the live production functions URL and anon key as the committed default, so every local `supabase db reset` silently pointed dev/test triggers at the real project. Adding a new migration instead of editing the already-applied one (checksum/drift safety), with local dev values as the committed default; production values are applied out-of-band, never committed. - Important: verify_jwt=true plus sending the anon key as a bearer token didn't actually stop anyone, since the anon key is public (ships in the frontend bundle) — confirmed empirically, a request with a valid anon key but no shared secret now gets a real 401. Added a random shared secret known only to the database and the Edge Functions' secret store. - Important: notification emails interpolated raw contact fields into HTML with no escaping. Added escapeHtml() and tests proving injected markup renders as literal text, not live markup. - Important: the URL/key/secret functions moved from public to a private schema with EXECUTE revoked from anon/authenticated, closing an unintended PostgREST RPC endpoint (verified 404 after the fix). Also adds a test:functions npm script now that Deno is confirmed available and the shared email builders' tests pass (12 total incl. the new escaping cases). Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
45 lines
1.7 KiB
TypeScript
45 lines
1.7 KiB
TypeScript
function escapeHtml(value: string): string {
|
|
return value
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''')
|
|
}
|
|
|
|
export interface BookingEmailInput {
|
|
serviceName: string
|
|
date: string
|
|
time: string
|
|
contact: { name: string; phone: string; email: string; notes: string }
|
|
}
|
|
|
|
export function buildBookingNotificationEmail(input: BookingEmailInput): { subject: string; html: string } {
|
|
const serviceName = escapeHtml(input.serviceName)
|
|
const name = escapeHtml(input.contact.name)
|
|
const phone = escapeHtml(input.contact.phone)
|
|
const email = escapeHtml(input.contact.email)
|
|
const notesLine = input.contact.notes ? `<p>Notes: ${escapeHtml(input.contact.notes)}</p>` : ''
|
|
return {
|
|
subject: `New booking: ${input.serviceName} on ${input.date}`,
|
|
html: `<p><strong>${name}</strong> booked <strong>${serviceName}</strong> on ${escapeHtml(input.date)} at ${escapeHtml(input.time)}.</p>` +
|
|
`<p>Phone: ${phone}<br/>Email: ${email}</p>${notesLine}`,
|
|
}
|
|
}
|
|
|
|
export interface InquiryEmailInput {
|
|
productName: string
|
|
contact: { name: string; email: string; notes: string }
|
|
}
|
|
|
|
export function buildInquiryNotificationEmail(input: InquiryEmailInput): { subject: string; html: string } {
|
|
const productName = escapeHtml(input.productName)
|
|
const name = escapeHtml(input.contact.name)
|
|
const email = escapeHtml(input.contact.email)
|
|
const notesLine = input.contact.notes ? `<p>Notes: ${escapeHtml(input.contact.notes)}</p>` : ''
|
|
return {
|
|
subject: `New inquiry: ${input.productName}`,
|
|
html: `<p><strong>${name}</strong> (${email}) asked about <strong>${productName}</strong>.</p>${notesLine}`,
|
|
}
|
|
}
|