fix: update lessons
This commit is contained in:
327
Les18-Supabase-Auth-RLS/Les18-Docenttekst.md
Normal file
327
Les18-Supabase-Auth-RLS/Les18-Docenttekst.md
Normal file
@@ -0,0 +1,327 @@
|
||||
# Les 18 — Supabase Auth + RLS
|
||||
## Docenttekst (Klas A — 3 uur, fysiek, demo-driven, **laatste les**)
|
||||
|
||||
**Les:** 18 van 18
|
||||
**Onderwerp:** Multi-user apps met magic link + RLS + social login
|
||||
**Duur:** 180 minuten
|
||||
**Demo-app:** `tasks-app` — multi-user tasks met RLS
|
||||
|
||||
---
|
||||
|
||||
## VÓÓR DE LES (60 min — meer tijd voor laatste les setup)
|
||||
|
||||
1. Werkende `tasks-app` op laptop (magic link + GitHub + RLS)
|
||||
2. Twee email-accounts klaar voor multi-user demo (Tim's eigen + tweede test-mail)
|
||||
3. GitHub OAuth App aangemaakt + Supabase configured
|
||||
4. Backup screenshots: magic link in inbox, RLS policy editor, multi-user side-by-side
|
||||
5. **Speciaal voor laatste les:** bedank-slide klaar, eindopdracht reminder
|
||||
6. Browser tabs: Supabase Dashboard (2 projecten, 1 demo + 1 backup), GitHub OAuth Apps, Vercel
|
||||
7. Mentaal voorbereid: dit is afsluiting cyclus, ruimte voor terugkijken + dank
|
||||
|
||||
---
|
||||
|
||||
## HET SCRIPT
|
||||
|
||||
### BLOK 1 — Welkom + recap 18 lessen (10 min)
|
||||
|
||||
`[SLIDE 1]`
|
||||
|
||||
**Vertel:** "Welkom bij de allerlaatste les. Les 18. Vandaag: Supabase Auth en RLS. En aan het eind: terugkijken op 18 lessen samen."
|
||||
|
||||
`[SLIDE 2 — Curriculum recap]`
|
||||
|
||||
**Vertel:** "Even kort. We hebben vandaag de stof voor jullie eindopdracht compleet. Lessen 1 tot 10 — foundations. Next.js, Supabase, Tailwind, basics. Lessen 11 tot 14 — AI: SDK, tool calling, agents, RAG. Les 15 — Cursor en Vercel. Les 16 — MCP. Les 17 — externe APIs in diepte. Vandaag — multi-user maken.
|
||||
|
||||
Na vandaag heb je álle bouwstenen voor je eindopdracht. Niet meer 'volgende les leer ik nog X'. Vanaf morgen — bouwen."
|
||||
|
||||
`[SLIDE 3 — Planning]`
|
||||
|
||||
**Vertel:** "Drie uur. 65 min theorie en 4 demos. Pauze rond minuut 100. Aan het eind: 15 minuten ruimte voor terugkijken en vragen over eindopdracht."
|
||||
|
||||
---
|
||||
|
||||
### BLOK 2 — Theorie (55 min)
|
||||
|
||||
`[SLIDE 4 — Waarom auth]`
|
||||
|
||||
**Vertel:** "Waarom auth. Wat kun je niet zonder. Per-user data — mijn tasks, jouw tasks. Privacy — alleen jij ziet je data. Audit trail — wie deed wat. Subscriptions — wie betaalde, wie niet.
|
||||
|
||||
Twee concepten die mensen vaak verwarren. Auth — 'wie ben jij'. Authz — 'wat mag jij'. Login is auth. Permissies zijn authz.
|
||||
|
||||
Supabase doet beide. Auth via magic link of OAuth. Authz via RLS. Geïntegreerd, simpel."
|
||||
|
||||
`[SLIDE 5 — Auth methoden]`
|
||||
|
||||
**Vertel:** "Drie methoden. Email + password — klassiek, werkt, maar wachtwoord-management. Magic link — passwordless, user vult email in, krijgt link, klikt, ingelogd. Modern. Social OAuth — login met Google of GitHub.
|
||||
|
||||
Voor demo vandaag: magic link én GitHub OAuth. Beide werken naast elkaar in dezelfde app. User kiest."
|
||||
|
||||
`[SLIDE 6 — RLS]`
|
||||
|
||||
**Vertel:** "Nu het belangrijkste. RLS. Row Level Security. Zonder dit kan iedereen met je anon key letterlijk alle data ophalen uit alle tabellen. Disaster.
|
||||
|
||||
Met RLS: Postgres past automatisch een filter toe. Welke filter? Wat jij definieert in een policy.
|
||||
|
||||
`*[Wijs naar code op slide]*`
|
||||
|
||||
`alter table tasks enable row level security` — RLS aan.
|
||||
|
||||
`create policy "users see own tasks" on tasks for select using (auth.uid() = user_id)` — alleen rows waar user_id matched de ingelogde user mogen geselecteerd worden.
|
||||
|
||||
`auth.uid()` is een Supabase helper — geeft de UUID van ingelogde user, of NULL. Postgres weet wie ingelogd is door de JWT in de Authorization header."
|
||||
|
||||
💬 *Vraag: 'Werkt RLS ook vanuit server-side calls?'*
|
||||
|
||||
**Antwoord:** "Ja, mits je client met de user-JWT calls maakt. Voor server-side acties als admin gebruik je service-role key — die bypasst RLS. Voor user-actions vanuit je server: gebruik de user's session client, RLS werkt zoals verwacht. Komt straks in demo."
|
||||
|
||||
`[SLIDE 7 — Session in Next.js]`
|
||||
|
||||
**Vertel:** "Eén stuk complexiteit. Supabase Auth in Next.js heeft een vier-file pattern. Client, server, middleware, en root middleware.
|
||||
|
||||
Waarom complex. Cookies moeten gesynced. Sessies verlopen. Server components hebben aparte clients dan client components.
|
||||
|
||||
Goed nieuws — Supabase heeft een template voor exact deze setup. Kopiëren werkt. Snappen waarom is bonus. Vandaag kopieer ik en leg uit waarom elke file doet wat-ie doet."
|
||||
|
||||
---
|
||||
|
||||
### BLOK 3 — DEMO 1: Auth setup + magic link (25 min)
|
||||
|
||||
`[SLIDE 8]` + `[SLIDE 9]` `[SCHERM: terminal + editor]`
|
||||
|
||||
```bash
|
||||
cd ~/novi/novi-lessons/Les18-Supabase-Auth-RLS
|
||||
pnpm create next-app@latest tasks-app --typescript --tailwind --app
|
||||
cd tasks-app
|
||||
pnpm add @supabase/supabase-js @supabase/ssr
|
||||
```
|
||||
|
||||
`*[Supabase Dashboard — projectsetup]*` `[SCHERM: browser]`
|
||||
|
||||
**Vertel:** "Nieuw Supabase project of bestaande. Auth → Email → Magic Links staat default aan. Site URL: localhost:3000. Redirect URLs ook localhost. Voor productie pas je dit later aan."
|
||||
|
||||
`*[Editor — maak utils/supabase/client.ts]*`
|
||||
|
||||
**Vertel:** "Eerst de client. Voor Client Components. `createBrowserClient` uit `@supabase/ssr`. Wrap in een `createClient()` functie. Klaar."
|
||||
|
||||
`*[utils/supabase/server.ts]*`
|
||||
|
||||
**Vertel:** "Server-side variant. Belangrijk verschil: leest cookies via Next.js `cookies()` API. Net iets meer code. Belangrijk: `await cookies()` in Next 15+."
|
||||
|
||||
`*[app/login/page.tsx — magic link form]*`
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
const supabase = createClient();
|
||||
await supabase.auth.signInWithOtp({
|
||||
email,
|
||||
options: { emailRedirectTo: `${location.origin}/auth/callback` },
|
||||
});
|
||||
```
|
||||
|
||||
`*[app/auth/callback/route.ts]*`
|
||||
|
||||
**Vertel:** "Callback route. User klikt magic link → Supabase redirect naar dit endpoint met `?code=xyz`. We exchangen die code voor een sessie."
|
||||
|
||||
`*[Test in browser]*`
|
||||
|
||||
**Vertel:** "Email invullen, click. Check mail. Magic link mail komt binnen — onderwerp 'Confirm your signup'. Klik link."
|
||||
|
||||
`*[Browser opent /auth/callback en redirect naar /]*`
|
||||
|
||||
**Vertel:** "Sessie actief. Cookie geset. Maar — we hebben nog geen home page met user-data. Doen we volgende demo."
|
||||
|
||||
---
|
||||
|
||||
### BLOK 4 — DEMO 2: Middleware + protected (20 min)
|
||||
|
||||
`[SLIDE 10]` `[SCHERM: editor]`
|
||||
|
||||
`*[utils/supabase/middleware.ts]*`
|
||||
|
||||
**Vertel:** "Middleware. Draait elke request. Refreshed de sessie zodat user niet om de minuut wordt uitgelogd. Code is template-werk, kopiëren mag."
|
||||
|
||||
`*[middleware.ts root]*`
|
||||
|
||||
**Vertel:** "Root middleware. Importeert `updateSession` uit utils, runt 'm. Matcher excludeert static assets — performance."
|
||||
|
||||
`*[app/page.tsx — protected]*`
|
||||
|
||||
```tsx
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) redirect("/login");
|
||||
```
|
||||
|
||||
**Vertel:** "Server Component check. `auth.getUser()` returnt of user is ingelogd. Geen user? Redirect naar login. Wel? Render welkom-bericht."
|
||||
|
||||
`*[Test]*`
|
||||
|
||||
**Vertel:** "Niet ingelogd — gaat naar login. Inloggen — terug naar home. Werkt. Inkomende vraag uit publiek?"
|
||||
|
||||
💬 *Vraag: 'Hoe weet middleware welke user ingelogd is?'*
|
||||
|
||||
**Antwoord:** "Cookies. Browser stuurt Supabase auth cookies bij elke request. Middleware reads cookie, parseerd JWT, doet `getUser()` om te valideren bij Supabase. Resultaat cached, sessie refreshed indien nodig."
|
||||
|
||||
---
|
||||
|
||||
### BLOK 5 — Pauze (15 min)
|
||||
|
||||
`[SLIDE 11]`
|
||||
|
||||
**Vertel:** "Pauze. Vijftien minuten. Laatste pauze van het hele vak."
|
||||
|
||||
---
|
||||
|
||||
### BLOK 6 — DEMO 3: RLS policies (30 min)
|
||||
|
||||
`[SLIDE 12]` `[SCHERM: editor + supabase + browser]`
|
||||
|
||||
**Vertel:** "Nu het hart van vandaag. RLS. Multi-user."
|
||||
|
||||
`*[Supabase SQL Editor]*`
|
||||
|
||||
```sql
|
||||
create table tasks (
|
||||
id bigserial primary key,
|
||||
user_id uuid not null references auth.users(id) default auth.uid(),
|
||||
text text not null,
|
||||
done boolean default false,
|
||||
created_at timestamp default now()
|
||||
);
|
||||
|
||||
alter table tasks enable row level security;
|
||||
```
|
||||
|
||||
**Vertel:** "Tasks tabel. `user_id` met `default auth.uid()` — bij insert wordt user_id automatisch de ingelogde user. RLS aanzetten."
|
||||
|
||||
```sql
|
||||
create policy "select own" on tasks for select using (auth.uid() = user_id);
|
||||
create policy "insert own" on tasks for insert with check (auth.uid() = user_id);
|
||||
create policy "update own" on tasks for update using (auth.uid() = user_id);
|
||||
create policy "delete own" on tasks for delete using (auth.uid() = user_id);
|
||||
```
|
||||
|
||||
**Vertel:** "Vier policies. Select, insert, update, delete. `using` = check tijdens read. `with check` = check tijdens write. Default-deny: zonder policies kan niemand iets."
|
||||
|
||||
`*[app/page.tsx aanvullen met task UI]*`
|
||||
|
||||
`*[Test in browser — voeg task toe]*`
|
||||
|
||||
**Vertel:** "Werkt. Task in database. Maar — staat user_id daar? Open Supabase Table Editor."
|
||||
|
||||
`*[Toon — user_id is auto-filled met auth.uid()]*`
|
||||
|
||||
**Vertel:** "Magic. Default in schema deed het."
|
||||
|
||||
`*[Open incognito tab — login met andere mail]*`
|
||||
|
||||
**Vertel:** "Tweede gebruiker, andere browser-context. Home page laadt — geen tasks. Even niet vermengd. Voeg ander task toe."
|
||||
|
||||
`*[Toggle terug naar eerste tab]*`
|
||||
|
||||
**Vertel:** "Eerste gebruiker. Refresh. Eigen tasks alleen. Werkt. Multi-user isolation."
|
||||
|
||||
`*[Demo: probeer expliciet andermans data te halen]*`
|
||||
|
||||
```typescript
|
||||
// In Console of via direct supabase call:
|
||||
const { data } = await supabase.from("tasks").select("*");
|
||||
// Krijgt ALLEEN eigen tasks
|
||||
```
|
||||
|
||||
**Vertel:** "Postgres past de filter ALTIJD toe. Geen workaround vanuit client-side. RLS is sterke garantie."
|
||||
|
||||
---
|
||||
|
||||
### BLOK 7 — DEMO 4: Social login (15 min)
|
||||
|
||||
`[SLIDE 13]` `[SCHERM: github + supabase + editor]`
|
||||
|
||||
**Vertel:** "Eén methode is genoeg, maar laat ik GitHub OAuth ook tonen. User-friendlier en common."
|
||||
|
||||
`*[GitHub Settings → Developer settings → OAuth Apps]*`
|
||||
|
||||
**Vertel:** "New OAuth App. Naam: tasks-app. Homepage: localhost:3000. Callback: `https://[project-id].supabase.co/auth/v1/callback` — die URL krijg je van Supabase."
|
||||
|
||||
`*[Save, generate Client Secret]*`
|
||||
|
||||
`*[Supabase Dashboard → Auth → Providers → GitHub]*`
|
||||
|
||||
**Vertel:** "Enable, plak Client ID, plak Secret, save."
|
||||
|
||||
`*[app/login/page.tsx — voeg GitHub button toe]*`
|
||||
|
||||
```tsx
|
||||
async function signInWithGitHub() {
|
||||
await supabase.auth.signInWithOAuth({
|
||||
provider: "github",
|
||||
options: { redirectTo: `${location.origin}/auth/callback` },
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
`*[Test — klik GitHub button]*`
|
||||
|
||||
**Vertel:** "Redirect naar GitHub. Klik Authorize. Terug. Ingelogd als GitHub user. RLS werkt zelfde — GitHub user heeft eigen `auth.uid()`, eigen tasks."
|
||||
|
||||
💬 *Vraag: 'Als ik magic link met test@mail.com gebruik én GitHub-account met dezelfde mail — twee verschillende users?'*
|
||||
|
||||
**Antwoord:** "Default: ja, twee aparte users. Supabase heeft 'Link Identity' optie om same-email users te mergen. Stel je in Auth settings. Voor demo: niet doen, complicates. Productie: meestal wel."
|
||||
|
||||
---
|
||||
|
||||
### BLOK 8 — Eindopdracht recap (5 min)
|
||||
|
||||
`[SLIDE 14]`
|
||||
|
||||
**Vertel:** "Eindopdracht. Tijd om concreet te worden. Wat heb je nodig:
|
||||
|
||||
Next.js met TypeScript en Tailwind — eerste 10 lessen. Supabase Postgres met Auth en RLS — vandaag. AI SDK met Tool Calling of Agent — lessen 11 tot 13. Eventueel RAG — les 14. Cursor en Vercel deploy — les 15. Externe API — les 17. Alles geïntegreerd in een multi-user app — vandaag.
|
||||
|
||||
Ideale eindopdracht: een app die mensen écht zouden gebruiken. Voor jezelf, je ouders, je vereniging. Doet één ding goed. Heeft AI als kern-feature, niet als afterthought. Multi-user met auth. Deployed met productie URL.
|
||||
|
||||
Tijd: 40 tot 60 uur thuis, verspreid over 4 tot 6 weken. Niet één weekend van 80 uur. Push klein en vaak. Vraag hulp tijdig."
|
||||
|
||||
---
|
||||
|
||||
### BLOK 9 — Afsluiting cyclus (15 min)
|
||||
|
||||
`[SLIDE 15]`
|
||||
|
||||
**Vertel:** "Achttien lessen. Achtenvijftig uur klassikaal samen. Plus jullie thuiswerk. Een vol vak.
|
||||
|
||||
Even terugkijken. Les 1 — `pnpm create-next-app`. Vandaag — multi-user app met RLS, AI agents, Vercel preview deploys. Dat is een hele weg.
|
||||
|
||||
Wat je nu zelf kunt — een productie-app van scratch bouwen. AI integreren als kern, niet als gimmick. Externe APIs verwerken met OAuth en webhooks. Multi-user met auth en RLS. Code reviews via PR's. CI/CD pipelines opzetten.
|
||||
|
||||
Drie jaar geleden was dit een specialist-skill. Nu — basis. En jullie zijn er.
|
||||
|
||||
Voor je eindopdracht: werk er gestaag aan. Niet laatste week. Push klein en vaak. Vraag hulp via Slack, mail, of office hours. Eindopdracht-presentatie volgt na inleveren.
|
||||
|
||||
Bedankt voor jullie inzet. Het was een genoegen om met jullie te werken. Veel succes met de eindopdracht. En als je iets bouwt waar je trots op bent — laat het me weten. Echt waar.
|
||||
|
||||
Vragen?"
|
||||
|
||||
`*[Vragenronde — laat dit echt open. 10+ min over laten als kan]*`
|
||||
|
||||
---
|
||||
|
||||
## NA DE LES
|
||||
|
||||
- Push `tasks-app` referentie naar GitHub
|
||||
- Brightspace: eindopdracht-instructies + referentie-repos van alle 18 lessen
|
||||
- Email-template klaar voor "office hours" tijdens eindopdracht-fase
|
||||
- Voor jezelf: noteer welke onderdelen meer of minder werkten — voor Klas B / volgende cyclus
|
||||
|
||||
---
|
||||
|
||||
## Veelvoorkomende fouten
|
||||
|
||||
| Fout | Oplossing |
|
||||
|------|-----------|
|
||||
| `cookies is not a function` | `await cookies()` in Next 15+ |
|
||||
| Magic link mail komt niet | Check spam, productie SMTP via Resend |
|
||||
| RLS error op insert | Default `auth.uid()` op user_id kolom, met check policy |
|
||||
| OAuth callback 404 | Auth → URL Configuration: callback URL exact match |
|
||||
| Tasks van anderen zichtbaar | RLS niet aan? `enable row level security` |
|
||||
| Loop login → home → login | Middleware werkt niet correct — check matcher |
|
||||
| Productie sessie verloopt direct | Site URL in Supabase niet bijgewerkt voor productie URL |
|
||||
Reference in New Issue
Block a user