fix: update lessons
This commit is contained in:
1
Les18-Supabase-Auth-RLS/.~lock.Les18-Slides.pdf#
Normal file
1
Les18-Supabase-Auth-RLS/.~lock.Les18-Slides.pdf#
Normal file
@@ -0,0 +1 @@
|
||||
,sharp-zealous-planck,claude,21.05.2026 11:30,file:///sessions/sharp-zealous-planck/.config/libreoffice/4;
|
||||
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 |
|
||||
332
Les18-Supabase-Auth-RLS/Les18-Huiswerk.md
Normal file
332
Les18-Supabase-Auth-RLS/Les18-Huiswerk.md
Normal 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!**
|
||||
137
Les18-Supabase-Auth-RLS/Les18-Huiswerk.pdf
Normal file
137
Les18-Supabase-Auth-RLS/Les18-Huiswerk.pdf
Normal 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
|
||||
283
Les18-Supabase-Auth-RLS/Les18-Lesopdracht.md
Normal file
283
Les18-Supabase-Auth-RLS/Les18-Lesopdracht.md
Normal 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`.
|
||||
137
Les18-Supabase-Auth-RLS/Les18-Lesopdracht.pdf
Normal file
137
Les18-Supabase-Auth-RLS/Les18-Lesopdracht.pdf
Normal 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
|
||||
542
Les18-Supabase-Auth-RLS/Les18-Lesstof.md
Normal file
542
Les18-Supabase-Auth-RLS/Les18-Lesstof.md
Normal 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
|
||||
238
Les18-Supabase-Auth-RLS/Les18-Lesstof.pdf
Normal file
238
Les18-Supabase-Auth-RLS/Les18-Lesstof.pdf
Normal 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
|
||||
407
Les18-Supabase-Auth-RLS/Les18-Slide-Overzicht.md
Normal file
407
Les18-Supabase-Auth-RLS/Les18-Slide-Overzicht.md
Normal 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
|
||||
BIN
Les18-Supabase-Auth-RLS/Les18-Slides.pdf
Normal file
BIN
Les18-Supabase-Auth-RLS/Les18-Slides.pdf
Normal file
Binary file not shown.
BIN
Les18-Supabase-Auth-RLS/Les18-Slides.pptx
Normal file
BIN
Les18-Supabase-Auth-RLS/Les18-Slides.pptx
Normal file
Binary file not shown.
BIN
Les18-Supabase-Auth-RLS/lu45mkbrn.tmp
Normal file
BIN
Les18-Supabase-Auth-RLS/lu45mkbrn.tmp
Normal file
Binary file not shown.
Reference in New Issue
Block a user