fix: lessons
This commit is contained in:
245
Les10-Supabase-Auth/Les10-Lesstof.md
Normal file
245
Les10-Supabase-Auth/Les10-Lesstof.md
Normal file
@@ -0,0 +1,245 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user