Quando usarlo
Gli admin devono vedere o gestire dati oltre ai propri, per esempio l'elenco
degli utenti con ruolo e stato del billing. I passi usano una pagina /users
come esempio.
Regole da rispettare
- L'app admin da sola non è un confine di sicurezza: ogni pagina e Route
Handler chiama
getCurrentAdmin()e si ferma senza un admin. Un controllo solo nel layout non basta, perché i layout non si renderizzano di nuovo durante la navigazione e non impediscono il rendering della pagina. - Il ruolo si legge sul server da
profilestramite la RLS, mai da un cookie, da un parametro della query o da un flag del client. - Gli admin leggono le righe degli altri utenti tramite policy RLS, con la anon
key e la sessione. Non usare una service-role key in
apps/admin: scavalca la RLS, quindi un errore in una pagina esporrebbe tutte le righe. - Nessuna policy deve permettere agli utenti di cambiare
profiles.role. Promuovi un admin con SQL dalla dashboard di Supabase o con una migration.
Passi
Aggiungi una migration in
supabase/migrations/(append-only) con una funzione di supporto che dice se chi chiama è admin e con le policy che servono alla pagina. Una policy suprofilesnon può interrogare direttamenteprofiles(Postgres segnala una ricorsione infinita), per questo la funzione èsecurity definer:create function public.is_admin() returns boolean language sql stable security definer set search_path = '' as $$ select exists ( select 1 from public.profiles where id = (select auth.uid()) and role = 'admin' ); $$; revoke execute on function public.is_admin() from public, anon; grant execute on function public.is_admin() to authenticated; create policy "admins read all profiles" on public.profiles for select to authenticated using ((select public.is_admin())); create policy "admins read all billing accounts" on public.billing_accounts for select to authenticated using ((select public.is_admin()));La funzione risponde solo su chi la chiama, quindi si può invocare dall'API senza rischi. Aggiungi solo policy
select, a meno che la pagina non modifichi davvero i dati.In
apps/admin/lib/current_admin.tssposta la creazione del client Supabase in una funzione esportatacreateAdminSupabaseClient()e usala ingetCurrentAdmin(), così le pagine interrogano con lo stesso client per richiesta.Crea
apps/admin/app/users/page.tsxcome Server Component. Chiama primagetCurrentAdmin()e, senza un admin, mostra lo stesso "Access denied" diapps/admin/app/page.tsxprima di qualsiasi query. Poi leggi concreateAdminSupabaseClient(), per esempiofrom('profiles').select('id, email, role, billing_accounts(status)'), e mostra solo i campi che servono alla pagina.Se la pagina modifica dati, usa una Server Action o una Route Handler che chiama di nuovo
getCurrentAdmin()e aggiungi per quell'operazione una policy RLS che usa anch'essapublic.is_admin().Applica la migration e verifica con due account: un
membervede "Access denied" su/userse unaselectsuprofilescon la sua sessione restituisce ancora solo la sua riga; unadminvede tutti gli utenti.Descrivi le nuove policy in
DATABASE.mde cosa possono fare gli admin inPRODUCT.md.
Completato quando
- i comandi di verifica superano;
- la pagina e ogni azione al suo interno chiamano
getCurrentAdmin()sul server; - un
membernon ottiene righe in più né dall'API né dal database, verificato come nel passo 5; DATABASE.mdePRODUCT.mddescrivono la modifica.
