Tutti i playbookNext.js + Flutter Product Monorepo

Aggiungere una pagina admin

Aggiungere all'app admin una pagina che legge i dati di tutti gli utenti, autorizzata sul server e da una policy RLS riservata agli admin.

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 profiles tramite 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

  1. 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 su profiles non può interrogare direttamente profiles (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.

  2. In apps/admin/lib/current_admin.ts sposta la creazione del client Supabase in una funzione esportata createAdminSupabaseClient() e usala in getCurrentAdmin(), così le pagine interrogano con lo stesso client per richiesta.

  3. Crea apps/admin/app/users/page.tsx come Server Component. Chiama prima getCurrentAdmin() e, senza un admin, mostra lo stesso "Access denied" di apps/admin/app/page.tsx prima di qualsiasi query. Poi leggi con createAdminSupabaseClient(), per esempio from('profiles').select('id, email, role, billing_accounts(status)'), e mostra solo i campi che servono alla pagina.

  4. 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'essa public.is_admin().

  5. Applica la migration e verifica con due account: un member vede "Access denied" su /users e una select su profiles con la sua sessione restituisce ancora solo la sua riga; un admin vede tutti gli utenti.

  6. Descrivi le nuove policy in DATABASE.md e cosa possono fare gli admin in PRODUCT.md.

Completato quando

  • i comandi di verifica superano;
  • la pagina e ogni azione al suo interno chiamano getCurrentAdmin() sul server;
  • un member non ottiene righe in più né dall'API né dal database, verificato come nel passo 5;
  • DATABASE.md e PRODUCT.md descrivono la modifica.