chore: deploy Phase 2A backend to live Supabase project (Task 9)
Substitutes the real admin UID everywhere the placeholder appeared, points the Edge Functions base URL at the deployed project, and adds production JWT verification: trigger functions now send the anon key as a bearer token so verify_jwt can stay enabled instead of leaving the functions open to unauthenticated calls from the public internet. Also adds scripts/create-admin.ts (one-off helper used to create Mira's admin auth user), documents the seed script's non-VITE env vars in .env.example, and closes a gitignore gap where plain .env wasn't excluded alongside .env.local. Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
This commit is contained in:
@@ -1,2 +1,7 @@
|
|||||||
|
# Frontend (exposed to the browser)
|
||||||
VITE_SUPABASE_URL=
|
VITE_SUPABASE_URL=
|
||||||
VITE_SUPABASE_ANON_KEY=
|
VITE_SUPABASE_ANON_KEY=
|
||||||
|
|
||||||
|
# scripts/seed.ts (server-side only, never expose the service role key to the browser)
|
||||||
|
SUPABASE_URL=
|
||||||
|
SUPABASE_SERVICE_ROLE_KEY=
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -13,6 +13,7 @@ dist-ssr
|
|||||||
*.local
|
*.local
|
||||||
|
|
||||||
# Environment variables
|
# Environment variables
|
||||||
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
|
|
||||||
|
|||||||
36
scripts/create-admin.ts
Normal file
36
scripts/create-admin.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { createClient } from '@supabase/supabase-js'
|
||||||
|
|
||||||
|
const supabaseUrl = process.env.SUPABASE_URL
|
||||||
|
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY
|
||||||
|
const adminEmail = process.env.ADMIN_EMAIL
|
||||||
|
const adminPassword = process.env.ADMIN_PASSWORD
|
||||||
|
|
||||||
|
if (!supabaseUrl || !serviceRoleKey) {
|
||||||
|
console.error('Set SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY environment variables before running this script.')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!adminEmail || !adminPassword) {
|
||||||
|
console.error('Set ADMIN_EMAIL and ADMIN_PASSWORD environment variables before running this script.')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = createClient(supabaseUrl, serviceRoleKey)
|
||||||
|
|
||||||
|
async function createAdmin() {
|
||||||
|
const { data, error } = await supabase.auth.admin.createUser({
|
||||||
|
email: adminEmail,
|
||||||
|
password: adminPassword,
|
||||||
|
email_confirm: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Failed to create admin user:', error.message)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Admin user created.')
|
||||||
|
console.log('UID:', data.user.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
createAdmin()
|
||||||
@@ -387,17 +387,16 @@ deno_version = 2
|
|||||||
|
|
||||||
# These two functions are invoked exclusively by database triggers via
|
# These two functions are invoked exclusively by database triggers via
|
||||||
# pg_net (see supabase/migrations/20260808000003_notify_triggers.sql), not
|
# pg_net (see supabase/migrations/20260808000003_notify_triggers.sql), not
|
||||||
# by end-user clients carrying a Supabase JWT. Local `functions serve`
|
# by end-user clients carrying a Supabase JWT. They're reachable over the
|
||||||
# enforces JWT verification by default; without disabling it here, pg_net's
|
# public internet once deployed, so JWT verification stays enabled to stop
|
||||||
# request (which sends no Authorization header) is rejected with 401
|
# anyone from POSTing directly to the URL and triggering an outbound email
|
||||||
# "Missing authorization header" before the function code ever runs —
|
# send; the trigger functions supply the anon key as a bearer token (see
|
||||||
# confirmed empirically via `select * from net._http_response` showing that
|
# public.edge_functions_anon_key() in that migration) to satisfy it.
|
||||||
# exact error after a trigger fired.
|
|
||||||
[functions.on-booking-created]
|
[functions.on-booking-created]
|
||||||
verify_jwt = false
|
verify_jwt = true
|
||||||
|
|
||||||
[functions.on-inquiry-created]
|
[functions.on-inquiry-created]
|
||||||
verify_jwt = false
|
verify_jwt = true
|
||||||
|
|
||||||
[analytics]
|
[analytics]
|
||||||
enabled = true
|
enabled = true
|
||||||
|
|||||||
@@ -81,48 +81,48 @@ alter table public.inquiries enable row level security;
|
|||||||
create policy "products_public_read" on public.products
|
create policy "products_public_read" on public.products
|
||||||
for select using (true);
|
for select using (true);
|
||||||
create policy "products_admin_write" on public.products
|
create policy "products_admin_write" on public.products
|
||||||
for all using ((select auth.uid()) = '00000000-0000-0000-0000-000000000001'::uuid)
|
for all using ((select auth.uid()) = '21c90224-e0b2-412e-a431-8e7f097c1cef'::uuid)
|
||||||
with check ((select auth.uid()) = '00000000-0000-0000-0000-000000000001'::uuid);
|
with check ((select auth.uid()) = '21c90224-e0b2-412e-a431-8e7f097c1cef'::uuid);
|
||||||
|
|
||||||
-- services: public read, admin write
|
-- services: public read, admin write
|
||||||
create policy "services_public_read" on public.services
|
create policy "services_public_read" on public.services
|
||||||
for select using (true);
|
for select using (true);
|
||||||
create policy "services_admin_write" on public.services
|
create policy "services_admin_write" on public.services
|
||||||
for all using ((select auth.uid()) = '00000000-0000-0000-0000-000000000001'::uuid)
|
for all using ((select auth.uid()) = '21c90224-e0b2-412e-a431-8e7f097c1cef'::uuid)
|
||||||
with check ((select auth.uid()) = '00000000-0000-0000-0000-000000000001'::uuid);
|
with check ((select auth.uid()) = '21c90224-e0b2-412e-a431-8e7f097c1cef'::uuid);
|
||||||
|
|
||||||
-- schedule_template: public read, admin write
|
-- schedule_template: public read, admin write
|
||||||
create policy "schedule_template_public_read" on public.schedule_template
|
create policy "schedule_template_public_read" on public.schedule_template
|
||||||
for select using (true);
|
for select using (true);
|
||||||
create policy "schedule_template_admin_write" on public.schedule_template
|
create policy "schedule_template_admin_write" on public.schedule_template
|
||||||
for all using ((select auth.uid()) = '00000000-0000-0000-0000-000000000001'::uuid)
|
for all using ((select auth.uid()) = '21c90224-e0b2-412e-a431-8e7f097c1cef'::uuid)
|
||||||
with check ((select auth.uid()) = '00000000-0000-0000-0000-000000000001'::uuid);
|
with check ((select auth.uid()) = '21c90224-e0b2-412e-a431-8e7f097c1cef'::uuid);
|
||||||
|
|
||||||
-- schedule_exceptions: public read, admin write
|
-- schedule_exceptions: public read, admin write
|
||||||
create policy "schedule_exceptions_public_read" on public.schedule_exceptions
|
create policy "schedule_exceptions_public_read" on public.schedule_exceptions
|
||||||
for select using (true);
|
for select using (true);
|
||||||
create policy "schedule_exceptions_admin_write" on public.schedule_exceptions
|
create policy "schedule_exceptions_admin_write" on public.schedule_exceptions
|
||||||
for all using ((select auth.uid()) = '00000000-0000-0000-0000-000000000001'::uuid)
|
for all using ((select auth.uid()) = '21c90224-e0b2-412e-a431-8e7f097c1cef'::uuid)
|
||||||
with check ((select auth.uid()) = '00000000-0000-0000-0000-000000000001'::uuid);
|
with check ((select auth.uid()) = '21c90224-e0b2-412e-a431-8e7f097c1cef'::uuid);
|
||||||
|
|
||||||
-- bookings: anyone can insert (submit a booking); only admin can read/update/delete
|
-- bookings: anyone can insert (submit a booking); only admin can read/update/delete
|
||||||
create policy "bookings_public_insert" on public.bookings
|
create policy "bookings_public_insert" on public.bookings
|
||||||
for insert with check (true);
|
for insert with check (true);
|
||||||
create policy "bookings_admin_all" on public.bookings
|
create policy "bookings_admin_all" on public.bookings
|
||||||
for all using ((select auth.uid()) = '00000000-0000-0000-0000-000000000001'::uuid)
|
for all using ((select auth.uid()) = '21c90224-e0b2-412e-a431-8e7f097c1cef'::uuid)
|
||||||
with check ((select auth.uid()) = '00000000-0000-0000-0000-000000000001'::uuid);
|
with check ((select auth.uid()) = '21c90224-e0b2-412e-a431-8e7f097c1cef'::uuid);
|
||||||
|
|
||||||
-- booked_slots: public read; only admin policy exists for writes (in practice only
|
-- booked_slots: public read; only admin policy exists for writes (in practice only
|
||||||
-- the security-definer trigger in the next migration ever writes here)
|
-- the security-definer trigger in the next migration ever writes here)
|
||||||
create policy "booked_slots_public_read" on public.booked_slots
|
create policy "booked_slots_public_read" on public.booked_slots
|
||||||
for select using (true);
|
for select using (true);
|
||||||
create policy "booked_slots_admin_write" on public.booked_slots
|
create policy "booked_slots_admin_write" on public.booked_slots
|
||||||
for all using ((select auth.uid()) = '00000000-0000-0000-0000-000000000001'::uuid)
|
for all using ((select auth.uid()) = '21c90224-e0b2-412e-a431-8e7f097c1cef'::uuid)
|
||||||
with check ((select auth.uid()) = '00000000-0000-0000-0000-000000000001'::uuid);
|
with check ((select auth.uid()) = '21c90224-e0b2-412e-a431-8e7f097c1cef'::uuid);
|
||||||
|
|
||||||
-- inquiries: anyone can insert; only admin can read/update/delete
|
-- inquiries: anyone can insert; only admin can read/update/delete
|
||||||
create policy "inquiries_public_insert" on public.inquiries
|
create policy "inquiries_public_insert" on public.inquiries
|
||||||
for insert with check (true);
|
for insert with check (true);
|
||||||
create policy "inquiries_admin_all" on public.inquiries
|
create policy "inquiries_admin_all" on public.inquiries
|
||||||
for all using ((select auth.uid()) = '00000000-0000-0000-0000-000000000001'::uuid)
|
for all using ((select auth.uid()) = '21c90224-e0b2-412e-a431-8e7f097c1cef'::uuid)
|
||||||
with check ((select auth.uid()) = '00000000-0000-0000-0000-000000000001'::uuid);
|
with check ((select auth.uid()) = '21c90224-e0b2-412e-a431-8e7f097c1cef'::uuid);
|
||||||
|
|||||||
@@ -59,7 +59,23 @@ returns text
|
|||||||
language sql
|
language sql
|
||||||
immutable
|
immutable
|
||||||
as $$
|
as $$
|
||||||
select 'http://host.docker.internal:54321/functions/v1'
|
select 'https://ylxppnclfpnnolnjkbwv.supabase.co/functions/v1'
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- The Edge Functions gateway enforces JWT verification (verify_jwt = true,
|
||||||
|
-- see supabase/config.toml) in production, since these functions are
|
||||||
|
-- reachable over the public internet and would otherwise let anyone trigger
|
||||||
|
-- an outbound email send by POSTing directly to the URL. pg_net's request
|
||||||
|
-- carries no Authorization header by default, so the trigger must supply
|
||||||
|
-- one. The anon key is not a secret (it already ships in the frontend
|
||||||
|
-- bundle), so embedding it here as a literal, single-source-of-truth
|
||||||
|
-- constant is safe — the same pattern as edge_functions_base_url() above.
|
||||||
|
create or replace function public.edge_functions_anon_key()
|
||||||
|
returns text
|
||||||
|
language sql
|
||||||
|
immutable
|
||||||
|
as $$
|
||||||
|
select 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InlseHBwbmNsZnBubm9sbmprYnd2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODY0NTkyNzYsImV4cCI6MjEwMjAzNTI3Nn0.j0UaxlBvDctsDOXxPuWj43XvbObtX68-mjNf3AAefX8'
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
create or replace function public.notify_booking_created()
|
create or replace function public.notify_booking_created()
|
||||||
@@ -71,7 +87,10 @@ as $$
|
|||||||
begin
|
begin
|
||||||
perform net.http_post(
|
perform net.http_post(
|
||||||
url := public.edge_functions_base_url() || '/on-booking-created',
|
url := public.edge_functions_base_url() || '/on-booking-created',
|
||||||
headers := jsonb_build_object('Content-Type', 'application/json'),
|
headers := jsonb_build_object(
|
||||||
|
'Content-Type', 'application/json',
|
||||||
|
'Authorization', 'Bearer ' || public.edge_functions_anon_key()
|
||||||
|
),
|
||||||
body := jsonb_build_object('record', to_jsonb(new))
|
body := jsonb_build_object('record', to_jsonb(new))
|
||||||
);
|
);
|
||||||
return new;
|
return new;
|
||||||
@@ -92,7 +111,10 @@ as $$
|
|||||||
begin
|
begin
|
||||||
perform net.http_post(
|
perform net.http_post(
|
||||||
url := public.edge_functions_base_url() || '/on-inquiry-created',
|
url := public.edge_functions_base_url() || '/on-inquiry-created',
|
||||||
headers := jsonb_build_object('Content-Type', 'application/json'),
|
headers := jsonb_build_object(
|
||||||
|
'Content-Type', 'application/json',
|
||||||
|
'Authorization', 'Bearer ' || public.edge_functions_anon_key()
|
||||||
|
),
|
||||||
body := jsonb_build_object('record', to_jsonb(new))
|
body := jsonb_build_object('record', to_jsonb(new))
|
||||||
);
|
);
|
||||||
return new;
|
return new;
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ select throws_ok(
|
|||||||
|
|
||||||
-- The admin (matching UID) can read bookings
|
-- The admin (matching UID) can read bookings
|
||||||
set local role authenticated;
|
set local role authenticated;
|
||||||
set local request.jwt.claim.sub = '00000000-0000-0000-0000-000000000001';
|
set local request.jwt.claim.sub = '21c90224-e0b2-412e-a431-8e7f097c1cef';
|
||||||
select isnt_empty(
|
select isnt_empty(
|
||||||
$$ select * from public.bookings $$,
|
$$ select * from public.bookings $$,
|
||||||
'admin can read bookings'
|
'admin can read bookings'
|
||||||
|
|||||||
Reference in New Issue
Block a user