Wann du es brauchst
Admins müssen Daten über ihre eigenen hinaus sehen oder verwalten, zum Beispiel
die Liste der Nutzer mit Rolle und Billing-Status. Die Schritte verwenden eine
Seite /users als Beispiel.
Regeln
- Die Admin-App ist für sich keine Sicherheitsgrenze: Jede Seite und jeder
Route Handler ruft
getCurrentAdmin()auf und bricht ohne Admin ab. Eine Prüfung nur im Layout reicht nicht, denn Layouts werden bei der Navigation nicht neu gerendert und halten die Seite nicht vom Rendern ab. - Die Rolle wird auf dem Server über RLS aus
profilesgelesen, nie aus einem Cookie, einem Query-Parameter oder einem Flag des Clients. - Admins lesen die Zeilen anderer Nutzer über RLS-Policies, mit dem Anon-Key
und der Sitzung. Verwende in
apps/adminkeinen Service-Role-Key: Er umgeht RLS, ein Fehler in einer Seite würde also alle Zeilen offenlegen. - Keine Policy darf Nutzern erlauben,
profiles.rolezu ändern. Ernenne Admins per SQL im Supabase-Dashboard oder mit einer Migration.
Schritte
Lege eine Migration in
supabase/migrations/an (append-only) mit einer Hilfsfunktion, die sagt, ob der Aufrufer Admin ist, und den Policies, die die Seite braucht. Eine Policy aufprofileskannprofilesnicht direkt abfragen (Postgres meldet eine unendliche Rekursion), deshalb ist die Hilfsfunktionsecurity 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()));Die Hilfsfunktion antwortet nur über den Aufrufer selbst und darf daher über die API aufgerufen werden. Lege nur
select-Policies an, außer die Seite ändert wirklich Daten.Verschiebe in
apps/admin/lib/current_admin.tsdas Erzeugen des Supabase-Clients in eine exportierte FunktioncreateAdminSupabaseClient()und nutze sie ingetCurrentAdmin(), damit Seiten mit demselben Client pro Request abfragen.Lege
apps/admin/app/users/page.tsxals Server Component an. Rufe zuerstgetCurrentAdmin()auf und zeige ohne Admin vor jeder Abfrage dasselbe "Access denied" wieapps/admin/app/page.tsx. Lies dann mitcreateAdminSupabaseClient(), zum Beispielfrom('profiles').select('id, email, role, billing_accounts(status)'), und rendere nur die Felder, die die Seite zeigt.Ändert die Seite Daten, verwende eine Server Action oder einen Route Handler, der erneut
getCurrentAdmin()aufruft, und lege für diese Operation eine RLS-Policy an, die ebenfallspublic.is_admin()nutzt.Wende die Migration an und prüfe mit zwei Konten: Ein
membersieht auf/users"Access denied", und einselectaufprofilesmit seiner Sitzung liefert weiterhin nur seine eigene Zeile; einadminsieht alle Nutzer.Beschreibe die neuen Policies in
DATABASE.mdund was Admins tun können inPRODUCT.md.
Fertig, wenn
- die Prüfbefehle durchlaufen;
- die Seite und jede Aktion darauf
getCurrentAdmin()auf dem Server aufrufen; - ein
memberweder über die API noch über die Datenbank zusätzliche Zeilen erhält, geprüft wie in Schritt 5; DATABASE.mdundPRODUCT.mddie Änderung beschreiben.
