fix: update lessons

This commit is contained in:
2026-06-03 16:58:25 +02:00
parent eb1ba2e28d
commit a852aa0d52
83 changed files with 16830 additions and 0 deletions

View File

@@ -0,0 +1 @@
,sharp-zealous-planck,claude,21.05.2026 11:30,file:///sessions/sharp-zealous-planck/.config/libreoffice/4;

View 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 |

View File

@@ -0,0 +1,332 @@
# Les 18 — Huiswerk
## Multi-user Tasks app met RLS + social login + AUTH.md
**Vak:** AI-Assisted Development
**Deadline:** Voor eindopdracht-inlevering (4 weken)
**Inleveren:** GitHub repo + `AUTH.md` in root + Vercel productie URL
---
## Doel
Bouw je auth-demo uit naar productie-niveau: tasks per user met RLS, GitHub social login, en deploy. Eindopdracht-ready.
---
## Onderdeel A — Tasks tabel + RLS (verplicht)
### A1 — Schema
In 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;
create policy "select own tasks" on tasks
for select using (auth.uid() = user_id);
create policy "insert own tasks" on tasks
for insert with check (auth.uid() = user_id);
create policy "update own tasks" on tasks
for update using (auth.uid() = user_id);
create policy "delete own tasks" on tasks
for delete using (auth.uid() = user_id);
```
### A2 — Tasks UI
`app/page.tsx` — extend met tasks lijst + form:
```tsx
import { createClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
async function addTask(formData: FormData) {
"use server";
const supabase = await createClient();
await supabase.from("tasks").insert({ text: formData.get("text") as string });
revalidatePath("/");
}
async function toggleTask(id: number, done: boolean) {
"use server";
const supabase = await createClient();
await supabase.from("tasks").update({ done: !done }).eq("id", id);
revalidatePath("/");
}
async function deleteTask(id: number) {
"use server";
const supabase = await createClient();
await supabase.from("tasks").delete().eq("id", id);
revalidatePath("/");
}
export default async function Home() {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) redirect("/login");
const { data: tasks } = await supabase
.from("tasks")
.select("*")
.order("created_at", { ascending: false });
return (
<main className="p-8 max-w-2xl mx-auto">
<h1 className="text-2xl mb-4">Tasks van {user.email}</h1>
<form action={addTask} className="flex gap-2 mb-6">
<input name="text" className="border p-2 flex-1" placeholder="Nieuwe taak..." />
<button className="bg-blue-600 text-white px-4 py-2 rounded">+</button>
</form>
<ul className="space-y-2">
{tasks?.map((t) => (
<li key={t.id} className="flex items-center gap-2 p-2 border rounded">
<form action={toggleTask.bind(null, t.id, t.done)}>
<button className={t.done ? "line-through" : ""}>{t.text}</button>
</form>
<form action={deleteTask.bind(null, t.id)} className="ml-auto">
<button className="text-red-500">×</button>
</form>
</li>
))}
</ul>
</main>
);
}
```
### Eisen
- [ ] `tasks` tabel met RLS aan
- [ ] 4 policies: select / insert / update / delete
- [ ] UI: add, toggle, delete tasks werkt
- [ ] In incognito met ander account: andere tasks zichtbaar (geen vermenging)
---
## Onderdeel B — Social login (GitHub) (verplicht)
### B1 — GitHub OAuth App
1. github.com → Settings → Developer settings → OAuth Apps → New
2. Application name: jouw app
3. Homepage URL: `http://localhost:3000` (dev) of `https://yourapp.vercel.app` (prod)
4. Callback URL: `https://your-project.supabase.co/auth/v1/callback`
5. Save, generate Client Secret
### B2 — Configure in Supabase
1. Dashboard → Auth → Providers → GitHub
2. Enable + plak Client ID + Secret
### B3 — Button toevoegen
In `app/login/page.tsx`:
```tsx
async function signInWithGitHub() {
const supabase = createClient();
await supabase.auth.signInWithOAuth({
provider: "github",
options: { redirectTo: `${location.origin}/auth/callback` },
});
}
// In render:
<button onClick={signInWithGitHub} className="border px-4 py-2 mt-3 rounded">
Of: Login met GitHub
</button>
```
### Eisen
- [ ] GitHub OAuth App aangemaakt
- [ ] Supabase configured
- [ ] Klik op GitHub button → redirect → ingelogd
- [ ] Tasks werken met GitHub-account
---
## Onderdeel C — Deploy naar Vercel (verplicht)
Combineert Les 14/15:
1. Push naar GitHub
2. Vercel: import repo
3. Add env vars: `NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_ANON_KEY`
4. Deploy
5. Update Supabase Auth → URL Configuration:
- **Site URL:** `https://yourapp.vercel.app`
- **Redirect URLs:** `https://yourapp.vercel.app/**`, `http://localhost:3000/**`
6. Update GitHub OAuth App callback URL to productie
### Eisen
- [ ] App live op Vercel
- [ ] Magic link werkt op productie URL
- [ ] GitHub login werkt op productie URL
- [ ] Tasks per user werken op productie
---
## Onderdeel D — `AUTH.md` documentatie (verplicht)
In repo-root.
### Sectie 1 — Auth methoden
Welke auth methoden ondersteund:
- Magic link (default)
- GitHub OAuth
- (eventuele extra's)
### Sectie 2 — RLS policies
Lijst je policies + waarom:
```markdown
**Table: tasks**
- `select own tasks` — users zien alleen eigen tasks
- `insert own tasks` — voorkomt inserten op andermans naam
- `update own tasks` — alleen eigen toggle
- `delete own tasks` — alleen eigen delete
```
### Sectie 3 — Test van multi-user isolation
Bewijs dat RLS werkt:
- Screenshot van Account A's tasks
- Screenshot van Account B's tasks (incognito)
- Bewijs: account A's tasks staan NIET in account B's lijst
### Sectie 4 — Deployment checklist
Wat je moest aanpassen voor productie:
- Site URL in Supabase
- Redirect URLs in Supabase
- GitHub OAuth callback URL
- Env vars in Vercel
### Sectie 5 — Eindopdracht-architectuur (~250 woorden)
Concrete plan voor je eigen eindopdracht:
- App naam + doel
- User-flows (signup, login, kernfeature)
- Welke tabellen + RLS policies
- AI feature(s) en hoe ze user-context gebruiken
- Externe APIs (uit Les 17)
- Tijd-plan (4-6 weken)
### Vorm
- Max 800 woorden
- Screenshots zijn waardevol
- Concrete plan, geen vaagheid
---
## Bonus (optioneel)
### Bonus 1 — Email + password naast magic link
Voeg klassieke signup/login toe. Combine met magic link.
### Bonus 2 — Profile pagina
`/profile` — user kan naam, avatar URL, etc. instellen. Aparte `profiles` tabel met RLS.
### Bonus 3 — Team feature
Multi-user binnen team. Aparte tabel `teams`, junction `team_members`. RLS policies die team-leden toelaten elkaars data te zien.
### Bonus 4 — AI integration met user context
Voeg een AI chat toe waar de user-tasks deel zijn van de context. AI weet wat jouw tasks zijn (door RLS), kan helpen prioriteren.
---
## Inleveren
1. **GitHub repo URL** in Brightspace
2. **Vercel productie URL** in Brightspace
3. **`AUTH.md`** in repo-root
4. **Test-account credentials** voor docent (email of GitHub)
5. **2-3 screenshots** van multi-user test
---
## Beoordeling
| Criterium | Punten |
|-----------|--------|
| A — Tasks + RLS werkt (4 policies) | 3 |
| B — GitHub OAuth werkt | 2 |
| C — Deployed op Vercel + auth werkt op prod | 2 |
| D — AUTH.md compleet (5 secties) | 2 |
| Multi-user isolation bewezen | 1 |
| **Totaal** | **10** |
Voldoende = 6+. Bonus telt mee.
---
## Tijd-indicatie
| Onderdeel | Tijd |
|-----------|------|
| A — Tasks + RLS | 50 min |
| B — GitHub OAuth | 25 min |
| C — Deploy | 20 min |
| D — AUTH.md | 35 min |
| **Totaal** | **~2 uur 10 min** |
---
## Veelvoorkomende valkuilen
| Probleem | Oplossing |
|----------|-----------|
| Tasks van andere users zichtbaar | RLS niet aan, of policy `using (true)` |
| Insert faalt met "row violates" | RLS check faalt — check `with check (auth.uid() = user_id)` + default |
| OAuth redirect 404 | Site URL niet bijgewerkt voor productie |
| Magic link werkt lokaal niet prod | Site URL in Supabase staat nog op localhost |
| Cookies werken niet | Middleware draait niet — check matcher en path |
| `auth.uid()` returnt null | User niet ingelogd — check session in Server Component |
---
## Tips
- **Test isolation in incognito** — alleen daar ben je écht "another user"
- **RLS error messages** kunnen vaag zijn — log policy + auth.uid() in Postgres function voor debug
- **Productie env vars** — vergeet niet je Vercel env vars te zetten
- **Eindopdracht-koppeling:** dit huiswerk is je startpunt — bouw eindopdracht hier op
- **Veel succes met de eindopdracht!**
---
## Voor je begint aan de eindopdracht
Als afsluiting:
1. Plan je eindopdracht concreet (Sectie 5 van AUTH.md is de basis)
2. Start een nieuwe repo van scratch óf bouw verder op deze
3. Push klein en vaak — niet één big commit aan eind
4. Vraag hulp tijdig — Slack, mail, office hours
5. Plan tijd in van min 8-10 uur/week voor 4-6 weken
**Bedankt voor jullie inzet deze 18 lessen. Veel succes met de eindopdracht!**

View File

@@ -0,0 +1,137 @@
%PDF-1.4
%<25><><EFBFBD><EFBFBD> ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 4 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
5 0 obj
<<
/Contents 12 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 11 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/Contents 13 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 11 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
7 0 obj
<<
/Contents 14 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 11 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
8 0 obj
<<
/Contents 15 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 11 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
9 0 obj
<<
/PageMode /UseNone /Pages 11 0 R /Type /Catalog
>>
endobj
10 0 obj
<<
/Author (NOVI Hogeschool Utrecht) /CreationDate (D:20260521113057+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260521113057+00'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Les 18 Huiswerk) /Trapped /False
>>
endobj
11 0 obj
<<
/Count 4 /Kids [ 5 0 R 6 0 R 7 0 R 8 0 R ] /Type /Pages
>>
endobj
12 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2319
>>
stream
GauHL>Ar7U(4Ol=3#J<=dj>JV&k-B:%irlj!BRi`Nn\t:0Mj#A;'S`:0$RoKk-rY[78\s&fP.q'9jp]UiOP'5I-77B;#Fhs1Kss/^P((.(bcHC94/nL@JL[f>R$f&_Bk8\#`);V,TK?!bWfdDJdN\nAASuaMT/''+bY^bV%NqOf>TJ&U![iT113@V8RiJ:;(Rl&5$0Ioj;^mL,a'q9?LRej`k@$rk8h,or't<H_-BfCWsB#C;&mDm%?#`7qr9]LWNUE5?.X3*+1apSf,^qPO5]d-am6Aeg.pi#KDqUgHF)1&G6H)bY^C/$c1Hc8,,(\"IJn),$V\C+^1_G-F#3$.QX1D,$>cU'=5;;&PN:"f.2[o*8DmqCh=3bi\oeS(dBLk08pj29Vp`f5[U1N#VfrGMi*R<7KLQP0CSrmY7*H;5g)*He.<!FkAAN)n%%K%:676Vl16r(Xh'hgBah'2^MFeXi#C+OI.E)J1c7rCRbo2;Jn\:!Q$WJQa234<KKIRn>.<_:B.<!FkR+V@qO55lD8K44ZI<4aE=[XIR'Uo.3$'/2?`A6#3q&;rboI%!h9snA<EE=d!'ZLbuZ/>[mFHP!2RA[:6gbFlYQ[f5GAjJcMeXMYR1ZJM9rM;0ZihUL8"3\'bBd);K@')M8s7SBr"`9eFBaJ@1V/ie8cp2Dt#4#ePi(H7se-gX6W1>!="Poj(FmdXo>YdYa@0oC\r4e:AQsV0UZ`d,\Kuh4D3mGjBdV&;:T=^Cs:77YtWW@;#^rO$(5SmF,Dt55D^cEpE&eV,mW[h1'6FbQ)\lp+;C*,6e4tF#3_2bZ.a7`I?>I"-3FDq!`9]&R(H1:[JndSabq".EBR7;jYs)qaKcO+DiI*/&mca1LaP=+4d&![+*A],s;'Od>,=RfV;4:-\X$U!dKmsi?n>Diptee#u7J-CTPphb4tKQ6>nIL][hBIl1-dYh#`+_2U(A[*t,f`3T29+SBF)J(2DIA#[g:9UU=#.i#0no"^<_at/$XOiKY)6Ur^dV6(-mZp\E.Dr=pVl:=H5,n32K5:8.9;mFY]#l,565#_8ML1(k'HpA;AE^!&;ru7%'7:[JoZ=%_kK[>4ge/.\Dp47c.r5Dn`-f5$,E1-`[#&;ZKnHbY4YZ+W3O8FomVh'JL:$EP:NF.OTeAI*d^4R";s/S[(&F^U^?Dpg_]:If]0Yek!:4+f(gQIQ_=ndg"hEKSoc4pVGk'f6H5]2`C$oqpLJj6OH-N*%*cd*3/5ZXtO2++a,*\ch$@o6tGk>LuLp??q:sI"iE`/b^8Nn5Q7[YgsA,(T1eaaSBKG>f[H`nBXWKlu`MusqY3'NZ0_c\ll;Qkn&.u"X(PP90>dOk0$bT_7!@CsoCRkRCDeAI$2&>Wm-)tHIO:e45Q7t*T_6GeIohYs,2[mU;Nh2[43E\_TqHhrLUO2Zo'rlV\FeH/ssVCXEZ[X-JaXPei?#\cqi0T,]=rVZCh_*L&#+@?Rt/RDsI*]4>'Ta?kf%;@pH;5;@*.P"*W2.TIIPsQ>HB'#\U-$0^h*r$=H28,BqLm=E'dn(FJCmj'T;[dd=eZGI<R3$S7#4aHpFED_A$l"]Cd-eM/\I;-DdgekQlrYi_?\A9&Ch&cORIpPK%N!)!@ZA3h9iJ.Bjpl;$h=1Lt+ZcT)ij9Q,qAiT/4ap?f_HUB5LsMEBOCZF2%JUM[eZKZV^,eU=\kJ6"\d9?[XH!eD`+ht>QPjU_qN#Oh(_%NicO^q&3r5AuQ%f[Z>O&TdmTcW#Wt4Hg&RdF5r><g&s#iehMR@?6FM75@Wqt:7[U3Ge/?00e=%E;C09(f?OD5%GrO<QiM_N'gMUe,\*B'+,QKW's^%Erog/p\BZb@m"]*$'JQl+Ro^[X!%p*)@ed7oU6EX1L0\hrfg^9,,%\oQY4,&)36I60.EH!\9N`rnK63<FJ^0EA]%FA\`p?)HGrD:)WVG-/*?kM,0U3p*?%?2".iDI+F=G8&jNqYUi4AN@fEh.bP:I#[7D/\p-p>mpG?K7&a@C*Z5h]!Q]`p+:YO5D(f=X`)4eoeD]MVm`oGW2m,)0O%mEIuc<e)Ji63,'U&ff#fs9-:Z_>kV$XQR#QN\G1W=f\_2b>kWqm97C/BTpYB-LR%_c/,Y)%hm^nj3`m8<\Zh\-DMel^[GG%H/fb:Mm.i-0d3f.-kpdJ\uoi:agDT4HOT+fY[HISOTh80-jTUpGd%u2&RFF,nif]e2>Vdj/-]"Hi4l?"?Pqa5eop"rd5Z)bn)&Nkeh2*X9IqZ?[[`qQS(c7;#dXM1]lY.".o<k3pOXR<.WCj6tqrNBYL(O1X\jDkF??[]tH1B~>endstream
endobj
13 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1786
>>
stream
Gb"/'>Ar7S'RnZ;3%4LYFtG:D'LcTc!9_cf=I,hldqr3A<08&&N@_&lm%Pp!hfl#GRb3Y6,Ys=#90+93F5aZuJr(RQI/>^Mh%B9d;kOOgQU!Ri0KOf<On%*I9JD7V5V-VQ/d(*:q@D#2&V2NiE1'?bRLYGi#;.HfdZqUeTAr`Q;?`C?$jsobVB\)48SoWiXo0"IN$2t3Ca%U&pK-oW/Y5U',oqkGrZ`8_TTrCaWl9fdD9%CY%!os459.P0GbQsC)TO"kk,IpL3Lhhd^u4_4+No?f=(Bla$Pg\'9L\bu(k%"`$dG`)cRkW*bB.X]+/o[IKK&KFqkDN>')0>6"H`qj"8!(_=sF>S8a;\+=;oV[XgGTHTk1%="W&jKQqb`XL)(]O+f<@mLW3l4MA,7\etq$l)W,4AGXEdR)QFsk\B_LU_7"$fV@fIJpaBNu*uYjNde9Nj!1=94h_87`6@<I"X>W"PF>W`OWDPX?rfh'QJ$#9>n6SCkjWC$LEs\Zq@hL3L>1/_URI,1CL!\8lhd8J?U;1sZ]8b,SZ)NYAHNSa7qgfPM(o%n_Y<i/"IK=)pKh@O_5i4YM^<SBb1`O4)]LG<C`&-)i3u0:3dB!&W*R++(%$KZUmk4>]IKX87QFg;4Y0DNp2[aTUUl'K5`!E=F?LGs/">2Eeo[BRGWJK?2,-U)2jo5s,H4tT;jO<o.n`(M"q?;U'c\#nol2)t,<=2lag)-r:3`]UR8qs>66'Q$V:ql(6)m0<XeaAe0H@&s8??lGaSA%5AJV$Vr[8j?q:%LKhG5D0PP;N97r;7Y"bnKQ_b7pDGn"Ccu$ek>*N"pM\&2RG_0Z/&d0_?.G,tq:6;D2+U(C$8rd1uRYjgTKKqi5k6TXGOF(8/RfcJZajAMBOMC41olgtXMM+3$tB';B<To@`lDksas8@i]PXpu1lkN>KE5V*u5e$14u\n32E5SV@B3G7c>"&X(MQWZCSJ*snE_+Vm]0E0D%("#[=p@DgW#9<F-G!BW-Tb!IsA\Drgu>!iZ,NWfYH[\Tdq'?&t/e"t-3NGgX18o7l66ikib*+F!6VZOn:>V9b:BIF/:53NKnOg=T4Cr+>]QYn!XX+T*J>4$79BlEHV+tR\k"#B?5+^@L)!7sOG<u-SFlhB?lW[`DU:"7uNA%`UO@bD`L:N,p3YE9?^o>o713.`]K7#0/3=IDe%53nb&LMs6`KM+:kOqi57'1\?USZh!b5W?R@<k=U:qe7pXX_hiV["@n`GX2^-FRCNYPXS;/l`H;2gI,/(X:PDq.Y;m&=]pEM]j)"S=i@Rek>ihGM)JGD"5e&W(^J^4kG*8,55t%'N."L:,B4X[H!H]dW\.9niHbq`L(:9)Nf)B[>'ne$^&E=`05!</Hd&ksn+!$Wa5;7)*e.NC@JgiacV*5L8pqH2X2T!,33uhufVjC#o>sULLXej/mUVn`9?XE=eagh>[G4NDnn`LmQmHa@A*QYF($^T&l[Hfrl3t[B:sf=[?Ab`Spu'KGq8qmhdG`sXnFU7q3uB$/j*:(amPfdY\&[[H*W3=^!"Ujeo#D[MbIDum3R3)d%f%q!lcGt@)hnAWiLsZ^q7\>;qCsKF&.@rONh+2\U#LVAB(9ODF/AAc+#<c>0Y:*-V>W2[U6C60gD"iCA1H;95IjWD\'+ZWG_f9ZUQ2Z[8?Cr3`S0/,[97NA&dX,D_Hs@EM6!.A5+OkhYDr2Mh4"O2C(ss)rbXUGHJKs?R/(j([CeCqXU:qGJD!W,-e)u`eOEkgQ%Ao`Dk>pS<_c)a[4=$9)#jXp7[iW~>endstream
endobj
14 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2152
>>
stream
Gb"/'96Zjb']/F3m\Y'RG+p0(5g6=Xkp)U[:*4I3)fIDY0aSs03,Z'ih`b-$@Jil@P-,]b2?rEX%paVXl;FkV7EoJ0!PBJiX*57+K@'j!:,n2-iU+__AeF/[T=[?.@64Jl&if^ub"F5if-=V;!Xcg<M=<NE6&?/(!/C)mTDDAV\9MFZ+MMLcJccV7on9e4h]X%N.%1h$o7GNgXaWU;(D*e#%b5CP#_jR;CK-A[E+2btG@?`P_*9$f:Z,>\asri0Q,;.aPgQjm0k3BeSK&\N<Dq@&L,\ZmVQ.^J?9a4m'oZI4=Ta'>\:crP^PmXl/"cs(B1XtIj`Q4Q.D/@F&=`dJB67mDjKV`ah\O/XOAWI8j-G=h&J"QF#FWP9dTlM?;N(@h[^d71A"k!_$2$G>]]40F,-/J6(K=52U&%soNXK+UYoVPWknWE"7&XL;SQZ`"d`"Sc#I8\*O_$2Q8F]3RA37=\@pjRm9e@#XqUM\GX*6UL9_<X![.,kQZ[bjj0ID;oXs71[f'LXh>mP<&]_Xe,pC)L9fK)3/`2*J=mLoNHf?l=p@Mg3BaKe>MN48?KIBh]6Frq*<s$?6_NcB0*Q=)VQh/$8seA4_:4/Pi0V,B1%oAo0<.N-cnXSu=b4-\Oh(KYh`agQKMRGZ1r4Z.?Q"UM#)G&-;Zd[_uU:<?NO\_Z\\:=;1."0qnI3"?a6<_jm&oi/mc2c:D,*AguC0%4SB<t\IRb7^4N[^?($ob$+FMTP2p-@67Zq9/"?'FJ=[f7('FBl[5(n.Okl=K2c:=Z-qBlnFnnMV68qbiTElqnjtePJ^OW`[$gA_`CXTe?Z)5qk/g=.J%;Sl3_FG*#O?+Ui>S!*DU"'iWO]8-a8@diJfBdog<@GLo)r=:6TPgcbir,r/FUapn`j\RO]LZkV*:[]EkR<h(Y=(nL0uJn)0%X3T=n=a/Xs=&=N8H/>HoW,9+p?jgmA\raW)?F*%O_`;"W<Sk.s(5\_]F$kD;DR9[lS_P1/]q=BL34elN'o5I2!&:V[jW7R!DfcXgDPH3L*hcSa6FLrJ.W\?!NFPcsrB09%^iS&[]c?T6)FiJ3^cRK?3<G8t-1>Q,FoXURbTF6F"V0^a?N#b-[^:uO-E?/>BX4GJ*n#"FJ,fREna_Qp+Ao4a7@>OQA@;R&o'$RP'nXK<!k'"&uC$qls>pQC;F7i[Gbuh&nOKHC*QC?b)a9q3O,.3317bF!gm%7p;-gRA4O(=WoKad9M.5rV6>mXIa8b--O3WOrm-?t6`*jTA]EEJ".>jrCin?Be'`%8-lG;*1Mo)"_@+&+K=:Sk^;"s;E)F^7;#<o#U.O%<gq"m-rnX>%`@r5S:!O5J6dXR]n)70MIN<dFGh=[edB!u^YBK*.mJBY,QmC;`a(0aYLZq_J\&Jj)6hoTO6c-PfFP6cT$&;<obhL@\,L8>$npB_okq2[_'H+*-:&!8(q2EVW)!p>E69<j%/$EiL03.L)TibX2;/@bFs4ZYW9!bJr2QZDAZC$-th/<W?f^&F>M#Ju82A^L_Ce:&fm/@2b>G0T:7+-s__#NU!lIcK9*pbALL%YBsF@PPTM^JWJs+YM4$N>'R`>(4pCeJ]qq8fSaFg<\akZ^pu!@P=S%m%r^YVe/jGui)YE3hmB8,JR&oqfssZH=_)p<G"nO@5&AWT9#I3tT5QhL:ZPd;0#bXR#qZ57-D=S[OfH;Qh#l19V=n;ak**RMrsBW=#j*7L0OJe>1mA/A$%.#:&JsRm1LuDLUOT.[[QSURRGk#QVXE0Z?-&c=J8a#FM8U$"=&2q9Q+-l,V+IBXfJj9#qHbDu7]G/RM!$f#m%pSh1LM%9U;[WTkXjU6J>.=:!>=l*3aP<92peujLj3WX;f%F-iKoI`m<1bdBcB2[R)6Tk+LdpcK<Ns[ILU\enc4JcGL&apOH"jPNUD15TR&\<W,UO"V8T()`7^U(i8>T92u4YGXYQ)lDSl$lL/mW8XiFMD,TTO1Jk4L'E2:*nZ7QT*F_>lL"_!VP1fNLm1`[-[WWAQ&%C;9S:5mLY7]i)!L("Gs^RR4R-HXf72"\-&=]n&fclYOlZ@Z%a=dh5kH;Xio/(9DYY>Wub>Td_>+gWF8+,lgZ5"j?ne_?a)ns68dN.5b,5tDtqKq%u-`>rN776,/W#[%*/=NLHi~>endstream
endobj
15 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 726
>>
stream
GarnS9lHLd'YO<E9Fm8U87(c%Z7JL9D3RRgS#F9=j?heD-H>!7@8$RlI1&#X)V#srB6InMbo6i.Qg14-(:]AZ^JRfKi`@N[1u`k0M^Q7e_D>Ms!smdV5Oop@hu-BT-7&^<9]SVL<eeJG)VQZLD.MT[/dta9RdbV\;%TF`U"']T"id!,G[D<6j.j!fN/@PQO8>rA(C;mtA!t6ZA@t9h9tdn0>"BB]!TG].hg[3GE*KHC8_6sBi2Nh-?+5(P]F^"5fqN3OVPO].Bhq1*gJZ^2,(;n,jj#pIf7"jL0>ikKpXDH2gidsaTR&E`4s[2A9VB^f*nf%=8r_B<##b%Hq")H40aqpi;@je/[Rc=(kPSf=(-4:)aV"anlpp!GinSq=SB:irWA)JGhCG5]U/*&SSCmX#ALoLRaN1RU#a(,rjX&j2Eni?2AZmo/)S`FO:+iKh54p*D)-QB/6ltM7icU&Y*t-cf3,4t'0r.).[LL.Bj!sr9HLG9*qPpq"\+fH0DA'[(UJcNOk;OA>jTti-ED2^!<TB'(H'q[EXA1`1r@m('H9dNMDg*Ei$(F*m8biCDZmsOnLf682a^4&H<pb1]i[MSVg[+`6i4A1`fMe><NG=6-r]&eY'rk?"3HYRXLTXci;_[et=&UT:-eSbVlQfVAh%`#Yq-0/g"RM*?FBq$o&8eXq*U0!.JMpI7N88_Kd7^289p_a,I/+=X(Ym(1_W3AN.L>b~>endstream
endobj
xref
0 16
0000000000 65535 f
0000000061 00000 n
0000000112 00000 n
0000000219 00000 n
0000000331 00000 n
0000000436 00000 n
0000000641 00000 n
0000000846 00000 n
0000001051 00000 n
0000001256 00000 n
0000001325 00000 n
0000001618 00000 n
0000001696 00000 n
0000004107 00000 n
0000005985 00000 n
0000008229 00000 n
trailer
<<
/ID
[<f588e83396de72a40617a65dfbfe026a><f588e83396de72a40617a65dfbfe026a>]
% ReportLab generated PDF document -- digest (opensource)
/Info 10 0 R
/Root 9 0 R
/Size 16
>>
startxref
9046
%%EOF

View File

@@ -0,0 +1,283 @@
# Les 18 — Lesopdracht
## Magic link login + protected home page
**Duur:** 30 min in-class
---
## Doel
Werkende Next.js app met Supabase magic link login. Niet ingelogd → redirect naar /login. Ingelogd → home page met "Welkom {email}".
---
## Stap 1 — Setup
```bash
pnpm create next-app@latest auth-demo \
--typescript --tailwind --app --no-src-dir --import-alias "@/*"
cd auth-demo
pnpm add @supabase/supabase-js @supabase/ssr
```
`.env.local`:
```
NEXT_PUBLIC_SUPABASE_URL=https://...supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
```
In Supabase Dashboard:
- Auth → Email → enable "Magic Links"
- Auth → URL Configuration → Site URL: `http://localhost:3000`
---
## Stap 2 — Supabase clients
`utils/supabase/client.ts`:
```typescript
import { createBrowserClient } from "@supabase/ssr";
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);
}
```
`utils/supabase/server.ts`:
```typescript
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
export async function createClient() {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() { return cookieStore.getAll(); },
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
);
} catch {}
},
},
},
);
}
```
---
## Stap 3 — Middleware
`utils/supabase/middleware.ts`:
```typescript
import { createServerClient } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";
export async function updateSession(request: NextRequest) {
let response = NextResponse.next({ request });
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() { return request.cookies.getAll(); },
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value }) =>
request.cookies.set(name, value)
);
response = NextResponse.next({ request });
cookiesToSet.forEach(({ name, value, options }) =>
response.cookies.set(name, value, options)
);
},
},
},
);
await supabase.auth.getUser();
return response;
}
```
`middleware.ts` (root):
```typescript
import { type NextRequest } from "next/server";
import { updateSession } from "@/utils/supabase/middleware";
export async function middleware(request: NextRequest) {
return await updateSession(request);
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};
```
---
## Stap 4 — Login page
`app/login/page.tsx`:
```tsx
"use client";
import { createClient } from "@/utils/supabase/client";
import { useState } from "react";
export default function Login() {
const [email, setEmail] = useState("");
const [sent, setSent] = useState(false);
async function signIn() {
const supabase = createClient();
await supabase.auth.signInWithOtp({
email,
options: { emailRedirectTo: `${location.origin}/auth/callback` },
});
setSent(true);
}
if (sent) return (
<main className="p-8 max-w-md mx-auto">
<h1 className="text-2xl mb-4">Check je email!</h1>
<p>We hebben een magic link gestuurd naar <b>{email}</b>.</p>
</main>
);
return (
<main className="p-8 max-w-md mx-auto">
<h1 className="text-2xl mb-4">Login</h1>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="jouw@email.nl"
className="border p-2 w-full mb-3"
/>
<button
onClick={signIn}
className="bg-blue-600 text-white px-4 py-2 rounded"
>
Magic Link
</button>
</main>
);
}
```
---
## Stap 5 — Callback route
`app/auth/callback/route.ts`:
```typescript
import { createClient } from "@/utils/supabase/server";
import { NextResponse } from "next/server";
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url);
const code = searchParams.get("code");
if (code) {
const supabase = await createClient();
await supabase.auth.exchangeCodeForSession(code);
}
return NextResponse.redirect(`${origin}/`);
}
```
---
## Stap 6 — Protected home
`app/page.tsx`:
```tsx
import { createClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";
export default async function Home() {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) redirect("/login");
return (
<main className="p-8">
<h1 className="text-3xl">Welkom {user.email}</h1>
<form action={async () => {
"use server";
const sb = await createClient();
await sb.auth.signOut();
redirect("/login");
}}>
<button className="mt-4 underline">Uitloggen</button>
</form>
</main>
);
}
```
---
## Stap 7 — Test
```bash
pnpm dev
```
1. Open http://localhost:3000 → redirect naar /login
2. Vul echte email in (jouw eigen)
3. Klik Magic Link → "Check je email!"
4. Check je inbox (en spam!) — magic link mail
5. Klik link → redirect → home page met "Welkom {email}"
6. Klik Uitloggen → terug naar login
---
## Eisen
- [ ] Supabase project met Magic Links ingeschakeld
- [ ] Niet ingelogd → redirect naar /login
- [ ] Magic link email komt aan
- [ ] Klikken op link logt user in
- [ ] Home page toont user email
- [ ] Uitloggen werkt
---
## Tijdsindeling (30 min)
| Stap | Tijd |
|------|------|
| 1 — Setup | 4 min |
| 2-3 — Clients + middleware | 8 min |
| 4 — Login page | 5 min |
| 5 — Callback | 3 min |
| 6 — Protected home | 5 min |
| 7 — Test | 5 min |
---
## Veelvoorkomende problemen
| Symptoom | Oplossing |
|----------|-----------|
| Email komt niet aan | Check spam. Productie: eigen SMTP via Resend |
| `Invalid redirect_url` | Auth → URL Configuration → Site URL match |
| Callback URL 404 | Maak `app/auth/callback/route.ts` aan |
| Loop tussen login + home | Middleware werkt niet — check matcher |
| `cookies is not a function` | `await cookies()` in Next 15+ |
---
## Klaar?
Verder met huiswerk: voeg RLS toe + tasks tabel + multi-user. Zie `Les18-Huiswerk.md`.

View File

@@ -0,0 +1,137 @@
%PDF-1.4
%<25><><EFBFBD><EFBFBD> ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 4 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
5 0 obj
<<
/Contents 12 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 11 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/Contents 13 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 11 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
7 0 obj
<<
/Contents 14 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 11 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
8 0 obj
<<
/Contents 15 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 11 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
9 0 obj
<<
/PageMode /UseNone /Pages 11 0 R /Type /Catalog
>>
endobj
10 0 obj
<<
/Author (NOVI Hogeschool Utrecht) /CreationDate (D:20260521113057+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260521113057+00'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Les 18 Lesopdracht) /Trapped /False
>>
endobj
11 0 obj
<<
/Count 4 /Kids [ 5 0 R 6 0 R 7 0 R 8 0 R ] /Type /Pages
>>
endobj
12 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2109
>>
stream
Gb"/'D/\/e&H8h>E@Xl8m:M)+U-s.>d'QNV?toHMVqlk'JLWI4)Hh)+\`lr_^[F]bPaj^Ved^<!`doF6jMO(M\`Z_"5k2bYG;98F3e=CL$lFcA#r&"H0\M)LM0Inj*q7OC5_PjUi]n\rrh)CHEf^;.+s&I81aFRC_,lRD'e/'#0+:thV1t1(*Wm#Z1fQXM`FPWnC\%]936.kD,(h#L3kr'u)=o$b[+:q=bs-g=#hD&u:k_[nPG*ZO$qC,thX/0u`Mp#'Z_GP!YV('=<l5O4+`-V-\#V!\C-@:6qNK3^q+TuE6^W7SaWn.C!8ls'iK537j6u4>Qiao7o4]((kh#?=R`E2J=Dr.c40X7E`Wh$W0TlL`A*p.?!R@0[IOL\VVN>42,+Jl"al!:'-$\;W+Rr]h0gVb&bfoJO![9rHJO<ouC.>bQ>Yj>g*bSfkR-UG/)XS>64Gm`]pt^sK0;]P2kad!De:]9i"o?N,0#A5kiGSR;^2nje,#gqeGt=m.;Y@cqW#at8F$7Nef5SI9*nmC:I^+Ci])e*`18\1B(_T]O#ih`h1\5=T@I"*"&X\)7@du?c"t[bBB;+5b68XhQ0H.7L13Z"FjZp%J_5pUl[EoMB3&=#Hnk4po"L9jKLTJ:QJ0/3+(:?n2bafJ)%J0?oDi>=I%ZSnEPRIf,R$XLe""l]N+*SZUQ;METGOj>0a3CI*'Qg;L?PXAeQZ9q0C("K.EqCtJJ%T=ad&IAnAGj1o2I"2XX@g[ZTbH>E_t1jA]S.$VjFb6u:%LfUF%GnBP/UFDD5B0m<ck5k6V=l*J@KXRrIBF/!S;D:gL%5WdY<d6)>HMI6k)U;*3?#u+%Z]^iSFO,%9)Tf6`)TVOrBK7SX[Q/F9n]dE&qNp,EP5DlOXjuhaGtF-8@<u[>[lU4U<O!6c]',kMde'&Zscp;lDuSn07BWD$MoTeCp>+)8(G%\T_EDdqc[ZLdm9]SmeEe'G;$9.ArH$?srsN)i`.u3inDGXQ@\>=&@Y5/.G$!*NbdcSG&&>L!D5hb^+^og(=o*SU*Xmmr`o9c>)"`T54#&Jp9J7RBGLV0onQ;Q%ms,2'T8siq"uk`@*g=_Y=r"C*2dPqQ*F8:e0KlKD.Dg04s]Bcf-eYqLI@mgqF+dL`-4^Er3S5GMMsf'nO0l<l,CF!0\WSM<<C^@>_pT6ufoSO/mGgOm7H-HXo&^nQ[0m@,A/VhK<6\$+/PtA"'Z4S,lncJd-DmRE+eGlQ9"07,9=>=3BBGg)J^?G&@^NDc>lM'Z_h,YS^4fnruufL3\Y"Y#(Li9qiP,C4nJ!I.]P9[j`*YSeZOtkG65F9DAa74NE48`?+5h1#J5enApH?0g&hXDc,fT[1??bn*:!Q,_]<7"B4bc1U6E6onQA9L;P(_N>[sU&l439b(lgW<.o;iK+;-Um3$%mlTqJ`@Aj_e20_fA0\W^AfV!O4BQ%ZJ^\=V??-*:%8F0d4<Fk<.'nNY?3,d)WT$M&2PWGs)jJXA7'Tsg][]18*7=@(cl<+/JEW+K/h^cik?CM-K_.AL-4#XuX^I*+DESO"SVIN\#VEO/>Bu@g1g)*V`n>'gLD6(8dU\#"AX)]H8n?/P?5>Ll>kFY:G,]SRH?)P&j@2iK-g::]-dbO'Ak+JRogd;CNbkMm]q!d\-FTK0n,a/A?N.9Hu44QcK(V7[l?0q,=egOYj<h"?WUX2lt@KfiF8u#n_Y]df@PP?-Og-Tm-`1g#5ap$3o@g?^")3c&\k%f#g?%15`MSTEBH8[_Y!K2%rND,8+\cqVlm"k.\-N]%hk.\V.qZ.>\QVt=]^s%_7)<<>f4XLcNL._ZPL\>,_?gE^c!Mbg^5[+/G[^KQq,ApOs@;E@W-(W;=ZA(#L8pOSUQcPG)((%^l8Vr_n]d:M/p[`]UgjC8nN')d.Gne!fpqBY])PKB",>K#";Put?g<pgX1cfpg?e-kZ6Xj*'(&S@,NM'UBq'qGc7dGVQP]?<D8:q$,kC>s%Pt`M#4+Bqa?QVJfgO46mE;4bVl1Vs=0rB^JIThfCPeCu=]"S,Ys7l>Z5;s`=ASQajLDs!#R%h?5dP#31>/:().IBl/R^e9?-CU1m17_/rI2SBp^N"i!2o6Lq%f~>endstream
endobj
13 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 851
>>
stream
GasIegMYJ*&:O:S%!bGp)"4X%g'U+X+=^CQ(3mY$)/#-O`HH!7H;WFHBC33P_PN*ah<\\7baC8nVEk&HgGR4V?;2Sj97\jcc@L#s0Kk+Ke)2E8-CNip0[_4%%+%De#HC3hJ<2/g1DDZCW"fQ?.;E]V0H[2,<DZhc*s<9S8YS$He5Ssjs,)k61?MMV'N1Aaj#__$Tk@2Flo`P;f7S>Zg(i/57N;^L9#*9=n_*fC-h+9DfK)/Kb9_OUPtfRF`/H-)5'_<Vf3.M-$sq[]].@$o/_gK+^L2B7jP%K6N$C,`mGabl)Q3kTQ)e&Qo["\C791\L[]hrA>p$j-lc9E#;/i^TRKl@i2:@E6mi1d9d/pL,1?%U)npi6u&\PS)"0P3/310QF80j_(D2Wl48V>kh"[sU<D4u8[j?"P1GQ=X.Cn,eU]u<>JLLi_JBta0u)8O=T.h0QbB'Bghf.MNNB)%buk<V18c*J:>n8/<*SRR2^S!0u2))R:-kpKc3;3JDj$U`"AfkbB+P00N"+Wa]rl/Kh"qqkm[KgZO%@V#;7X;p#I]3/8rZe+W/]ia%C_*-#C15A`cD..:^93[C9p)k$"<!p?t]O&hqB0TTbW+FAo^'rUG>Lm5r?6qh"g4hsDqpH>=#%;VCk5Sb:"JU&n)`%,u-'Qh;71/X/Oh-:jOO>Yg.Jn'gK6RI,O&M/F3S0_-8\j50\#p'/Y*.#rf%h^0-[Vj,WkD38KtMojQOB%?dZ4bJ7AqEMoX5<F=k)h"cPH"t?^8jmVWPSC<LG3e4h&rbHlIe03$,hZnmaI$/ob$?[lIuEZESQEj>-=0cnFnl&)t^pqpR^QmIn!bURek)"MXtM*->c~>endstream
endobj
14 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1707
>>
stream
Gb"/'?#SIU'ReT:\-r#g'HKnG'M3F=YqBn&!a49Sme/=FW2_P[)Hh),WpLAm^[LI*PjANM$4PT\PG")BH-a/L%s\Z-JDr4trf'tZ5L3a6S,qY(])X16JCuA6Rs?)=_Bi8-!9.eZLE&kKf0<(_]F4j5AB7+BROY%s!21:X3n1r":FW060Q?Qt@mH9S8dcc1"]ENZq*'q2@qe41.Du,^Y!64$(D>DDLMaJL#_jR+2^sOI$US!h!gp)to((<Q0AtH0@ENh_k:+'OKo)-3k+!rHo&>EPAXSe6SAS:sae\`YAMST<ZO\a::tHWD$SD\qL"aL12'jpcprM(M7HC+K7uJXN^`Qt_NpF7[PIt[3;,)G1.bp(h(Z"\&EO7BpOKe;#5h]9U90KE`V*Y]%>^)qG@7p*!,hS-64Vu!PVAZG.>U[O1@/pelBC-Pf^bua6KA6.MA,%YA^SD?q\gjsTq^teEi0s"qh)#<lO1hCAn>cJ)[t"7ek"e?P8T[Ib_6u'j\7XM$L:Fr-FL,gnYV1[;bImipi6)<h,lB^;i$;\REd%cgNK)L3=t6')8-N*c$St6VJ]ea"*Ah;7'OgM7\S"QmL(&=f#]Inh4J>`$qHPTEKVim$+*'kN&h9gu7eCjtm.CTd@6UI!JT<J?I*V$;hLZtTgHhtE(I>#!6d5k4p#'W>K@.D69aTY%JbHd#q>G.+f,H0t6eL`B]nm?2`gh#oR*^N_X?FT/<G<RN'0Q7!5Mgbg\3+,]qH9^BX*%`N\:V4EOYnKq!,USfaB*?Me$QDP>#*7U*PIM+1VI:LqPN4.fWBZ5,-.ebkZBP\a'uR(m2U$^6n&^)'MUmbV($Y,/tsi'E)C2O:%GB^nU*)VKWEj#19F,u>]%gfi]J_Cr+/o+P!4QIfOsm3hf.po1nPa-bR[2jJsQQG67k7JW;J$$qrZ%3OhJ8/G>^8qXi&ic\(r6ECUfWei`K[k7s6B@!2Ik;dK>Z2Z:iX>pLds%YO$R!)=(-BEAd'/"DF-?,Rs4n:e<B4bLcFXP`Lnj%DnMn:J_&Y/:Rfk^;!;333!4F]L[!c2q,@kBcNu6q^B1+#FhuU%#%!p^<t4#Em]Ck)%;f]2$L/S_9(-DQghhuNIEh3LsX1(TP)4*p<e,Cq16=s]cCAq>88%"-b!a1O9hO;GMO<KF4oU.P6+2<8o1^B7nL.l/:<?'arFbG1Yb6eSLgOPJV!X8+ENSc]AnGC@\[C@/89.eLDuV@PSF4]^='p!mji2DFC:e!M(apn=rM9"s+]qeL=7E[4IW]g7^.<5.%g)Pr+LD3L#AmL[@uol`cne:f#'FV@d.[Ym*.KPFK`t=\.@a:[*Ip3]83-$.m*$f3#0,lra4Ip6_C-MA4-+0<..arhmk,3F?@Qdi$,RUEZ^fuEY7#XdD"0p];H\&Rnb,1YBSN,g2%\4j]?-O=5'Y%'VA&RAq>,9d8bF*^=Y5?Y#-3d99irTY,DHe];+)&c>(\:r:4Y^T+pEtAP2h*cJ"V)+1%koXc'_V!Ju\Yq)U""<V[20=i]bI6Y8O"gTsg[MH!#*H6Ma6Am6P+cYK",X@,m`:r;O:`cZW(;j.SgiAu%-cI[aBRWiP\f2k#CIGqi\^mQiqUX^aEkjuH3A',ECDqk%XJN`ZGE@Q=8ff><@?V/Yu\G68%gt'*Nqeu.m8aN4<Rr@kDO7Fdh6XF?uIoJjSO[S=OQMG8s?GZLT@9]kR~>endstream
endobj
15 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2004
>>
stream
Gb"/'lYkN9&HAoJi`;^;q?TenPXoq"LTYFEDFn2T>$qUiW%o$eQ6fUqRheHmpDk]>OqU'K!m@L\8L2]rhnAK'@R0J$m/LT+nCndL:Dkhrj"k(p#T+Z#PHjl0,<Y$b"@.?'rJ1Q4I.II%\X,YpOtbl-"%=M5;LhT&-l"2h81,^I:hhu%lS0M;Uf%LAM?T[\YL*.lj;_;1&qj\8RD+q^)=\m`[aLk:bqF\-%b<,k:jkVX$Y07t3UFG(q=Zg2/'`:&9T(*IPIf8c["_N),s6Lfs/;sGWn%jY?%::*Z'aji^'!C+0YV=I%IaN;KL\A3iZIFmQiao7Mb+dJkZ@;"R`?NT=Kg,T*E^Be@7*7'/.;[X_]t,G-q'i@CL_a%1_6]^G_#)lbWR+\@rSoCGsY@qfIo7gKS0[^LF]CZPB^u)TehAj1CN<q!YPk!6O1o`j*$WM^o\_Ys,_Mb8>QMtdiOJA]Fp;!_IWH"A8URXD>N#*RsI!(bbkc!d8M,2)o\&?2GOIjMb(c(%fu@o"(Z4R2]=up=H"$c\L)IlH+hfkatP#+.)dd7rk'Z%)CG;3?=l(5eX&CLZXo`XBouo4c1U>XkoKQ1*AW^%YXU=.!(i;S2,qafr'%8?quZCWSCWdd3j//9/B,C]W'>2*'kh)aWYNa]0T#,j!FD+X0&[au3qSFQM3mn"SEp"SGC0W7h^?k8]]53I_<KfP"s)5Hod%L%6YUIuZ3&M5ekS2bnRaPkKe%;Z/9<e<a(bPB@*0P)e(Lgi_cOa,BO&is*K4t9?t?b55jti[r7eB-$mMbt,*4]+[o>Z>a(hGu&W.,5Op@enF_dDc-)";:_&?DB/jYj5q/@YDp*?56_],J?8Z$qc_L58]%bkX@*[a4^OJ%-[]u``NYl7;\J>hL;;DIC3VnJ0DYu>/*q:\e)"l8<M\1SRP6b-[]#DTb&$CVfp"2>DuR/-;qJ0.tmM"%J?Js3'*XPc<7/"LVJ^$?Z*i6M'=4_hb"Ql<dm;sp)b8$;o&_iegBR"9p(=sK52UqsEtGOLbf\K^$089A.QcqsmOd/7b]B<0t2?"'/e)NM$`-#f76ZO5.92TL'>ZcP])$Db0l'mj9=;Xm25WYor`l:bUc+Z/<$'r3G8"l[^?fCQS>G)ag'oGQ9g#Zq\DpVckC[=!+<CA#6BV:K!V3h,@RMFF9Z]$*-RMc-)%S5MMRCFNhT!+69P:_uQ,2gs;(D4=ijW7DJnFct>>STAo(Ke@">e*6NOG-7_M&JEQCs)4kqfi$@Mgf!$ESUDuF''kY)-Q);`nNPFnh/1`@]F0K8P9r/^@6_3eF5\IheiIsbB'GaI,VgSll?_^K;FTP>@**lu=/_\"I(N.E7omqT"+O_VZq'TF0Co@5R/-R\0GDkSds\_(jWZ3oi6BRQq_'%4U+KVa1\<lqqbM*UmV3D=.mQaQ]SPa49Q`,&DrP?6`0!VU!1fRYEdT]m[3!@^DO19Ir[H.@M/2AU/B_i7l^%*$XYlW]3Q0bUO)Wa&9%Pq(U>!O?BVpp4EL+;!PF86^<:&Po;B<aHbYFL:Xr8T<`i(GS[2Ck]>Ki%,c&7:mF@:J)fnV#RfFGnQfhjeuAGRLiOKq=/3p;$&EgXm)hNXKh7i!6sMHQtL>Za9uTPrXo]0h+'\fFrn$p+Wf!\a3/)H^rsJ7-O:TD8-]9,!?94r+,NQ14/k,D@EEhYc>p!%qN2chj4eNu8pUDE'bQ7&oAd\[DW\?b$Uf^"`"`Z#^pqr;mX5OC[Um]VttU=,%/k_c=d6KRp=WY.XK!#Lt)MUjis(&@n\EeIt):^jC19:QQ>F=c8*FZm27K.D\Z-?dj0co:K2H':?YK]9%BKYo6usKg[aq<.Gj?"0CQ/lJF02J75:lLSc8DpB6/oM7$uRQ#d)7g!C]\Ce8+@?usFKa2'_fJ$ji:3/V=_4/RN7T3`5u60/61<bA4s0%7srqu_1bGRX>n!YnHhBEt*edsRS6l+VRq!.Up#]Wbm\s&hu^(6^P[iB.5!^Ur=LTE~>endstream
endobj
xref
0 16
0000000000 65535 f
0000000061 00000 n
0000000112 00000 n
0000000219 00000 n
0000000331 00000 n
0000000436 00000 n
0000000641 00000 n
0000000846 00000 n
0000001051 00000 n
0000001256 00000 n
0000001325 00000 n
0000001621 00000 n
0000001699 00000 n
0000003900 00000 n
0000004842 00000 n
0000006641 00000 n
trailer
<<
/ID
[<2b2591697dfee3e383d5e69da893bb63><2b2591697dfee3e383d5e69da893bb63>]
% ReportLab generated PDF document -- digest (opensource)
/Info 10 0 R
/Root 9 0 R
/Size 16
>>
startxref
8737
%%EOF

View File

@@ -0,0 +1,542 @@
# Les 18 — Lesstof
## Supabase Auth + RLS — multi-user apps in productie
**Vak:** AI-Assisted Development
**Vorige les:** Les 17 — Externe APIs in diepte
**Volgende les:** Geen — eindopdracht thuis
---
## Inhoud
1. [Auth vs Authz](#1-auth-vs-authz)
2. [Supabase Auth — drie methoden](#2-supabase-auth--drie-methoden)
3. [Magic link flow](#3-magic-link-flow)
4. [Social login (OAuth)](#4-social-login-oauth)
5. [Session in Next.js App Router](#5-session-in-nextjs-app-router)
6. [Middleware — sessie refresh](#6-middleware--sessie-refresh)
7. [RLS — wat en waarom](#7-rls--wat-en-waarom)
8. [RLS policies in praktijk](#8-rls-policies-in-praktijk)
9. [Protected routes + server checks](#9-protected-routes--server-checks)
10. [Eindopdracht checklist](#10-eindopdracht-checklist)
---
## 1. Auth vs Authz
Twee verschillende vragen — vaak verward.
- **Authentication (Auth)** — "wie ben jij?" — login flow
- **Authorization (Authz)** — "wat mag jij?" — permissions
Voorbeeld: jij bent ingelogd als `tim@example.com` (auth). Dat betekent niet dat je een admin task kan deleten (authz). Authz checkt: heeft deze user de juiste rol/permissie voor deze actie?
In Supabase:
- **Auth** wordt geregeld door Supabase Auth — magic link, social, password
- **Authz** wordt geregeld door RLS — Row Level Security policies in Postgres
Geïntegreerd: Supabase Auth zet `auth.uid()` beschikbaar in Postgres, RLS policies gebruiken dat voor filtering.
---
## 2. Supabase Auth — drie methoden
### 2.1 Email + password
Klassiek. User registreert met email + wachtwoord.
```typescript
const supabase = createClient(...);
// Signup
await supabase.auth.signUp({
email: "user@example.com",
password: "geheim123",
});
// Login
await supabase.auth.signInWithPassword({
email: "user@example.com",
password: "geheim123",
});
// Logout
await supabase.auth.signOut();
```
Voordelen: bekend, werkt offline (na login). Nadelen: wachtwoord-management (reset, lekken, weak passwords).
### 2.2 Magic link (passwordless)
User vult email in, krijgt link, klikt → ingelogd.
```typescript
await supabase.auth.signInWithOtp({
email: "user@example.com",
options: {
emailRedirectTo: `${location.origin}/auth/callback`,
},
});
// Email is verstuurd. User klikt link → redirect naar callback → sessie actief.
```
Voordelen: geen wachtwoord, secure (link bevat one-time token), modern. Nadelen: vereist email-access (geen offline login).
**Aanrader voor demo + meeste apps.**
### 2.3 Social login (OAuth)
"Login met Google", "Login met GitHub".
```typescript
await supabase.auth.signInWithOAuth({
provider: "github", // of "google", "discord", "apple"...
options: {
redirectTo: `${location.origin}/auth/callback`,
},
});
```
Voordelen: geen account-creation, vertrouwde providers. Nadelen: vereist OAuth-setup bij provider.
Supabase ondersteunt 20+ providers: Google, GitHub, Discord, Twitter, Apple, Microsoft, Spotify, etc.
---
## 3. Magic link flow
```
1. User opent /login
2. Vult email in, klikt Submit
3. Server: supabase.auth.signInWithOtp({ email })
4. Supabase verstuurt email met magic link:
https://your-project.supabase.co/auth/v1/verify?token=...&redirect_to=...
5. User klikt link in email
6. Supabase verifies token, redirect naar je app /auth/callback?code=...
7. Callback route: exchange code voor sessie:
await supabase.auth.exchangeCodeForSession(code)
8. Cookie gezet, user is ingelogd
```
### Callback route in Next.js
`app/auth/callback/route.ts`:
```typescript
import { createClient } from "@/utils/supabase/server";
import { NextResponse } from "next/server";
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url);
const code = searchParams.get("code");
if (code) {
const supabase = await createClient();
await supabase.auth.exchangeCodeForSession(code);
}
return NextResponse.redirect(`${origin}/`);
}
```
### Custom email template
Supabase Dashboard → Auth → Email Templates → "Magic Link". Pas onderwerp + body aan. Variabelen: `{{ .Token }}`, `{{ .ConfirmationURL }}`.
Belangrijk: voor productie eigen SMTP setup (Resend, Sendgrid). Supabase's default SMTP is rate-limited en sender-reputation onbekend.
---
## 4. Social login (OAuth)
### Setup GitHub
1. GitHub → Settings → Developer settings → OAuth Apps → New OAuth App
2. **Application name:** "Mijn App"
3. **Homepage URL:** `https://yourapp.vercel.app`
4. **Authorization callback URL:** `https://your-project.supabase.co/auth/v1/callback`
5. Save → kopieer Client ID + generate Client Secret
### Configure in Supabase
1. Supabase Dashboard → Auth → Providers → GitHub
2. Enable
3. Plak Client ID + Client Secret
4. Save
### Frontend
```tsx
"use client";
import { createClient } from "@/utils/supabase/client";
async function signInWithGitHub() {
const supabase = createClient();
await supabase.auth.signInWithOAuth({
provider: "github",
options: { redirectTo: `${location.origin}/auth/callback` },
});
}
// Render button
<button onClick={signInWithGitHub}>Login met GitHub</button>
```
Klik → redirect naar GitHub → user authorize → terug naar je callback → ingelogd.
### Combineer providers
Same app kan magic link **én** GitHub login hebben. User kiest wat hij prefereert. Supabase merged users met dezelfde email automatisch.
---
## 5. Session in Next.js App Router
Supabase Auth in Next.js is iets complexer dan andere libraries. Drie redenen:
- Cookies moeten gesynced tussen browser + server-side rendering
- Sessies verlopen — middleware refreshed automatisch
- Server components hebben aparte client nodig dan client components
### Setup
```bash
pnpm add @supabase/supabase-js @supabase/ssr
```
### Vier files
**`utils/supabase/client.ts`** — voor Client Components:
```typescript
import { createBrowserClient } from "@supabase/ssr";
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);
}
```
**`utils/supabase/server.ts`** — voor Server Components / actions:
```typescript
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
export async function createClient() {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() { return cookieStore.getAll(); },
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
);
} catch {}
},
},
},
);
}
```
**`utils/supabase/middleware.ts`** + **`middleware.ts` (root)** — zie volgende sectie.
Supabase heeft een **template** voor deze exacte setup. Niet zelf uitvinden, kopieer van docs.
---
## 6. Middleware — sessie refresh
Sessies verlopen na een uur (default). Zonder refresh moet user steeds opnieuw inloggen.
`utils/supabase/middleware.ts`:
```typescript
import { createServerClient } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";
export async function updateSession(request: NextRequest) {
let response = NextResponse.next({ request });
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() { return request.cookies.getAll(); },
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value));
response = NextResponse.next({ request });
cookiesToSet.forEach(({ name, value, options }) =>
response.cookies.set(name, value, options)
);
},
},
},
);
await supabase.auth.getUser(); // refreshed sessie
return response;
}
```
`middleware.ts` (root):
```typescript
import { type NextRequest } from "next/server";
import { updateSession } from "@/utils/supabase/middleware";
export async function middleware(request: NextRequest) {
return await updateSession(request);
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};
```
Elke request → middleware draait → sessie auto-refreshed → user blijft ingelogd.
---
## 7. RLS — wat en waarom
### Zonder RLS
```typescript
// Client met anon key
const { data } = await supabase.from("tasks").select("*");
// → Returns ALL tasks from ALL users — disaster
```
Met alleen anon key in browser kan iedereen alles. Niet acceptabel voor multi-user apps.
### Met RLS
```sql
alter table tasks enable row level security;
create policy "select own" on tasks for select using (auth.uid() = user_id);
```
Now:
```typescript
const { data } = await supabase.from("tasks").select("*");
// → Returns alleen tasks waar user_id = auth.uid()
```
Postgres past de filter ALTIJD toe, ongeacht wie de query stelt. Sterke garantie.
### `auth.uid()`
Supabase helper functie. Returns:
- UUID van ingelogde user (uit JWT)
- `NULL` als anonymous
Te gebruiken in:
- `WHERE` clauses
- `default` waarden bij INSERT
- Custom functions
### Hoe Postgres weet wie ingelogd is
Supabase JS client stuurt JWT mee in `Authorization: Bearer <jwt>` header. Postgres parsed JWT, extract user-id, beschikbaar via `auth.uid()`.
Magic onder de motorkap, voor jou: werkt gewoon.
---
## 8. RLS policies in praktijk
### Basis: per-user data
```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
);
alter table tasks enable row level security;
-- Read
create policy "select own tasks" on tasks
for select using (auth.uid() = user_id);
-- Create
create policy "insert own tasks" on tasks
for insert with check (auth.uid() = user_id);
-- Update
create policy "update own tasks" on tasks
for update using (auth.uid() = user_id);
-- Delete
create policy "delete own tasks" on tasks
for delete using (auth.uid() = user_id);
```
`using` = check tijdens read (welke rows mag user zien?)
`with check` = check tijdens write (welke rows mag user maken?)
### Variant: shared data per team
Voor team-features waar users elkaars data zien:
```sql
create table team_members (
team_id uuid,
user_id uuid,
primary key (team_id, user_id)
);
create policy "see team tasks" on tasks
for select using (
team_id in (
select team_id from team_members where user_id = auth.uid()
)
);
```
### Variant: admin role
Voor admins die alles mogen zien:
```sql
create policy "admins see all" on tasks
for select using (
auth.jwt() ->> 'role' = 'admin'
OR auth.uid() = user_id
);
```
Set custom JWT claims via Supabase Dashboard of code.
### Best practices
- **Begin met `enable row level security`** — anders heeft policies geen effect
- **Default-deny:** zonder policies = niemand mag iets
- **Per-operation policies:** select/insert/update/delete apart
- **Test in incognito** — eerlijke check zonder admin-context
- **Hou queries simpel** — complexe policies = trage queries
---
## 9. Protected routes + server checks
### Protected page (Server Component)
```tsx
import { createClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";
export default async function Dashboard() {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) redirect("/login");
return <div>Welkom {user.email}</div>;
}
```
### Protected layout
`app/(authenticated)/layout.tsx`:
```tsx
export default async function AuthLayout({ children }: { children: React.ReactNode }) {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) redirect("/login");
return <>{children}</>;
}
```
Alle pages onder `(authenticated)` zijn nu beschermd.
### Protected API route
```typescript
import { createClient } from "@/utils/supabase/server";
export async function POST(req: Request) {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) return new Response("Unauthorized", { status: 401 });
// user is ingelogd — handle request
}
```
### Logout
```tsx
"use client";
import { createClient } from "@/utils/supabase/client";
async function logout() {
const supabase = createClient();
await supabase.auth.signOut();
window.location.href = "/login";
}
<button onClick={logout}>Uitloggen</button>
```
---
## 10. Eindopdracht checklist
Wat je voor je eindopdracht moet hebben:
### Technische stack
- ✓ Next.js 16 + TypeScript + Tailwind
- ✓ Supabase Postgres
- ✓ Vercel AI SDK met Tool Calling
- ✓ Minstens 1 externe API
- ✓ Multi-user — Supabase Auth + RLS
- ✓ Deployed op Vercel
- ✓ Code op GitHub met PR's + CI
### Feature must-haves
- Login flow (magic link of social)
- Per-user data (RLS policies actief)
- AI feature met tools (chat, agent, RAG, etc.)
- Externe API integratie (geen alleen-OpenAI)
- Productie URL die werkt
### Documentatie
- README met setup-instructies
- ENV vars template (`.env.example`)
- Beschrijving van je AI feature(s)
- Architectuur-diagram (mag schets zijn)
### Tijd-indicatie
40-60 uur thuiswerk. Verspreid over 4-6 weken. Niet laatste weekend.
### Inleveren
- GitHub repo URL
- Vercel productie URL
- Demo-account credentials (test user)
- 2-5 min screen recording van app in actie (optioneel maar aangeraden)
---
## Bronnen
- **Supabase Auth:** https://supabase.com/docs/guides/auth
- **Next.js setup:** https://supabase.com/docs/guides/auth/server-side/nextjs
- **Magic links:** https://supabase.com/docs/guides/auth/auth-magic-link
- **Social login:** https://supabase.com/docs/guides/auth/social-login
- **RLS guide:** https://supabase.com/docs/guides/database/postgres/row-level-security
- **RLS policies:** https://supabase.com/docs/guides/database/postgres/row-level-security#policies
- **Custom claims:** https://supabase.com/docs/guides/database/postgres/custom-claims-and-role-based-access-control-rbac
- **Auth UI library:** https://supabase.com/docs/guides/auth/auth-helpers/auth-ui

View File

@@ -0,0 +1,238 @@
%PDF-1.4
%<25><><EFBFBD><EFBFBD> ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 5 0 R /F4 10 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Contents 18 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 17 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
6 0 obj
<<
/Contents 19 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 17 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
7 0 obj
<<
/Contents 20 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 17 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
8 0 obj
<<
/Contents 21 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 17 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
9 0 obj
<<
/Contents 22 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 17 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
10 0 obj
<<
/BaseFont /Symbol /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
11 0 obj
<<
/Contents 23 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 17 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
12 0 obj
<<
/Contents 24 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 17 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
13 0 obj
<<
/Contents 25 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 17 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
14 0 obj
<<
/Contents 26 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 17 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
15 0 obj
<<
/PageMode /UseNone /Pages 17 0 R /Type /Catalog
>>
endobj
16 0 obj
<<
/Author (NOVI Hogeschool Utrecht) /CreationDate (D:20260521113057+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260521113057+00'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Les 18 Lesstof) /Trapped /False
>>
endobj
17 0 obj
<<
/Count 9 /Kids [ 4 0 R 6 0 R 7 0 R 8 0 R 9 0 R 11 0 R 12 0 R 13 0 R 14 0 R ] /Type /Pages
>>
endobj
18 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1812
>>
stream
Gatm;>BcPr&:WeDbY-8R02Zim+o7LBD`QR!]\OC=&<LC2N%!V[mB>CZ@),N)W7Q$LD$fHrq4sEs`XF90=T';C!FF(0q&9Ns_e]@t,qdEu_s<`^h'$$Dht;L*77G"C"N(.(ETL`h8Kn6X0Mp7Hb'1[%N$oDhi4.,-R1G"+`=n"-nKmjn$Uc^F.X>[A+Hge&m1N4;@"3[lT<dkg7(1sGS'=0Li<n/!l;31-.._fsAnsC%q!$Nn]JEX#N0_JZn5!'JBN`0AF!&]Q:X@0=.4ZFV.t+0kPHT+04bhWIbaqp@"+8_^`'a9Bn_`XFQid16/$ins5DN<3('5H5I).Vp!_M60.Kq&Gi!I&Sd0"S<mTVcUmkq":+iQ`430j3Zi:NiY]H9e&S8Q-0+Op"Mh:p7I,((.kLW3HCXQkae1QaJ<S3!dWDMIjuZo8a\'\u=M]j9NEfNF/!BBe>3';cH[aIQh>LcT2`T#U<1F+KFi=$uE2-WEr=p"Xi>Y3Lsg1R<AHkdYgmZq$n)Z2`))JpG:E9X11V9=W"Dba!%&fiO@DcekB)Bp/]&Mmq@pLg\GGjpNfW7i*WIK4+o7OaF@T_oo:6E42a3n.`UN^Xu88=tWo/(Lkl^5Q]641MGab-EWH[9rSL+o=TaaXru=b1D>OD#YAaYYY%U?\<.RShF(A<O^o=/Hn^W.Wc*"co4YY,;ZmV0GIueF6gMeI,=A);c%gN&\(sglU"P#sS,Zd9_>(>>mCB\K[=Xut$Wf!)S/utk@(=mRR>T74YohPu?#p^53f(`3BimRC@Oa@L$P$f'57t-Q'X7@8-@.P!4Ml^!_lc<B2^Z3>P<[9&WeikkFTVA5jM(Ujkp"%Cq0ZL8P!PW%][C;s%V(3WF;AX_on5DqBOd+S>HQY%e4CFF4>&s)332-f.IWZ%Z=gbl9b49N="&$7\_/W$M;'YmgFmr&Mj(WX'Im4;.t^:p;2*ugpW5&c*q6f5kYbnFL+9i0XdVWm6tqh=-H+IMP^;$5Q+@T!:T3*\_M.?3LtW@Vf'@E;o[/)rLLD7k%G3t"qsA%M_V4IcgIH%paN>!qT^A31S_!d0Y_7Lu5it2GbJ1rO">iphcOQ5l_ZE,UVNkgf8aXdRf2FbG.`c-\`402l..)Zu@s'<OKM<JeFb.';UTl8CMqI5mB6>J&ZYm+I$W4.uebaDCB>+nDc.L&_BiCYd-b!2%/hp&'PO&$S\1-b>?hQcf_)f5D*n3)!>QaPs^g4R;%P,uU`!E`u=f/MAOG@CmCPqnT77e[oPAKn#8eWLZ\d.u'>6'T2gnN6pdT)!'(j7"oO,@2/HTNip(eBXGbk]B8UjBM\KMU/4A.b5GO^)`1?*pLcfXQ&Bmi+.!KH"D.^aaY_L%K(@MdP%,*2iWk/G't`eoSo,RR@Hs(7o'!>i23P29`Y8d^CLkmJL4V$6p"<:16&GDMn3XA,.dac:qmkg=u7G+<7<,;Fdc%$Puc3g[f:H9p-)3-1S6A^/mdWT\Y4IT.`:".m%mUIq.t;q9H=Cfu3X-C8WnInq#'-H%_nY`Llu%q^TD&]fs$7rJ`Qdok/,r9oEjTh.m4+9c\opT0QsqlAqnA51R\5aeNef;`Mn:3:2RJ(`E&rADAVEg]K3C<tALY*94tu_TA:aM''f1A!:eb70(X(*55VH/B4V/7iLG&C'RAKLT\3Fm9s_.g.#UMk]/]#m5S#&:S;m(;V:PB;mPcmf#?At/BiEnd;ah+9ji(9:@&[BN5`F8=&j:S)W>cX]a,s(3`%I&>EKLMa3b2_V\Ik&a:)8ukZXbo9#k?sV0+bVd*';Y(AaWR%\3P!%bMdQ~>endstream
endobj
19 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1366
>>
stream
Gb"/&?#SIU'ReT:\0q^6BfDKV.$D^oL(%k&!83Mnme5Xq.^EM6PpJpu&th\mq^t<oOqYlR;`*5r/]3aSaE,,ATkFWE&(^Zu97YnH0RgoiEL<6u'1BXs#[U<>994&6PmDp8Ree04')L_mjkS$&O@4gcA>#nG=pYb/&ZnD0*9Sci'U/oo^[r?S-)o;oLh;4fo%nbd8C#Vj+G#d9h>(o8g4U7Tfg^a*g+S':#hC?a:jkVX$WI+i-&hgarqd9+.-=)908^s+pN;nBM]/I_.l]Ndq4Ob9W_R0qHZGZf`G1h%oc?dZ@.Mb@)W2">8V#d%iu]Q^0F`9,lJgU9Sj`0,O+7a[hI92@+N`A'("L7sWPVtX/\'tR@C.\`OKeA%5jY>RAKFp+;P==RY1lg*0I?PA&p.Vmce7f>BuURG1PIe@%)?<Kb`6ZDFG:V7m&i.*iD\;lU^i2A0<n%E-o/GT6+2t_)M?7DDrERD15B`Fpl46-6XE?+992B^(aWECmK5g]?O+/V)&r4%0O*\Ok5i;5n3b(]iu:Il&:(mBIrJC@i48u2rq$0q**K,tNWYUhD'!oH9`g;qmW@.,8#]?IAIEJ^O^a/ACe[53ZdJV_b*6';E>Tt00OWNekNb=)L$<$N9(lK7D'9he5*_71aGZNEJ[6VWMtH#\(Q]@[(c]j!Rg3H>KV'sU8#Q7KTmWURQI3@iGEnE9'Gm>bYO2Scp.eG%[?,qV0P00fA);#bn&$,?Y?'%R<$hEL$(==1U,7TUbs.5h,#4nL]S``SVC6)qZF:_EmcY@h4XWZucc`+.&([ope[u]Wc@;p2:UmRtn%>fG"L@r>GWdQSa318b'XK(BostN1fWjFV7t@(>D!TJJA+Y'bV&?ptNiouC):4HnTc6TZMTOi6`18J;)bT+%L2`X[dM#7+I>tRET$Yt\lqMBh,7,)a%5#.1'78"?"JuHh2c-&>'?]hI*I&.(K3SQ)Jl)E*@AVCun<ioW[.e-fWV[ZK+)<sQc_eJ485(,BV@Rb^01-@m`f](@;*O0/2UP6`ekid8+VBlm<9ZMp^RC0H6Xi+PPA;H!:A94j]`7943>Tm+'"<H7k5V7MihT"kS>5Mbc-Po[4WUXLD5<e<F\BJfjo1lid>$m6MXiNk-095e[flU?-(o4nqTh#A'i>CsB1!,!;f:.`RBYV,[=_]ec=/WgDO^p$L%\o]p[uHC&-7p-g+EELdc9%0@65:W'Ep(V^Hs:J`MtinNI>K:n1L-*pfFDc'`7CYq<+JGE=[4S2R/8%.*Ko1pu;Uhh;MnCOkVP9JT%",Eq!:eE8#,2:^"i$L%HYui7bkqIL+k],_2bA?R^P+=^7l7o'O?$3FZ6a4aXOWb3ptenGWS5Rd.<~>endstream
endobj
20 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1520
>>
stream
Gatm;gMZ%0&:N/3%"^Z&M_.Y$'M5,,g24FS#"\KOFkRWrXB)$326u:6EfqWf^NZUa/a$R^g";$*O2s-?E/'l;",_"tJ&cV\ha]@_B#$Bb-GT`r0KOp^,O`c">]M+l+MO_%aFT#YI6fU\1'f.f+V,H(Wea[WKP!X<:]na`8hgL'J]Sjl!E#"0/V%LM81bq.pPIrN>mh99)*XSJo$`n287]mRBf7%9_\&sbj\U@k-jruSUH&`HIerpErlh:;N+[F-_Kf.KGZMYJEs;#-IXY@a8QhGhpqp@+1geI$<pdfg?PpJ]^3]Q@E"X;,Fo50:!n2[dZH=O(m,r&NA.eT"S1hCL*K]>6;+3A4IB"t:)].<$EFH4-Ul[;)GX.^%B1kCj#c0l^Cg=HE"CXpn^*W!_`1F<o%F^8=-t`m,>Sr"/p/s2k&a0WX'(#$bO5X$GH:T%fF^AO<Y#qLp_i\a_3uL3CYrD:>S?hT1oYY1.4*C*dU>,>(KoAmG2<-kS`:\8Gl]Cf"6<LS<&$J-D=lY9j5K:/M_*ZB#2>k3Cr%Y!=FpX?h^Nh.6>e8q/E*7/AdJ@kGhFaa&F13s;q&6?=HHq9#l&iqb[e+</%;<H2#E5`=luNlJc1ISXBKjaMZZYX+dM.crE)4;(c#Hg8al%K5<ab&eP!Y<plPoNE=4,'4d77Q@n#In?9lU0pEY3_\8jf=a2'ZZfbtbQoP%s]i>)W]1Xk<O@rW]e,<Q+_qLt+0.g1gai>ZT$Zd04N-.[[i<h]Pe1MF=_%7/6rLHC6+HeM\l9<MTaZ8.e'-X^mT^nh&GBMZerF"^)NS0XRf?N2&6.*lXJ\`qb<K,ij5;=PR7PZeRk<$W!7e<X]]<Z8)YmDW=la9at3EE1Q.qPZXgsZU*ZQBN.',I%eJ@^J#&D4(G;"YdF4pP^&-^Z$2UB4S&@1JTo^l"NY(QF>d4:VpmUn=$NtVcRO.QrI>L*\?+F+7^7JQTBNH&HIm!E2O9"T"a9;?V4&ISfhtUZC+$/XXq?qP$EEq<h2R7SB4FOF/L@HkS.5?9V0qLBRF9>&a1tghCA#Jf5"b.tA%D`:.poa\d45ZJ`O28O;R<r_ZMF#6XM[;ql?tu3er.`RNj,Mnh-`3qBX([sqSP@1Uai!SHQ<T.N7KP]EK^,Aa7K<@ike*K3D'rhipU;RjE&uND7H[1ZlF6hEELe'Epo>H*j@d9Pn`O*5+:6kb`n#J_o&SIOJ-Yq<+\K_CYg""d=<ng1qq4Om5lftb@6hiZa`Wu$pM<GN<b0J>+2`XJ@"BM]FT4UrJ*SV0[pOei),`(<`Wq/,+a4CMJ/#?F'^-#3P'u5aNP*`T:J#EA6G_gr@K3JH2U"dNpSp-:VFY':Ln4?nR0[,g"Qi`6R%37]EM%!hp&s^go`Y6`3Nh=b`5Md%nJ1]M3;U*HcC]?KLA)$YI$Z65h::cm`[m^U;qQLeXlM\ee-g!_"W]V3U])cD][0n`3=_mgX1_SQ:86JYG<\Zj2&3sm[>9U&&2uCd9S?nrRuf0hZ$V#W@n~>endstream
endobj
21 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1807
>>
stream
Gb"/'=``=U&:WfGfLbt5,S!3^"@77cWU%KZB@:$cdYGCiTd`NA9G%U<3ULAThn-Pc6\eUjIAs%tTdBL$KtU$n`=FR#7rs2f7PHs!5T,M-5Qq`RH3qnec=_ndaaE%JiQC6_!tt+_&$i0uJgZB`=?o0_5#K#oTdWbf:r)Q\hkrYK)CCLb$H/:6Y65D*4fRM%k;eZ`+_sA+.>-s4fCGn-]R\(,3Y\ps+rKa@[8JS+(W</An%iZIK3,6@*5`.?eTiu--@tnLe'ZCV.:YO]*'ZI)o/O4*$r?PM:<[1M;u7+qT4%;ii75^]Je8O4]8HVdk<Kd?-+@Ja_7g5RE>jGmiOhUK2F*GUK)2VoS@^u]@]dHeNeUD980MF00TT2VQBjMS-a"t!O2b/49+^A]eu*c;T#bV[\=e`-!idOM\1d?E`TFP]dgFG5+r%7a-P",-[(GS25]4akEnjEI"FmdFedo6)oJDW!R8]]Vi`!C:GZ:3(o'f8GlJp95)0H6aP]<@PbE\ttQhC;DBP$>lf,hXWnH6XGiYNto#'b$hi1HAb%MMAAScUkl]KTHOg$-CKLI!E!4UdrAa@4iC('VaM)+l+^::+:Zi-B-G/C1!M%gS$3'aa^l[#lRf#BV`2jmC&#Kf)L=XA'>Po*PW(`EmtM=,:-jQAK6>a\!/!>K:!h$**G$BR1E)[KRSh`p#=8-U0HhY;=7HeCIgbY?1SD8s(g^m6k`uL"6^2RrB*5Vf0m60K9K)j=H88iVgBTqbXR9*q=R>?tSdMh]E>X?Y&;_NpIi5?S94%0\7R*_[u2h]V*0@``Q`+SX=tH@>8^lrCM7-$YQ'#7BFnmLImFr*fcl0j(<_6UJ0D]!kc2n8/nY%Ouj'&6^(:)kn5KfVMgYl".Nd89@:5f.Ta6$f.$CkPsSsJpF'YM*j7YNXGfI;fUL)i`+,@'Yulc8Y+DKK=gHaL(msaiWS$aPPDX:AnGEbDZ)?>>WqO]r[XBEX5j^+cN\Zf%SW6cdeoF3S&LkAmo:dkgE,8P/CLto!o>9@MYc>^h*T?lqS(l@(7j&F.fTFlo=;"]FI\3@'4d"4A.(6pCF':o<1_6G#9%SSlm'Fl0XTU3ZTt2=7oAqLQ]BscLDTAgA^Jq+`gW<V`heCOkDsb;!"Jl4UgY/i]"H5)BW#,2iES2@dq-Rh=P@YZcfVFVLe+>Wu/[hu8^/#!3&)ljA*I*2a"!WCrXMb3s9U]1!C)?dR(dXkG<Bq:D5HKY3I,^E'WSkWH@GJ\]U(3>]b,UpMClhJdc+1#m7"@^saS+T/'LA$t&,+QVE)(f+=GL9"Kt6c&pklE*36!,]Nm'\7r;Nb=@VdkSX+K/,l]6"qJHNg-<X`1iLWbZhC9GPTb>$eD0334Gh*$BIY!aWg9P/?>ZB&3/U(*\51JRLJ>nZkhBq.[?)t+Mi>=&9HN\ZffM;88NUXb>m-SE[TKl6o$@eVm=H2mR\L-p6KA$E._V>mp`;dLFTG\<=CB:DXMo=a_N`d_;<3:#0#N\D1E\d"GCXse6lQ3/bO6@`j\IE!kCmQ(%9Zc41pbTRseiTDQ"%\(ENoM(C8Dg%32)u'9Z[c'T46bt$CFM><OVNQ+=n'H1"HQ/(.`_hlU2C^=rSE-e6L/D)9M9Y@[Yu,C8^#?OVVOaIlc$Wqt>cfpAY$MZU1Fe<Rkb%2-6>,WW5:Q@!oZ&RJ/A%?nQIsGTK5ILIbqBO7Z,AaGNt_I<bk+MCA9sF'Hj4kYD`-b>>U/Y-;Wm?MR@VtrD#`)ENb6q:pE293@]NZN@&hPYAUJCS421[SK/m[J)6?#Y<R>'9!1^e)mm^cc~>endstream
endobj
22 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 827
>>
stream
GasJPD,8n?&BE\k;k/(9'm"64O$f,N2UKgtSd:N,oh?4W2HuWS3%u]/n%Qtd@>Ha?h4/o.qqq3c"r&01r$EA52\F80)))QJ&ssdCU2r;NK&6UfMJFAdKSG5I"0l;5JT;Ld+;Ib>>6]gs@^H,D(bs#Ki97'rdni)-c2n2</#nR/@[L+arT>78`ArYg6n2aiqfhm1/^G'[WC**@(Lah*F\*%l:_:dFQ4Rn@s5U"qBCNW;3M3C2,(n(=FNM+G"m6eacX%T<TDH]9_)ll,ZJp2jo'-4LT+K94=?CmR30>WX+og>C?j:T><O$AFTArq?`uoRc?a-Cj8F]HRe<j46%IQYM(hH`V_1o3HHsNrV.nmH@]7is%Zu1dbfWmaZ>!%bA@7M$*K'HX"l.72s?t6.W.]OE:@$?7XbcQXVV:n@jK#*&;nKeP[dDkNHdh,'i=@kbJ0c@\6>!)a=S+1D0XFmCVlDJKLq.U,Cl!R+Wf-<BSAr5W..r6u22U")6Q>Wj`Z@[Eo8f(Rs_NhU5I%-tBqlkcc=_qL7[D084J&L3g]eB,?T'hJec4.$^M+@34pm#<DoZ5hD<V*VVY?^<>M+eS_BqtEj<>5hAA/j.V20!.+2c!h,P:'@6jB[X[Ef9n8!Sfm]fS3LECK_uk%_8dS9J3=H:Za0`s28Vio/1S5([S+t$G8T,AEg+`14Q%lMuCi&kD/QcjSRo#8nEhj`paHE]pb9dN`)'E$6'rc<j"L/ZF2ZD\1qDIc6'mrn9M[.m?o$[^@P)M\+*1Kq5cJ@qB--hk*,9g,Wb"6Uea!i4O!J`g,WssGH)bNp"K]f(^0j=drB_J~>endstream
endobj
23 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2131
>>
stream
Gb"/'D/\/e&H8h>EALG@$!Ndn7#G_VC`V6(/^gdTmL!bO$:Ug426rJ3V.n<M^[ESnPao[3118q)`IT=5eFCV,m^\*b5k2bGmplUklP([A(bcHK"gEL[0Kjs,;t#o?\E%'@JG>#u&I/sEjq#V2Q\1Af7Qb.QBLl)>5bJrk.T4(P]eEt?PSt>7*Wm$I)QEdn@R;PiiI.=hc>i-"#UuP?ms"a[j'G,ak8h-Js3V[D@,7QI<BhUjM,;!0"lkrarVd4s(/%pZ7n&QV]=QM6;%5>,6[8Cmm_c%sX'3j.OoDGgiaenWH4Qkp@CgC>)V>Q%:'A/;C&^G"^_G_'Wf)Qgq^PpLGmn_$k;D\lg5tBdUENXUpM#Gg\8oJM+#c[bJIEg*4>)H'Fa][u6UGL[#(9a!UB8;!abN#g4XT](+5S]jQnd^6+rhC*E411,a#F=T*=4d:^<\or:Kd^Z3^_EAP'FA.,/].VZ?t7VA$GOB47ZQKR?%gUjGC]J$XQD(#EM0l(aWEAmK5hg^(RFTfe>%3fkip&*S?^\rJ9s$p'i3JKXfjBs!"#[1OU/dL0HW:LlG@YU_fl1Ncot?7b!"+j<<1U9s!B8RAnp$V)EUYGN61"<#'f3-":$?J2#JtBpoZHj1/L79ifU6#Rl+Ir5'/(S%;_@Fc*.b2Vbsp;.5!kIG(?];;;CMGB^GCDUFC?5e,XJPo#+IV$bh@H%D^P`P0OcM'OpNiNPhSa!@8B@tG:B"D^`B4s*]qqQMm'KWp*!#He;+oaM#]:oV9@g)Hgc>R&*G?N]\DmG@;5e)+Jh@3;4%K_.#*7jR)Q@q&j67%nG75+YGUjT8!<$=d9U"U=[@k&N?s7P(?4CEm,U'P._\TftQcn5=b8hrmjqY%m_AMm?I$.*'%nV+Us?kl)(8<"LBFY\CX.RE)^Q0j3S_ln=R"::"\52(.p&>j4hpP[2a+(^:SOG+.fn&-BQO0StcFqFg-J>b0s_/=J]#NA(6p\8uiHpP+#KgOl\2]-8I4"A83-'#5cmRkfco1f!A]Fh7rlHTP7U6=S_^Xi^$5Nsu5SI]0F+#i@#'caas36rN)Gqn5Oen6jit3G+JSaPA>.*0MOPhLe$_PM(1=rqWH&(*kcC7e4e^dT*s0h\PbF'PATaKm^3kgDg*6B1HpQCV0H?]4b*haKa'h9;p5N@S`'f,),RIm+Cg!KmPo1j@QStUbaW6pZ*=7%_smYcApt(CokGUK>T8=\>URqQRG.co4sG+J9M&Dp4)>r6`!0F02%'Z#B-_V3/q^h\pYL;aI-7QTbkA(FC=n)h)a%65G=)J1BYu,ZTUrWN9nQn<`o?s/UnHiCnRjF^1e!54JnBZ$:kBoR$j:G$i'rVOu":nD]S9[Hl"G=B+](HqA!`r-$U$sR!&Su2f8Lei!A3.6gidYq>A*5:gc[7(I<`;$H7G@SP*Y^_:lN5fW`C%g2>P(/hR7:470PN#H%5>UKC8t;[N_p[?Rs3!&4DIaQJrC(f-Lm_0HqS+Z7g-\hge=r"ZGZ`)nQ4FN%cr-Ae^nZgmEDY)]R;aJkHME'pjBs7X%2oS^&aY>7N(gA4gc_h/&)B<]A/1^ufP(7"KuMXdoM?6lK$gPF"=C4OR0F68(cFXc#kkkKk7h2Yg`N_"sOj?`:m9@J^UR5#Cr-qtpbQ;o?(1lXm^aEc]iN&5Yd-V[kWm>]MBXrf2h2l'9?+[3Jj[6W0Nc/.Fo13:IT;OK@Q>+aV=%OIr@8HgJ1Q6p5uZIjrl1`o#GY7Y!'col%lXJMkQnO.FdRWOR!AIgT>5ktMjFt'\cX".0108n;+2c*H)9D;;q[*^#P8,K'PrXYh>HKIW;7PKU>E%]F!+2h;ocB!.$9f6,CTlG=]\\$o@-DSc*)kUkP'ihq8SHUC^5-RR^Lcmkf5(!87l9aAo#V:+7Iar-q6s3T__<3eMa)MU0`N$Y,T].@%"qKLKVu08\g:B#*<c9=>1LIap.DUhqmH.IJ[tRd?M?41'X\PT''+B$8@'1G;mbPKfO%^gh[+/4HcGg<&rV??T'cVfuK`8IbY34MfP41+d8\BFpE,_tUN\P6%EUk;<>89kTebXaQo);M]VAbcsY\_`tD*eNsT0@_qp^VD\,?8q1BrW(__YRMcENrO\JG1u`Fui_~>endstream
endobj
24 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1766
>>
stream
Gau`Thf%7-&:W5<ETiTb/.>*I,aG&6/\7Y[MQP^I4p;c;R<I*DLEjo8ZbLrZ8tqQgNg^0nil&8Ro"4@4+7G4u5k2hKHWgE^=i/9B#)pH?%87<6bE)/I1:RYsQX#@Q8>p5>F!,I9J"YX$,t*p;R)]Ul<CA><68P?YXF]CmV3lkg_"qWXJ1+!S(-j^7UmCc+LI?0CV?MgPN:3q<q.WJT-F.9>1qR)t_\oP"b+j]C:_p#9Q3Aggs5V9-G[G;]P?q3g%Y(sq8M=7,j'9Fd&'(lC->QekK"'m3afB8eb4Z+,?X`cYg7$q-_+B=PDW?4'%587ddBqH8KDh$NZ8)G<_>0&9F9%K:-`X*l@7V-QikYj@K_Sqs.&;r$"?[ma=$MuU>S0S^Sr@[FcOtm[aepB20II>-(k,n^0am)u@[n6)i3>Z#c1(pkJJ8D#ht)BW>rLQ8*f,+mBg>0(3F6rnrF568`:Ggq#b7dYWsemJ40'AL?:f5lO%ID0-!W5I!^ka>8(rYj:Z=]CpR(Du:@P*tNsWR^D\^)teEnjc5<o,NgFCOt%(24_YcX,42Z%e\-O6WMQc%>^1ZineYVmj>_EEXmRfm;N$<pFNmi#-B.VY)Np@g<R+s^#ZV%Y,LkSikss4I_-#nj^0iLT:'+I)/7^E:W$J\L'ZEQG$`8Z8:s3hhBiBeUkHcABo:4"*<?OCrCY9aAtSlm?u^IKZ@a$`cjqp#J"O_<>RUb#49ad#i+YIe77M\lY1@`%DmKT.4=`F2h)<PS-8l(ViICS"qpg-KhpfD_;tg>O5-R_g81;M(g8o4[?H-o5'&qcB9/)QPE)?"./04QPgJ`1$2[3-rqr"+Ok:aDe7Hc\]]%fa(nVC1.=JV]Cpj/=j>VD>FceNb,ge4hY&mIBQ82PB2]/8\DCh5^:2qQ?uU"3`%`BLi4Q'^"6Z'-9i?9ulQgDm%#i!sB\(Qj48O)32J0-p8S@[H=U`cUU)dj/\%)100f>f>W2j^TD8CeCUq8['Ds!#-LbK^k(lq$`jUfUA;(`O<^l-4n->fHf/mgkDG'"e6M>Joa"K%^W.BfcjHB;ErPoJ692!".@d*D!APVEbf_sO.cPmHq/RZSe[-(E9J6*@Qf!n%I??N*&g&J5m1]oVGr]<[caTGiPQKq.H8eK3e.:R#@H[%5Z0ROa?Mj(Jpp,Kh.g0eE&8)!i8t!ctcIN\T.:gAK'k9qYU1]AIo:$YS9uE)!C&YX%ecKX&Cs9\EgR8lc)kB`lXiPenue2/YoOk)3dfX,/K*/(\,@bh?WTMHMV^@#a^2eRkBSrXmaXrSQ]TGskn+Ea#VU.sA%j0@t3n:\"qlLb76X_O#%&6:W^jg3Vn%rD;'[N9N[lG1RB_f:<'Aj1,Hr*!9EWZ0.W]H7'BZTAK2?ZEC7&]-p?mNo_=b0<b]N%Do/M"of2!akE1fE'"1#5[r\-*-T2_>A)El[V&Cj%/ZTJS'W69R8fqqlL#.n)NkNhra7l?f7<6]a7f#Emf;)Pe"t4+),ofC7m)Dm$r:-bf&;S2p1<#<"qS"9R.aiM4kRdpjJ!f\cfae2bIb(sSD=C!gGP&4=9re>iSUY?@K5K9EgPEbQC01b').c-%r4O"W1VDfdFP(?rIeh$$4hqaRpilHS3631^e*-FSTOhP8`'LngSA^/E1>>>F*k.a851c)p-r%pAQirnXKB)6flTAB+7WlAAE@f7Q1e&FOLrj&m8#b>^=pER%Zc:AhBjb]\5>>+X(%Qfmq1)C.qn6Z@tr\2#^pkL#lXpIWPmB~>endstream
endobj
25 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1759
>>
stream
Gb"/'hfkPJ'Ro5K\BC)jY-K;aLp#K8f!K'MbM'=C],/4Y/DINK*5G.PSG_if&k69LNgmHL*nK6_i$*UDf6=D0$n*1@r/YIj^C?k9JcXmc(^'Ac#D3\75&edf_L3JM!f/A+)fd'pX8ei[3#N!op`U.c8/`$WGi2._U^SPgS/2;ZJ3q*ITu2B`4q4:m6o*^os42Rapme*oNsOVRfB4gSPM3[2pe0r"bkd*&3.``QZDaor-kh/^_$kWC?bJOUht)[Sj(Y*_n?sfG4KW#4GlWW?Nt9Dh.4[9-,B]^.Hp1J3o`,1<@.7"YlJR"1-N_Os`BR3G0F;tRX*e0iSjnnhMa2H)?=QYJ+J1:r%B(PT9*<DWSH3q>%#f9kht(:=E'`b^.n^m(&5#+=O`@oOjUi:']IOT=R+SZ!]O6mW%:qsONXH"mCmW[^X9";'7K].Ge7&-:[1B5?U/jqRd+4'gDlf)EajuH3kHQgc%&`_pVnW0tF2sBo-ZVe>:<FD">Vb^K6X3\"Bi5^inj6G!k4Nd#h`(<RiOA>7,?Tc7?,8`Wnk-li;anF!O<9OV<5jP@+J,V.A%@L5^8.&1k;t5*[\>pe]+Y_hNfG^_BVJne40'rYQI"]=L0Pfa=>=H`6,Jm6$&5@:U;pBgp0@4>n.[qbiH7P#NY_h@;m<RWn^'.`fSXM.n-Sc$d$;[a9^Gk!6S`].@g:)u6K/=163GMpH=i8H^e,0<^!TdgV!L4@/Jr>IBsu18#MRnCSDSYA?>a;S$_bZOh*fhUKUd$5!@eBHKums\_))lA>XMbpW)Csi"EOc+0J;o6*;nMt<(U9ebJcSi+-ij5oBbD5boNn5&@uOd.+^9iC6W+ECsb'3WTn-D75B_",gUY-@Qn&U+Ys^Qq^<QHlUf3XDY;-8*iX.d81TM=X"9"q,)`im3/YlCq`H>t_qsk`>i2Yh"@ObQ"rAE-Za,000(:YbQVgY_6rB`IE6/d3G6>C-Gn$OIMOraOJ^MJFh,s;=*JkQ^PY>'L(.Wb,)V69dbB]j1_.BG:K18A?S#d$F#Q6L"Fld!Y*65qCbNKSA:t;;]i9;Z#puO/oI`0`BCN-"[;3KcumHY"h(pc>?`1('295&c-gVqe1?\bQ"F[+^<Y<8m/.KoY;qc1`,Pp$]f[pOhMgG888^C%r#OL;krFMG+?[U3/DD8BndCX;ro;amlio'*3]_6@">o-a#sSM*rFrLXJOP[gRC0bu&f_PP-'I[b$dH'/$,7L)/P/S,YsMt.0pRDI-r9>9;=Bp=TTL"]51^LD>t8CFBF^9kIInd/@hmXjR%0$o@,h[=bV"atp.L"Oo,XquVRMI#NJWQFeq/EE9mZr?\m/GQVFgf2j91<EiS4pPo+atAloP[BS1O3RP6R20Or(,kLNf3D5$!tkkOIbuT'URe-?d0qm.<d&MBIcIQiF%rA(W/1'n$*u]ePHroa#"gHd^9e%S<'8Eg&p<Ld-iUW`)7L])I%u)"!)W9sM1ON()fT]0,W51ubOMFFgBkO3i#:'lSa$Ye&8X3:W?`;nVkXXbh\L1>$N:[I4QJl/1\h:\=*&L"(Wj><2u'PW$\#-k$%]QMs&r_&o'`YoUo/K@9aXnWiK!F%k>&)D?LGI)7BBBH4GL2;5@C?<)!Gmcc_<6jZH4-$\T326[CYefgReeTHn%K9!g6FrRF5<44+-%0Im,a$D?JHDXZ%9(d3m`2-]cMf[eWa7l$/!l0c&H3PI%k@gnTB!.V2+bT&s+a=3]o);tH<PfB*(r+7ZR<WW~>endstream
endobj
26 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1319
>>
stream
Gb!#[6$JeX&AI`dk-et_aH@)Wp?#tVf'd0K2g'JiKa??Y_;Ia+l2LQ#^gIeYS[\p>[J^MP,gH(31I<lOT_N*C^KLYcR)/!L!TPl'!u")<lj>r%SA-)]"!.dkLaT/<E?LBFJ"U%+3VWU]+s&J#1aFgj+=&107M7\`$UY[rWF>e_@6h\[L/J#6bR\6gZeco-k6NYq*sC!7oc&@cVAGA&_[^Ch5BMW9fLAj/eJ!Tk5P/BmK!Xbr5:5m1=D4Iq7?PE1>ACRQE6EM4j,#iU-8egOn>C`b=g;;tgqJEo4j;d7=9JRiE8i+UkPhZl!@@h#`YS0sm5/.FGmr*9cW?(i%aiY`7/Bkj/o[19_l]d_/%!D*mrQpZa"XtQ%O>c(Rt@T,N\5V?Z:XZ@/ITI/%#5fc7XW3$gHn4'gZ+bdpO,ma)N$D"9'tg,%T_<#o]A(7^Ve$$#P76hm!s<MTjeQT_HM(8YPZk-L669h?0^'C\>rYqZ0jAre7PoX5!K(5^QtKgS1[*3XJ7Om<T"pPGud&DmF_h*XF`"J*ru&>.9%3u\*s$"9+954"E(K)&ieF<>8^^5'&+6\hHYJ]FciLM`@!QE2V%DO&j:M$M-I'V,`X6+n$Y);@2@0JK1aZL[c\2Tn0C)NS+3CtcrQ9]@Q?U1M*c\`m)P`!Qaqk@F96/ooqIC"LF@A?FUiDWiPkc$f(jQ2=R,qbm$sA1;UtXX^>E0]K%DAU51m=dRsmb\a6%C*Zh-HW!sE`K$<:)X>umr(#nZm*9=$6nRR$D!c]rGOAG2[;!nSl=^FYBs<`6<T`e<]<c^#uFIF"Zs:%V9"iHWul85B@4J<e$#dba]+bi'_=<60WMbKVS(=;^P$`UDq;VmkUoArP9CH;fLg+s2-gR+GqF:S9Kl,hUpc%`;/P*Mg@ng7p&I-N9;oh3-PEmUZfk.qe*'4]VIJb"uI,![9RWBTV9TUD\&(JY?FHqQag<(`T:5E)g`TJd*>AHl&GnM-,FjgG=?jAWc.:J$&Y]SG284Wk:*5_Xc0.m<WUICrm/.2)tJ6XlI%Gi.4n]A_S#iNJXTC2+e3e7Z/`jZl-hBjhR!):KWK8X<Z8%l^sKu)_fuHlSka_@:dGG`&23Gjgb)7Im8\de-X;sLNR,-rp!]9o+q/pTA-_YDS>1/X*):2eR9K1eXM:*ZDOIG9CA;FDDV;h#Bms:]#-R,H/2FXb$L39pRq2pBO)Ie.+(p.:Ms4JEk4`@W_DeE#9puZGi&DN(1?^`86k)"<NuUj`TaCN!]m9d,GLnN05g*pNUt(?MbLbUUr87X_RF>_'tHc6>Md>6:K%DA^JFOd.f~>endstream
endobj
xref
0 27
0000000000 65535 f
0000000061 00000 n
0000000123 00000 n
0000000230 00000 n
0000000342 00000 n
0000000547 00000 n
0000000652 00000 n
0000000857 00000 n
0000001062 00000 n
0000001267 00000 n
0000001472 00000 n
0000001550 00000 n
0000001756 00000 n
0000001962 00000 n
0000002168 00000 n
0000002374 00000 n
0000002444 00000 n
0000002736 00000 n
0000002848 00000 n
0000004752 00000 n
0000006210 00000 n
0000007822 00000 n
0000009721 00000 n
0000010639 00000 n
0000012862 00000 n
0000014720 00000 n
0000016571 00000 n
trailer
<<
/ID
[<b5514340f90596be571d23fd606fedfe><b5514340f90596be571d23fd606fedfe>]
% ReportLab generated PDF document -- digest (opensource)
/Info 16 0 R
/Root 15 0 R
/Size 27
>>
startxref
17982
%%EOF

View File

@@ -0,0 +1,407 @@
# Les 18 — Supabase Auth + RLS
## Slide Overzicht (Klas A — 3 uur fysiek, demo-driven, **laatste les!**)
**Lesvorm:** Tim demonstreert klassikaal. Studenten kijken. Zelf bouwen = huiswerk.
**Demo-app:** Multi-user "Tasks" app met magic-link login + RLS
**Vervolg op:** Les 17 — Externe APIs in diepte
**Aansluit op:** Eindopdracht (thuis)
---
## Slide 1: Title
### Les 18 — Supabase Auth + RLS
**Visual:** "Les 18" BLUE, "Supabase Auth + RLS" BLACK, subtitle "Multi-user apps met per-user data isolation — productie ready"
---
## Slide 2: Terugblik + Curriculum recap
### Waar staan we?
**18 lessen — wat we hebben gedaan:**
- Les 1-10: Foundations (Next.js, Supabase basics, Tailwind, Shadcn, deploy basics, Auth intro)
- Les 11: AI SDK basics + Polderfest
- Les 12: Tool Calling
- Les 13: Agents
- Les 14: RAG + Embeddings
- Les 15: Cursor + Vercel deploy + GitHub Actions CI
- Les 16: MCP servers
- Les 17: Externe APIs in diepte
- **Les 18: Vandaag — Supabase Auth + RLS multi-user**
**Vandaag:** alles wat je hebt geleerd, multi-user maken. Klaar voor je eindopdracht.
---
## Slide 3: Planning
### Vandaag — 180 minuten
| Onderwerp | Duur |
|-----------|------|
| Terugblik + waarom auth | 10 min |
| Theorie: Supabase Auth basics | 20 min |
| Theorie: RLS — wat en waarom | 20 min |
| Theorie: Session in Next.js | 15 min |
| **Live Demo 1** — Auth setup + magic link | 25 min |
| **Live Demo 2** — Middleware + protected routes | 20 min |
| **Pauze** | 15 min |
| **Live Demo 3** — RLS policies multi-user | 30 min |
| **Live Demo 4** — Social login (GitHub) | 15 min |
| Eindopdracht recap | 5 min |
| Vragen + afsluiting cyclus | 15 min |
---
## Slide 4: Waarom auth + multi-user
### Echte apps hebben users
**Wat je niet kunt zonder auth:**
- Per-user data (mijn tasks, jouw tasks)
- Privacy (alleen jij ziet je data)
- Audit trail (wie deed wat wanneer)
- Subscriptions (Les 17 — wie betaalde, wie niet)
**Auth = 'wie ben jij?'**
**Authz = 'wat mag jij?'**
In Supabase: Auth + RLS doen beide. Geïntegreerd, simpel, productie-ready.
**Voor je eindopdracht:** als je app ÉÉN gebruiker heeft (bv. demo), kun je auth skippen. Maar als je app meerdere users heeft → must have.
---
## Slide 5: Supabase Auth basics
### Drie methoden
**1. Email + password (klassiek):**
- User registreert met email + wachtwoord
- Confirmation email
- Reset-flow voor vergeten wachtwoord
**2. Magic link (passwordless):**
- User vult email in → krijgt link in mail → klikt → ingelogd
- Geen wachtwoord te onthouden / lekken
- Modern, secure, simpel
**3. Social (OAuth):**
- "Login met Google", "Login met GitHub"
- User klikt → OAuth flow → Supabase regelt sessie
- Geen account-creation nodig
**Voor demo: magic link.** Snel, geen wachtwoord-hassle, productie-grade.
```typescript
await supabase.auth.signInWithOtp({
email: "user@example.com",
});
```
---
## Slide 6: RLS — wat en waarom
### Row Level Security
**Het probleem zonder RLS:**
```sql
-- Anyone with anon key can query everything
SELECT * FROM tasks;
-- Returns ALL tasks of ALL users — disaster
```
**Met RLS:**
```sql
-- Same query, maar Postgres filtert automatisch:
SELECT * FROM tasks WHERE user_id = auth.uid();
-- Returns alleen JOUW tasks
```
**RLS = filter dat Postgres ALTIJD toepast op queries**, ongeacht waar de query vandaan komt (client, server, anonymous, authenticated).
**Setup:**
```sql
alter table tasks enable row level security;
create policy "users see own tasks" on tasks
for select using (auth.uid() = user_id);
create policy "users insert own tasks" on tasks
for insert with check (auth.uid() = user_id);
```
**`auth.uid()`** is Supabase's helper — geeft de ingelogde user ID, of NULL als anonymous.
---
## Slide 7: Session in Next.js
### Cookies, middleware, server components
**Supabase Auth in Next.js (App Router):**
```bash
pnpm add @supabase/supabase-js @supabase/ssr
```
**Vier files:**
1. `utils/supabase/client.ts` — voor client components
2. `utils/supabase/server.ts` — voor server components + actions
3. `utils/supabase/middleware.ts` — refresh sessie elke request
4. `middleware.ts` (root) — gebruikt #3
**Waarom de complexiteit:**
- Cookies moeten gesynced tussen browser + server
- Sessies verlopen — middleware refreshed
- Voor server-rendering: auth state moet vooraf bekend zijn
Supabase heeft een **template** voor exact deze setup. Kopiëren werkt, snappen waarom is bonus.
---
## Slide 8: Wat we vandaag bouwen
### Multi-user Tasks app
**Doel:** simpele tasks-app, ieder z'n eigen lijst, geen vermenging.
**Tech:**
- Next.js 16
- Supabase Auth (magic link + GitHub OAuth)
- Supabase Postgres met RLS
- Tailwind + Shadcn
**Features:**
- `/login` — magic link form + GitHub button
- `/` — task list (alleen ingelogd)
- Add / complete / delete task — RLS bepaalt scope
- `/logout` — logout button
**Schema:**
```sql
create table tasks (
id bigserial primary key,
user_id uuid not null references auth.users(id),
text text not null,
done boolean default false,
created_at timestamp default now()
);
```
---
## Slide 9: LIVE DEMO 1 — Auth setup + magic link
### ~25 min
**Wat ik laat zien:**
1. `pnpm create next-app tasks-app + add @supabase/ssr @supabase/supabase-js`
2. Supabase project: Auth → Email → enable Magic Links
3. `utils/supabase/server.ts` + `client.ts` (template van Supabase docs)
4. `app/login/page.tsx`:
```tsx
"use client";
import { createClient } from "@/utils/supabase/client";
export default function Login() {
async function signIn(formData: FormData) {
const supabase = createClient();
await supabase.auth.signInWithOtp({
email: formData.get("email") as string,
options: { emailRedirectTo: `${location.origin}/auth/callback` },
});
alert("Check je email!");
}
return <form action={signIn}><input name="email" /><button>Login</button></form>;
}
```
5. `app/auth/callback/route.ts` — exchange code voor sessie
6. Test: email invullen → mail krijgen → klikken → ingelogd
---
## Slide 10: LIVE DEMO 2 — Middleware + protected routes
### ~20 min
**Wat ik laat zien:**
1. `middleware.ts` root:
```typescript
import { type NextRequest } from "next/server";
import { updateSession } from "@/utils/supabase/middleware";
export async function middleware(request: NextRequest) {
return await updateSession(request);
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};
```
2. Server Component check:
```tsx
import { createClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";
export default async function Home() {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) redirect("/login");
return <div>Welcome {user.email}</div>;
}
```
3. Test: niet ingelogd → redirect naar /login. Ingelogd → home page.
---
## Slide 11: Pauze
### 15 min
---
## Slide 12: LIVE DEMO 3 — RLS policies
### ~30 min
**Wat ik laat zien:**
1. `tasks` tabel + RLS aanzetten:
```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;
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);
```
2. Add task — werkt voor mij, niet voor ander account
3. Open tweede browser/incognito met ander account — andere data
4. **Demo: probeer SQL injection vanaf client** — RLS stopt het:
```typescript
// Probeer: "haal alle users hun tasks op"
const { data } = await supabase.from("tasks").select("*");
// Krijgt alleen JOUW tasks terug — RLS magic
```
---
## Slide 13: LIVE DEMO 4 — Social login (GitHub)
### ~15 min
**Wat ik laat zien:**
1. GitHub OAuth App aanmaken (Settings → Developer settings → OAuth Apps)
2. Callback URL: `https://your-project.supabase.co/auth/v1/callback`
3. Supabase Dashboard → Auth → Providers → GitHub → enable + client ID/secret
4. Login button:
```tsx
async function signInWithGitHub() {
const supabase = createClient();
await supabase.auth.signInWithOAuth({
provider: "github",
options: { redirectTo: `${location.origin}/auth/callback` },
});
}
```
5. Test: klik → GitHub → authorize → redirect terug → ingelogd
6. Database: zelfde RLS werkt — GitHub user heeft eigen `auth.uid()`
---
## Slide 14: Eindopdracht recap
### Hoe combineer je alles?
**Checklist voor je eindopdracht:**
- ✓ Next.js + TypeScript + Tailwind (Les 1-10)
- ✓ Supabase Postgres (Les 10)
- ✓ Vercel AI SDK met Tool Calling (Les 11-12)
- ✓ Agent met `stopWhen` (Les 13)
- ✓ RAG voor documenten (Les 14, optioneel)
- ✓ Cursor + GitHub + Vercel deploy (Les 15)
- ✓ Externe API (Les 17 patterns)
- ✓ Supabase Auth + RLS multi-user (Les 18)
**Ideaal eindopdracht-stack:**
- Multi-user app waarin elke user eigen data heeft
- AI feature met tool calling of agent
- Minstens 1 externe API integratie
- Deployed op Vercel met preview branches
- Code review via PR's
**Tijd:** ~40-60 uur thuis. Inleveren via Brightspace + repo URL + live URL.
---
## Slide 15: Afsluiting cyclus
### Bedankt en succes!
**Wat we samen gedaan hebben:**
- 18 lessen, ~54 uur klassikaal
- Van localhost naar productie
- Van simple fetch naar agents + RAG
- Van mock-data naar multi-user productie-apps
**Wat je nu zelfstandig kunt:**
- Een Next.js app van scratch naar productie
- AI SDK + tool calling + agents
- Externe APIs integreren met OAuth + webhooks
- Multi-user apps met auth + RLS
- Code reviews + CI/CD pipelines
**Voor je eindopdracht:**
- Werk er gestaag aan (niet laatste week 80 uur)
- Push klein en vaak
- Vraag hulp — Slack, mail, office hours
- Eindopdracht-presentatie volgt na inleveren
**Bedankt voor jullie inzet. Veel succes met de eindopdracht.**
---
## Slide Summary
| # | Title | Type |
|---|-------|------|
| 1 | Title | Opening |
| 2 | Terugblik 18 lessen | Recap |
| 3 | Planning | 180-min |
| 4 | Waarom auth + multi-user | Theorie |
| 5 | Supabase Auth basics | Theorie |
| 6 | RLS — wat en waarom | Theorie |
| 7 | Session in Next.js | Theorie |
| 8 | Wat we bouwen | Intro |
| 9 | **DEMO 1** — Auth + magic link | Demo |
| 10 | **DEMO 2** — Middleware + protected | Demo |
| 11 | Pauze | Break |
| 12 | **DEMO 3** — RLS policies | Demo |
| 13 | **DEMO 4** — Social login | Demo |
| 14 | Eindopdracht checklist | Praktijk |
| 15 | Afsluiting cyclus | Closing |
---
## Bronnen
- **Supabase Auth docs:** https://supabase.com/docs/guides/auth
- **Supabase Next.js setup:** https://supabase.com/docs/guides/auth/server-side/nextjs
- **Supabase Auth UI:** https://supabase.com/docs/guides/auth/auth-helpers/auth-ui
- **RLS guide:** https://supabase.com/docs/guides/database/postgres/row-level-security
- **RLS policies in praktijk:** https://supabase.com/docs/guides/database/postgres/row-level-security#policies
- **Magic links:** https://supabase.com/docs/guides/auth/auth-magic-link
- **OAuth providers:** https://supabase.com/docs/guides/auth/social-login

Binary file not shown.

Binary file not shown.

Binary file not shown.