246 lines
7.1 KiB
Markdown
246 lines
7.1 KiB
Markdown
# 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 <div>Hallo {user.email}</div>;
|
|
}
|
|
```
|
|
|
|
### Client component ("use client")
|
|
```tsx
|
|
"use client";
|
|
import { createBrowserClient } from "@supabase/ssr";
|
|
|
|
const supabase = createBrowserClient(URL, KEY);
|
|
|
|
export function LogoutButton() {
|
|
return <button onClick={() => supabase.auth.signOut()}>Logout</button>;
|
|
}
|
|
```
|
|
|
|
**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 <LoginPrompt />;
|
|
|
|
const { data: polls } = await supabase
|
|
.from("polls")
|
|
.select("*")
|
|
.eq("user_id", user.id);
|
|
|
|
return <PollList polls={polls} />;
|
|
}
|
|
```
|
|
|
|
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.
|