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
54 lines
1.6 KiB
TypeScript
54 lines
1.6 KiB
TypeScript
import { createClient } from 'jsr:@supabase/supabase-js@2'
|
|
import { buildBookingNotificationEmail } from '../_shared/email.ts'
|
|
|
|
const FROM_ADDRESS = 'bookings@braidsbymira.com'
|
|
const MIRA_EMAIL = 'hello@braidsbymira.com'
|
|
|
|
Deno.serve(async (req) => {
|
|
const notifySecret = Deno.env.get('NOTIFY_WEBHOOK_SECRET')
|
|
if (notifySecret && req.headers.get('X-Notify-Secret') !== notifySecret) {
|
|
return new Response(JSON.stringify({ error: 'unauthorized' }), {
|
|
status: 401,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
})
|
|
}
|
|
|
|
const payload = await req.json()
|
|
const booking = payload.record
|
|
|
|
const supabaseAdmin = createClient(
|
|
Deno.env.get('SUPABASE_URL')!,
|
|
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!,
|
|
)
|
|
|
|
const { data: service } = await supabaseAdmin
|
|
.from('services')
|
|
.select('name')
|
|
.eq('id', booking.service_id)
|
|
.single()
|
|
|
|
const { subject, html } = buildBookingNotificationEmail({
|
|
serviceName: service?.name ?? booking.service_id,
|
|
date: booking.date,
|
|
time: booking.time,
|
|
contact: {
|
|
name: booking.contact_name,
|
|
phone: booking.contact_phone,
|
|
email: booking.contact_email,
|
|
notes: booking.contact_notes,
|
|
},
|
|
})
|
|
|
|
const resendApiKey = Deno.env.get('RESEND_API_KEY')
|
|
await fetch('https://api.resend.com/emails', {
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: `Bearer ${resendApiKey}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ from: FROM_ADDRESS, to: MIRA_EMAIL, subject, html }),
|
|
})
|
|
|
|
return new Response(JSON.stringify({ ok: true }), { headers: { 'Content-Type': 'application/json' } })
|
|
})
|