# Les 10 — Supabase Auth & Row Level Security: Verdieping ## Lesstof Vervolg op Les 9. We bouwen de auth-flow uit tot productie-kwaliteit en zetten RLS goed in. Aan het einde is je QuickPoll-app multi-user veilig: elke gebruiker kan alleen zijn eigen polls bewerken en verwijderen, anderen kunnen meelezen en stemmen. --- ## 1. Recap Les 9 Aan het einde van Les 9 had je: - `signUp`, `signInWithPassword`, `signOut` server actions - Navbar met sessie-status - Middleware die de sessie ververst - Eén basale RLS-policy op `polls` Vandaag gaan we daar veel verder mee. Vier onderwerpen: de drie auth-methodes in detail, sessies onder de motorkap, RLS in detail, en beschermde routes via twee patronen. --- ## 2. De drie auth-methodes ### 2.1 E-mail + wachtwoord Wat we in Les 9 gebruikten. Vereist e-mailverificatie in productie. Code is simpel, UX is klassiek. ### 2.2 Magic link Geen wachtwoord, gebruiker krijgt een link in zijn e-mail. ```typescript await supabase.auth.signInWithOtp({ email: "user@example.com", options: { emailRedirectTo: "https://app.com/auth/callback" } }); ``` Voordeel: gebruiker hoeft geen wachtwoord te onthouden, geen "wachtwoord vergeten"-flow. Nadeel: gebruiker moet altijd zijn e-mail openen. ### 2.3 OAuth (Google, GitHub, Discord, etc.) ```typescript await supabase.auth.signInWithOAuth({ provider: "google", options: { redirectTo: "https://app.com/auth/callback" } }); ``` Voordeel: één klik. Nadeel: gebruiker moet account hebben bij die provider. **Wanneer welke?** - B2B SaaS → e-mail + wachtwoord of magic link - Consumer app → OAuth (Google/Apple) - Internal tools → magic link - High-security → e-mail + wachtwoord + MFA --- ## 3. Hoe een sessie écht werkt Een Supabase-sessie bestaat uit twee tokens: | Token | Levensduur | Gebruik | |-------|-----------|---------| | Access token (JWT) | 1 uur | Elke API-call | | Refresh token | 30 dagen | Nieuwe access token aanvragen | De flow: 1. Login → server stuurt beide tokens terug, opgeslagen in cookies 2. Elke request stuurt het access token mee 3. Na 1 uur: access token verlopen, refresh token genereert een nieuwe 4. Na 30 dagen of na logout: opnieuw inloggen `@supabase/ssr` regelt stap 3 automatisch in de middleware. ### Wat zit er in een JWT? ```json { "sub": "abc-uuid-van-gebruiker", "email": "user@example.com", "role": "authenticated", "exp": 1734567890 } ``` `sub` is de user-ID. Postgres leest dit uit voor `auth.uid()` in je RLS-policies. --- ## 4. Server- vs. client-componenten In Next.js 16 App Router heb je twee plekken waar je auth gebruikt: ### Server component (default) ```tsx import { createSupabaseServerClient } from "@/lib/supabase-server"; export default async function Page() { const supabase = await createSupabaseServerClient(); const { data: { user } } = await supabase.auth.getUser(); if (!user) redirect("/login"); return
Hallo {user.email}
; } ``` ### Client component ("use client") ```tsx "use client"; import { createBrowserClient } from "@supabase/ssr"; const supabase = createBrowserClient(URL, KEY); export function LogoutButton() { return ; } ``` **Vuistregel:** doe alles in server components tenzij je interactie nodig hebt (een knop, een form-event). --- ## 5. Row Level Security in detail RLS draait in Postgres zelf. Je server action vraagt data op, en Postgres beslist wat je terugkrijgt — niet je code. ### `using` vs. `with check` | Clausule | Gebruikt bij | Wat het doet | |----------|--------------|--------------| | `using` | SELECT, UPDATE, DELETE | Welke rijen zie/raak je? | | `with check` | INSERT, UPDATE | Mag de NIEUWE waarde bestaan? | Voorbeeld: voorkomen dat een gebruiker een poll claimt die van iemand anders is: ```sql create policy "owner can update" on polls for update using (auth.uid() = user_id) with check (auth.uid() = user_id); ``` `using` zegt: je mag de rij bewerken als je de eigenaar bent. `with check` zegt: na de update moet je nog steeds de eigenaar zijn (anders kun je `user_id` overschrijven). ### Vier basis-policies voor je `polls` tabel ```sql -- 1. Iedereen mag polls lezen create policy "polls public select" on polls for select using (true); -- 2. Ingelogde gebruikers mogen polls aanmaken create policy "auth users can insert" on polls for insert with check (auth.uid() is not null and auth.uid() = user_id); -- 3. Alleen eigenaar mag updaten create policy "owner can update" on polls for update using (auth.uid() = user_id) with check (auth.uid() = user_id); -- 4. Alleen eigenaar mag verwijderen create policy "owner can delete" on polls for delete using (auth.uid() = user_id); ``` --- ## 6. Beschermde routes: twee patronen ### Patroon 1 — middleware redirect Voor hele route-groepen (bijv. alle `/app/*`): ```typescript // middleware.ts const { data: { user } } = await supabase.auth.getUser(); if (!user && request.nextUrl.pathname.startsWith("/create")) { return NextResponse.redirect(new URL("/login", request.url)); } ``` Snel, gecentraliseerd. Nadeel: je kunt geen page-specifieke UX tonen ("log in om dit te zien" met preview). ### Patroon 2 — server component check Voor pagina-specifieke logica: ```tsx export default async function MyPollsPage() { const supabase = await createSupabaseServerClient(); const { data: { user } } = await supabase.auth.getUser(); if (!user) return ; const { data: polls } = await supabase .from("polls") .select("*") .eq("user_id", user.id); return ; } ``` Gebruik **patroon 1** voor "deze hele sectie is privé", **patroon 2** voor "deze pagina werkt anders per user". --- ## 7. Belangrijk: dubbele veiligheid Best practice = check op twee niveaus: 1. **Middleware** zegt: anonieme bezoeker mag deze route niet zien 2. **RLS** zegt: zelfs als de bezoeker er komt, ziet/wijzigt hij niks dat niet van hem is Waarom? Als ooit één laag faalt (bug, vergeten check, security update), is de andere er nog. Diepte-verdediging. --- ## 8. Veelvoorkomende fouten | Fout | Oplossing | |------|-----------| | Policy werkt niet | RLS aan? `alter table X enable row level security;` | | `auth.uid()` is null in policy | Cookies komen niet door — middleware check | | Anoniem account kan ineens INSERTen | `with check` ontbreekt op insert-policy | | User kan andermans rij stelen via update | `with check` op update vergeten | | Service-role-key in browser | NOOIT. Gebruik anon-key in browser, service-role alleen op server | --- ## 9. Checklist einde les - 4 RLS-policies actief op `polls` (select, insert, update, delete) - 2 RLS-policies actief op `options` (select-iedereen, insert-eigenaar-van-poll) - "My Polls" pagina toont alleen polls van ingelogde gebruiker - Edit + Delete knoppen werken alleen voor eigenaar - Middleware-bescherming op `/create` en `/my-polls` - Werkende auth-flow getest met 2 verschillende accounts --- ## 10. Vooruitblik In Les 11 verlaten we QuickPoll en starten met de Vercel AI SDK. Je app moet op dat moment auth + RLS op productie-niveau hebben — dat is de basis waar je AI-features veilig op kunt bouwen.