diff --git a/supabase/migrations/20260813000001_site_content_testimonials_rules.sql b/supabase/migrations/20260813000001_site_content_testimonials_rules.sql new file mode 100644 index 0000000..be42bcf --- /dev/null +++ b/supabase/migrations/20260813000001_site_content_testimonials_rules.sql @@ -0,0 +1,98 @@ +-- Phase 2D: adds site-editable content tables (site_content, testimonials, +-- rules) and featured-product columns, all following the existing +-- public-read/admin-write RLS pattern from the initial schema. Seed values +-- match what the site currently shows via hardcoded frontend constants, so +-- this migration doesn't blank any live content. + +create table public.site_content ( + id text primary key default 'default', + hero_headline text not null, + hero_tagline text not null, + about_bio text not null, + about_portrait_image text not null, + contact_email text not null, + contact_hours text not null +); + +alter table public.site_content enable row level security; + +create policy "site_content_public_read" on public.site_content + for select using (true); +create policy "site_content_admin_write" on public.site_content + for all using ((select auth.uid()) = '2b88676b-f901-459a-9147-b914190c4f81'::uuid) + with check ((select auth.uid()) = '2b88676b-f901-459a-9147-b914190c4f81'::uuid); + +insert into public.site_content (id, hero_headline, hero_tagline, about_bio, about_portrait_image, contact_email, contact_hours) +values ( + 'default', + 'Vos cheveux, votre couronne', + 'Coiffures protectrices par Mira', + 'Avec plus d''une décennie d''expérience derrière le fauteuil, Mira aborde chaque installation comme une œuvre d''art — douce pour le cuir chevelu, précise à la racine, et achevée pour une couronne que vous aurez envie d''afficher.', + 'about/mira-portrait.jpg', + 'hello@braidsbymira.com', + 'Mar–Sam, 9h à 18h' +); + +create table public.testimonials ( + id uuid primary key default gen_random_uuid(), + quote text not null, + author text not null, + avatar_image text not null, + sort_order integer not null +); + +alter table public.testimonials enable row level security; + +create policy "testimonials_public_read" on public.testimonials + for select using (true); +create policy "testimonials_admin_write" on public.testimonials + for all using ((select auth.uid()) = '2b88676b-f901-459a-9147-b914190c4f81'::uuid) + with check ((select auth.uid()) = '2b88676b-f901-459a-9147-b914190c4f81'::uuid); + +insert into public.testimonials (quote, author, avatar_image, sort_order) values + ('Mira m''a fait sentir comme une reine sur son fauteuil. Mes tresses ont tenu 8 semaines !', 'Amara T.', 'testimonials/amara.jpg', 0), + ('La tresseuse la plus douce et la plus précise que j''aie jamais consultée.', 'Keisha R.', 'testimonials/keisha.jpg', 1), + ('La réservation était si simple, et le résultat parle de lui-même.', 'Nadia F.', 'testimonials/nadia.jpg', 2); + +create table public.rules ( + id uuid primary key default gen_random_uuid(), + title text not null, + body text not null, + icon_key text not null, + sort_order integer not null +); + +alter table public.rules enable row level security; + +create policy "rules_public_read" on public.rules + for select using (true); +create policy "rules_admin_write" on public.rules + for all using ((select auth.uid()) = '2b88676b-f901-459a-9147-b914190c4f81'::uuid) + with check ((select auth.uid()) = '2b88676b-f901-459a-9147-b914190c4f81'::uuid); + +insert into public.rules (title, body, icon_key, sort_order) values + ('Acompte', 'Un acompte de 30 $ est exigé afin de confirmer toute réservation. Celui-ci n''est pas remboursable, mais transférable à une date ultérieure sur entente, pour un maximum d''une fois. Toute annulation moins de 48 heures avant le rendez-vous entraînera la perte de l''acompte, qui ne sera ni remboursé ni transféré. En cas de non-présentation (« no-show »), l''acompte est automatiquement perdu.', 'deposit', 0), + ('Préalables cheveux', 'Prière de garder vos cheveux propres, démêlés et secs, prêts à être tressés. Le blowdry peut être facturé si cette règle n''est pas respectée. Pas de bains d''huiles la veille ou le jour du rendez-vous, s''il vous plaît. Merci de toujours préciser, photo à l''appui, vos caractéristiques spécifiques (cheveux courts, locks, etc.).', 'hair-prep', 1), + ('Mèches et extensions', 'Les mèches sont à déposer 24 à 48h avant la prestation si ce n''est pas moi qui les fournis. Choisissez de préférence des mèches pré-étirées ; l''étirage des mèches vous sera facturé en fonction du nombre de paquets.', 'extensions', 2), + ('Retard', 'Les retards sans notification préalable ne sont pas autorisés. Un retard de 15 à 20 minutes entraîne des frais supplémentaires, et à partir de 30 minutes, le rendez-vous est automatiquement annulé.', 'lateness', 3), + ('Accompagnant', 'Vous êtes tenue de vous présenter seule à votre rendez-vous. Merci de ne pas venir accompagnée sans l''avoir signalé au préalable.', 'guests', 4), + ('Paiement', 'Le paiement peut être fait en espèces ou par virement Interac.', 'payment', 5); + +alter table public.products add column featured boolean not null default false; +alter table public.products add column featured_order integer; + +-- Grant SELECT to all public roles; write operations are restricted to admin via RLS +grant select on public.site_content to anon, authenticated; +grant select on public.testimonials to anon, authenticated; +grant select on public.rules to anon, authenticated; + +-- Grant write permissions only to authenticated (admin) role; RLS policies further +-- restrict these to the specific admin UUID +grant update on public.site_content to authenticated; +grant insert, update, delete on public.testimonials to authenticated; +grant insert, update, delete on public.rules to authenticated; + +-- Grant all permissions to service_role (which bypasses RLS for admin tasks) +grant all on public.site_content to service_role; +grant all on public.testimonials to service_role; +grant all on public.rules to service_role; diff --git a/supabase/tests/database/rls.test.sql b/supabase/tests/database/rls.test.sql index db78b43..4a20d16 100644 --- a/supabase/tests/database/rls.test.sql +++ b/supabase/tests/database/rls.test.sql @@ -1,5 +1,5 @@ begin; -select plan(53); +select plan(64); -- Seed reference data as service_role (bypasses RLS) set local role service_role; @@ -391,5 +391,91 @@ select is_empty( 'booked_slots mirror row is removed after the duration-test booking is cancelled, regardless of duration' ); +-- site_content: public read, admin-only write. Singleton row seeded by the +-- migration, so no insert test is needed -- only read and update. +set local role anon; +select isnt_empty( + $$ select * from public.site_content where id = 'default' $$, + 'anon can read site_content' +); + +select throws_ok( + $$ update public.site_content set hero_headline = 'hack' where id = 'default' $$, + '42501', + null, + 'anon cannot update site_content' +); + +set local role authenticated; +set local request.jwt.claim.sub = '2b88676b-f901-459a-9147-b914190c4f81'; +select lives_ok( + $$ update public.site_content set hero_headline = 'Updated Headline' where id = 'default' $$, + 'admin can update site_content' +); + +-- testimonials: public read, admin-only write. +set local role anon; +select isnt_empty( + $$ select * from public.testimonials $$, + 'anon can read testimonials' +); + +select throws_ok( + $$ insert into public.testimonials (quote, author, avatar_image, sort_order) + values ('hack', 'Hacker', 'x.jpg', 99) $$, + '42501', + null, + 'anon cannot insert into testimonials' +); + +set local role authenticated; +set local request.jwt.claim.sub = '2b88676b-f901-459a-9147-b914190c4f81'; +select lives_ok( + $$ insert into public.testimonials (quote, author, avatar_image, sort_order) + values ('Great service!', 'Test Client', 'test.jpg', 99) $$, + 'admin can insert into testimonials' +); + +-- rules: public read, admin-only write. +set local role anon; +select isnt_empty( + $$ select * from public.rules $$, + 'anon can read rules' +); + +select throws_ok( + $$ insert into public.rules (title, body, icon_key, sort_order) + values ('hack', 'hack', 'deposit', 99) $$, + '42501', + null, + 'anon cannot insert into rules' +); + +set local role authenticated; +set local request.jwt.claim.sub = '2b88676b-f901-459a-9147-b914190c4f81'; +select lives_ok( + $$ insert into public.rules (title, body, icon_key, sort_order) + values ('Test Rule', 'Test body.', 'payment', 99) $$, + 'admin can insert into rules' +); + +-- products.featured / featured_order: no new RLS policy (existing +-- products_admin_write already covers all columns), but confirm anon still +-- can't set them and admin can. +set local role anon; +select throws_ok( + $$ update public.products set featured = true where id = 'edge-control' $$, + '42501', + null, + 'anon cannot set products.featured' +); + +set local role authenticated; +set local request.jwt.claim.sub = '2b88676b-f901-459a-9147-b914190c4f81'; +select lives_ok( + $$ update public.products set featured = true, featured_order = 0 where id = 'edge-control' $$, + 'admin can set products.featured and featured_order' +); + select * from finish(); rollback;