diff --git a/Les01-Introductie-AI/[AI] Les 1 .key b/Les01-Introductie-AI/[AI] Les 1 .key new file mode 100755 index 0000000..57c4813 Binary files /dev/null and b/Les01-Introductie-AI/[AI] Les 1 .key differ diff --git a/Les10-Supabase-Auth/.env.local b/Les10-Supabase-Auth/.env.local new file mode 100644 index 0000000..1c7e1a1 --- /dev/null +++ b/Les10-Supabase-Auth/.env.local @@ -0,0 +1,2 @@ +NEXT_PUBLIC_SUPABASE_URL=https://ooozbbewsglfvysikbsf.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=sb_publishable_QNSXIe0FBRAP-Wgd_Rb_uA_GB1WcEHJ \ No newline at end of file diff --git a/Les10-Supabase-Auth/Les10-Docenttekst.md b/Les10-Supabase-Auth/Les10-Docenttekst.md new file mode 100644 index 0000000..a56206b --- /dev/null +++ b/Les10-Supabase-Auth/Les10-Docenttekst.md @@ -0,0 +1,1651 @@ +# Les 10 — Supabase Authenticatie & RLS: Docenttekst + +**Docent:** Tim +**Duur:** 3 uur (180 min) — 09:00 tot 12:00 +**Cursus:** AI Developer — NOVI Hogeschool Utrecht +**Datum:** Lesweek 10 + +--- + +## Overzicht & Tijdsindeling + +| Tijd | Duur | Onderdeel | Slide(s) | +|---------------|--------|-------------------------------------------------|----------| +| 09:00 – 09:10 | 10 min | Welkom + Terugblik Les 8-9 | 1, 2, 3 | +| 09:10 – 09:25 | 15 min | Eindexamenopdracht Introductie | 4 | +| 09:25 – 09:45 | 20 min | Theorie: Authenticatie & Supabase Auth | 5, 6, 7 | +| 09:45 – 10:00 | 15 min | Theorie: Auth in Next.js + RLS | 8, 9 | +| 10:00 – 10:15 | 15 min | **Pauze** | 10 | +| 10:15 – 10:30 | 15 min | Hands-on: Auth Opzetten in Supabase | 11 | +| 10:30 – 10:55 | 25 min | Hands-on: Login & Registratie Bouwen | 12 | +| 10:55 – 11:10 | 15 min | Hands-on: Sessie & Beschermde Routes | 13 | +| 11:10 – 11:25 | 15 min | Hands-on: Basis RLS | 14 | +| 11:25 – 11:45 | 20 min | Doorwerken + Tim loopt rond | 11-14 | +| 11:45 – 12:00 | 15 min | Samenvatting + Huiswerk | 15 | + +--- + +## Benodigdheden + +- Slides Les 10 (15 slides) +- Werkende Poll App uit Les 8-9 (Next.js 16 + TypeScript + Tailwind CSS + Supabase) +- Supabase dashboard open in browser (LIVE demo) +- VS Code met project geopend +- Terminal klaar voor npm commando's + +--- + +## BLOK 1 — Klassikaal (09:00 – 10:00) + +--- + +### 09:00 – 09:10 | Welkom + Terugblik Les 8-9 (10 min) + +--- + +📊 **Slide 1 — Les 10: Supabase Auth & Row Level Security** + +> **Tim zegt:** +> "Goedemorgen allemaal! Welkom bij Les 10. Vandaag gaan we iets heel belangrijks toevoegen aan onze Poll App: gebruikers. Op dit moment kan iedereen alles doen in onze app — polls aanmaken, stemmen, noem maar op. Dat gaat vandaag veranderen." + +--- + +📊 **Slide 2 — Planning Vandaag** + +> **Tim zegt:** +> "Laat me even laten zien wat we vandaag gaan doen. We beginnen met een korte terugblik op wat we in Les 8 en 9 hebben gebouwd. Dan heb ik een belangrijke aankondiging over de eindexamenopdracht. Daarna duiken we in de theorie van authenticatie en hoe Supabase dat regelt. Na de pauze gaan we hands-on aan de slag: we bouwen login en registratie, beschermen onze routes, en zetten Row Level Security op. Best een volle les, maar we doen het stap voor stap." + +--- + +📊 **Slide 3 — Terugblik Les 8-9: Supabase Setup & Database** + +> **Tim zegt:** +> "Even een snelle terugblik. In Les 8 en 9 hebben we Supabase opgezet als onze backend. We hebben een project aangemaakt, twee tabellen gebouwd — `polls` en `options` — en onze Next.js app verbonden met Supabase. Jullie kunnen polls aanmaken en erop stemmen." + +> **Tim zegt:** +> "Maar er mist iets heel belangrijks. Wie kan me vertellen wat er mist?" + +*Wacht op antwoorden. Stuur richting: er is geen login, iedereen kan alles doen, er is geen beveiliging.* + +> **Tim zegt:** +> "Precies. Er is geen authenticatie. Iedereen die de URL kent kan alles doen met onze database. In de echte wereld wil je weten WIE iets doet, en wil je bepalen WAT die persoon mag doen. Dat is precies wat we vandaag gaan bouwen." + +--- + +### 09:10 – 09:25 | Eindexamenopdracht Introductie (15 min) + +--- + +📊 **Slide 4 — Eindexamenopdracht: Vrije Keuze App** + +> **Tim zegt:** +> "Voordat we met de theorie beginnen, wil ik jullie alvast vertellen over de eindexamenopdracht. Dit is belangrijk, dus luister goed." + +> **Tim zegt:** +> "De eindexamenopdracht is een vrije keuze app. Dat betekent: jullie mogen zelf kiezen wat voor applicatie je gaat bouwen. Het kan een to-do app zijn, een recepten-app, een fitness tracker, een blog platform — het maakt niet uit, zolang je de technieken gebruikt die we in deze cursus leren." + +> **Tim zegt:** +> "Wat zijn die technieken? Laat me de requirements even langslopen:" + +Benoem de volgende requirements: +- **Next.js** als framework (met TypeScript) +- **Tailwind CSS** voor styling +- **Supabase** als backend (database + authenticatie) +- **AI-assisted development** — je gebruikt tools zoals ChatGPT, Copilot, of Claude om je te helpen bij het bouwen +- **Authenticatie** — gebruikers moeten kunnen inloggen (wat we vandaag leren!) +- **CRUD operaties** — je app moet data kunnen aanmaken, lezen, updaten en verwijderen +- **Row Level Security** — je data moet beveiligd zijn (ook vandaag!) + +> **Tim zegt:** +> "Jullie zien: alles wat we tot nu toe geleerd hebben komt samen in die eindopdracht. En vandaag leren we de laatste twee grote onderdelen: authenticatie en beveiliging. Na vandaag hebben jullie alle bouwstenen om je eigen app te gaan bouwen." + +> **Tim zegt:** +> "Begin alvast na te denken over wat je wilt bouwen. Volgende les gaan we er meer over praten en kunnen jullie vragen stellen. Maar ik wilde het alvast noemen zodat jullie weten waar we naartoe werken." + +*Geef ruimte voor korte vragen. Houd het kort — max 2-3 vragen.* + +--- + +### 09:25 – 09:45 | Theorie: Authenticatie & Supabase Auth (20 min) + +--- + +📊 **Slide 5 — Wat is Authenticatie?** + +> **Tim zegt:** +> "Oké, laten we beginnen met de basis. Wat is authenticatie eigenlijk? En wat is het verschil met autorisatie? Dit zijn twee termen die vaak door elkaar worden gehaald, maar ze betekenen iets heel anders." + +> **Tim zegt:** +> "**Authenticatie** is: WIE ben je? Het is het proces van bewijzen dat je bent wie je zegt dat je bent. Denk aan inloggen met je email en wachtwoord. Je bewijst: ik ben Tim, want ik ken het wachtwoord van Tim's account." + +> **Tim zegt:** +> "**Autorisatie** is: WAT mag je doen? Nadat we weten wie je bent, bepalen we wat je mag. Mag je alleen je eigen polls zien? Mag je polls van anderen verwijderen? Dat is autorisatie." + +> **Tim zegt:** +> "Een voorbeeld uit het dagelijks leven: als je naar een festival gaat, dan is je ID-bewijs de authenticatie — je bewijst wie je bent. Je ticket is de autorisatie — het bepaalt of je naar binnen mag en welke gebieden je in mag." + +> **Tim zegt:** +> "Vandaag doen we allebei. Authenticatie met Supabase Auth, en autorisatie met Row Level Security." + +--- + +📊 **Slide 6 — Supabase Auth: 3 Methodes** + +*Open nu het Supabase dashboard in de browser. Navigeer naar Authentication > Providers.* + +> **Tim zegt:** +> "Supabase heeft een ingebouwd authenticatiesysteem. Je hoeft geen eigen login-systeem te bouwen — Supabase regelt alles voor je: wachtwoorden hashen, sessies beheren, tokens genereren. Laat me dit even laten zien in het dashboard." + +**LIVE DEMO in Supabase Dashboard:** + +1. Open het Supabase project in de browser +2. Klik op **Authentication** in de linkerzijbalk +3. Laat de **Users** tab zien (nu nog leeg) +4. Klik op **Providers** onder Configuration + +> **Tim zegt:** +> "Kijk, hier zie je alle providers die Supabase ondersteunt. Er zijn er heel veel, maar wij focussen op drie methodes:" + +> **Tim zegt:** +> "**Methode 1: Email + Wachtwoord.** De klassieke manier. Gebruiker vult email en wachtwoord in, Supabase slaat het veilig op. Het wachtwoord wordt gehasht — dat betekent dat zelfs Supabase je wachtwoord niet kan zien. Dit is standaard ingeschakeld." + +*Wijs in het dashboard naar de Email provider — laat zien dat deze standaard aan staat.* + +> **Tim zegt:** +> "**Methode 2: Magic Link.** Dit is een coole methode. De gebruiker vult alleen zijn email in, en krijgt een link per mail. Als je op die link klikt, ben je ingelogd. Geen wachtwoord nodig. Heel veilig, want alleen de eigenaar van dat emailadres kan inloggen. Dit werkt ook via de Email provider." + +> **Tim zegt:** +> "**Methode 3: Social Login (OAuth).** Inloggen via Google, GitHub, Discord, enzovoort. De gebruiker klikt op 'Login met Google', wordt doorgestuurd naar Google, logt daar in, en komt terug in jouw app. Heel handig, maar iets complexer om op te zetten. Dit doen we vandaag niet, maar het is goed om te weten dat het bestaat." + +*Scroll even door de lijst met providers zodat studenten zien hoeveel opties er zijn (Google, GitHub, Apple, Discord, etc.).* + +> **Tim zegt:** +> "Wij gaan vandaag methode 1 en 2 gebruiken: email met wachtwoord, en magic link. Dat is voor 90% van de apps meer dan genoeg." + +--- + +📊 **Slide 7 — Hoe Werkt een Sessie? (JWT & Cookies)** + +> **Tim zegt:** +> "Oké, maar hoe werkt dat technisch? Als je inlogt, wat gebeurt er dan achter de schermen? Hier komen twee belangrijke termen: JWT en cookies." + +> **Tim zegt:** +> "Als je inlogt bij Supabase, krijg je een **JWT** terug — een JSON Web Token. Dat is eigenlijk een lange string, een soort pasje, dat bewijst dat jij ingelogd bent. In die token staat wie je bent, wanneer je bent ingelogd, en wanneer het verloopt." + +> **Tim zegt:** +> "Die token moet ergens bewaard worden zodat je niet bij elke pagina opnieuw hoeft in te loggen. Dat doen we met **cookies**. Een cookie is een klein stukje data dat je browser automatisch meestuurt bij elk verzoek naar de server." + +> **Tim zegt:** +> "Dus het werkt zo:" + +Leg het volgende stappenplan uit: + +1. Gebruiker logt in met email + wachtwoord +2. Supabase controleert de gegevens +3. Supabase stuurt een JWT token terug +4. De token wordt opgeslagen als cookie in de browser +5. Bij elk volgend verzoek stuurt de browser de cookie mee +6. De server leest de cookie, checkt de token, en weet wie je bent + +> **Tim zegt:** +> "Het mooie van Supabase is dat dit allemaal automatisch gaat. Wij hoeven alleen de juiste packages te installeren en een paar bestanden aan te maken. Supabase regelt het hashen van wachtwoorden, het aanmaken van tokens, en het vernieuwen van verlopen tokens." + +**LIVE DEMO in Supabase Dashboard:** + +1. Ga naar **Authentication > Users** +2. Klik op **Add user > Create new user** +3. Vul een test email en wachtwoord in (bijv. `test@voorbeeld.nl` / `test1234`) +4. Klik op **Create user** + +> **Tim zegt:** +> "Kijk, ik heb nu handmatig een gebruiker aangemaakt in het dashboard. Dit is handig voor testen. Straks gaan we dit vanuit de app doen, maar het is goed om te weten dat je ook handmatig gebruikers kunt beheren." + +*Laat de aangemaakte user zien in de lijst. Wijs op de kolommen: email, created at, last sign in, etc.* + +--- + +### 09:45 – 10:00 | Theorie: Auth in Next.js + RLS (15 min) + +--- + +📊 **Slide 8 — Auth in Next.js (@supabase/ssr)** + +> **Tim zegt:** +> "Nu we weten hoe authenticatie werkt, moeten we het koppelen aan onze Next.js app. Daarvoor gebruiken we een package die `@supabase/ssr` heet. SSR staat voor Server-Side Rendering." + +> **Tim zegt:** +> "Waarom een speciale package? Omdat Next.js zowel op de server als in de browser draait. In de browser heb je gewoon toegang tot cookies. Maar op de server — als een pagina wordt gerenderd op de server — moet je cookies op een andere manier lezen en schrijven. `@supabase/ssr` regelt dat voor je." + +> **Tim zegt:** +> "We gaan straks drie belangrijke bestanden aanmaken:" + +Benoem de drie bestanden: + +1. **`src/lib/supabase/client.ts`** — De browser client. Wordt gebruikt in componenten die in de browser draaien (Client Components). +2. **`src/lib/supabase/server.ts`** — De server client. Wordt gebruikt in Server Components en Server Actions. +3. **`src/middleware.ts`** — Middleware die bij ELK verzoek draait. Controleert of de gebruiker is ingelogd en vernieuwt de sessie. + +> **Tim zegt:** +> "De middleware is het belangrijkste stuk. Die draait bij elk verzoek — elke keer als je een pagina opent. De middleware checkt: is er een geldige sessie? Zo niet, stuur de gebruiker naar de login pagina. Zo ja, laat het verzoek door." + +> **Tim zegt:** +> "Denk aan de middleware als een beveiliger bij de deur. Elke bezoeker wordt gecheckt voordat ze naar binnen mogen." + +> **Tim zegt:** +> "En dan is er nog de **auth callback route**. Als een gebruiker een magic link aanklikt, of terugkomt van een OAuth provider, komt die terecht op een speciale URL in je app. Die route wisselt de code om voor een sessie. Dat is een technisch detail, maar het is een bestand dat je nodig hebt." + +--- + +📊 **Slide 9 — Row Level Security (RLS)** + +> **Tim zegt:** +> "Het laatste theorieblok: Row Level Security, oftewel RLS. Dit is de autorisatie-kant van het verhaal." + +> **Tim zegt:** +> "RLS is een feature van de database zelf — niet van Next.js, niet van Supabase Auth, maar van PostgreSQL. Het betekent dat je regels kunt instellen op de database die bepalen wie welke rijen mag lezen, aanmaken, updaten of verwijderen." + +> **Tim zegt:** +> "Zonder RLS kan iedereen die de URL van je Supabase project kent alles doen met je data. Dat is een groot beveiligingsrisico. Met RLS zeg je: 'alleen ingelogde gebruikers mogen polls aanmaken' of 'alleen de eigenaar mag een poll verwijderen'." + +**LIVE DEMO in Supabase Dashboard:** + +1. Ga naar **Table Editor** en klik op de tabel `polls` +2. Wijs op het gele waarschuwingsicoontje naast de tabel (RLS is uitgeschakeld) +3. Klik op **RLS disabled** (of ga via **Authentication > Policies**) + +> **Tim zegt:** +> "Kijk hier — Supabase waarschuwt ons: RLS staat uit op deze tabel. Dat betekent dat iedereen alles kan doen. Dat gaan we straks fixen." + +> **Tim zegt:** +> "Een RLS policy is een regel die je schrijft in SQL. Het ziet er zo uit:" + +Schrijf op het whiteboard of laat op de slide zien: + +```sql +CREATE POLICY "Iedereen kan polls lezen" +ON polls FOR SELECT +USING (true); +``` + +> **Tim zegt:** +> "Dit zegt: voor de tabel `polls`, bij een `SELECT` query — dus bij het lezen — mag iedereen alles zien. `USING (true)` betekent: altijd waar, geen beperking." + +> **Tim zegt:** +> "Maar we kunnen ook zeggen: alleen ingelogde gebruikers mogen polls aanmaken:" + +```sql +CREATE POLICY "Ingelogde gebruikers maken polls" +ON polls FOR INSERT +TO authenticated +WITH CHECK (true); +``` + +> **Tim zegt:** +> "`TO authenticated` is het belangrijke deel. Dat is een speciale Supabase rol die alleen geldt voor ingelogde gebruikers. Als je niet bent ingelogd, krijg je de `anon` rol, en die heeft geen toestemming voor INSERT." + +> **Tim zegt:** +> "Er is ook een speciale functie: `auth.uid()`. Die geeft het ID van de ingelogde gebruiker. Daarmee kun je zeggen: je mag alleen je EIGEN data lezen of bewerken. Dat gaan we vandaag niet gebruiken in onze Poll App, maar het is goed om te weten voor je eindopdracht." + +> **Tim zegt:** +> "Oké, dat was veel theorie! Laten we even pauze houden en dan gaan we het allemaal bouwen." + +--- + +### 10:00 – 10:15 | Pauze (15 min) + +--- + +📊 **Slide 10 — Pauze** + +> **Tim zegt:** +> "We houden een kwartier pauze. Om kwart over 10 beginnen we met de hands-on. Zorg dat je laptop klaar staat met je project geopend in VS Code." + +--- + +## BLOK 2 — Hands-on (10:15 – 11:45) + +--- + +### 10:15 – 10:30 | Hands-on: Auth Opzetten in Supabase (15 min) + +--- + +📊 **Slide 11 — Hands-on: Auth Opzetten in Supabase** + +*Deze slide blijft zichtbaar terwijl studenten werken. Alle stappen staan erop.* + +> **Tim zegt:** +> "Oké, we gaan beginnen! Ik doe het voor op het scherm en jullie doen mee. We beginnen met het installeren van de packages en het aanmaken van de bestanden." + +> **Tim zegt:** +> "Open je terminal in VS Code — je weet hoe dat moet: Ctrl+backtick of via het menu. Zorg dat je in de root van je project staat." + +--- + +#### Stap 1: Installeer de packages + +> **Tim zegt:** +> "Allereerst installeren we twee packages. Type het volgende commando:" + +```bash +npm install @supabase/ssr @supabase/supabase-js +``` + +> **Tim zegt:** +> "Even wachten tot het klaar is... `@supabase/ssr` is de package die we nodig hebben voor authenticatie in Next.js. `@supabase/supabase-js` hebben jullie misschien al — dat is de standaard Supabase client. Als die al geinstalleerd is, wordt hij geüpdatet." + +*Wacht tot iedereen klaar is. Loop rond en check of er errors zijn.* + +--- + +#### Stap 2: Controleer je `.env.local` + +> **Tim zegt:** +> "Controleer even of je `.env.local` bestand de juiste variabelen bevat. Dit zou er al moeten staan van Les 8:" + +``` +NEXT_PUBLIC_SUPABASE_URL=https://jouw-project.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=jouw-anon-key +``` + +> **Tim zegt:** +> "Als je deze niet hebt, ga naar je Supabase dashboard, klik op Settings, dan API, en kopieer de URL en de anon key." + +--- + +#### Stap 3: Maak de mappenstructuur aan + +> **Tim zegt:** +> "Nu gaan we de bestanden aanmaken. Maak eerst de map aan als die nog niet bestaat:" + +```bash +mkdir -p src/lib/supabase +``` + +--- + +#### Stap 4: Browser Client (`src/lib/supabase/client.ts`) + +> **Tim zegt:** +> "Het eerste bestand is de browser client. Maak een nieuw bestand aan: `src/lib/supabase/client.ts`. Dit is de simpelste van de drie." + +```typescript +import { createBrowserClient } from '@supabase/ssr' + +export function createClient() { + return createBrowserClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! + ) +} +``` + +> **Tim zegt:** +> "Dit is een simpele functie die een Supabase client aanmaakt voor de browser. De `createBrowserClient` functie uit `@supabase/ssr` regelt automatisch dat cookies goed worden gelezen en geschreven in de browser. Die uitroeptekens achter `process.env` zijn TypeScript — ze zeggen: ik weet zeker dat deze waarde bestaat." + +--- + +#### Stap 5: Server Client (`src/lib/supabase/server.ts`) + +> **Tim zegt:** +> "Nu het server bestand. Maak `src/lib/supabase/server.ts` aan. Dit is iets complexer, maar jullie hoeven het niet uit je hoofd te kennen — je kunt het altijd kopiëren." + +```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) { + cookiesToSet.forEach(({ name, value, options }) => + cookieStore.set(name, value, options) + ) + }, + }, + } + ) +} +``` + +> **Tim zegt:** +> "Dit bestand is voor de server-kant van Next.js. Het gebruikt `cookies()` van `next/headers` — dat is een Next.js functie die je toegang geeft tot de cookies op de server. Merk op dat de functie `async` is — `cookies()` is asynchroon in Next.js 16." + +> **Tim zegt:** +> "Het `cookies` object dat we meegeven aan `createServerClient` vertelt Supabase hoe het cookies moet lezen met `getAll()` en schrijven met `setAll()`. Supabase gebruikt dit om de sessie bij te houden." + +--- + +#### Stap 6: Middleware (`src/middleware.ts`) + +> **Tim zegt:** +> "Nu het belangrijkste bestand: de middleware. Maak `src/middleware.ts` aan — let op, dit bestand staat in de `src` map, NIET in `src/lib/supabase`. Het is een speciaal Next.js bestand." + +```typescript +import { createServerClient } from '@supabase/ssr' +import { NextResponse, type NextRequest } from 'next/server' + +export async function middleware(request: NextRequest) { + let supabaseResponse = 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) + ) + supabaseResponse = NextResponse.next({ + request, + }) + cookiesToSet.forEach(({ name, value, options }) => + supabaseResponse.cookies.set(name, value, options) + ) + }, + }, + } + ) + + // Belangrijk: haal de user op om de sessie te vernieuwen + const { + data: { user }, + } = await supabase.auth.getUser() + + // Als er geen user is en we zijn niet op de login pagina, redirect naar login + if ( + !user && + !request.nextUrl.pathname.startsWith('/login') && + !request.nextUrl.pathname.startsWith('/auth') + ) { + const url = request.nextUrl.clone() + url.pathname = '/login' + return NextResponse.redirect(url) + } + + return supabaseResponse +} + +export const config = { + matcher: [ + '/((?!_next/static|_next/image|favicon.ico|login|auth).*)', + ], +} +``` + +> **Tim zegt:** +> "Dit is het langste bestand, maar het is heel logisch als je het stap voor stap bekijkt." + +> **Tim zegt:** +> "Bovenaan maken we een Supabase client aan, net als bij de server client, maar dan met de cookies van het request object. Dan halen we de user op met `getUser()`. Dit doet twee dingen: het controleert of er een geldige sessie is, en het vernieuwt de sessie als die bijna verlopen is." + +> **Tim zegt:** +> "Dan de if-statement: als er geen user is EN we zijn niet al op de login pagina, dan redirecten we naar `/login`. Simpel." + +> **Tim zegt:** +> "Onderaan staat de `matcher` config. Die bepaalt voor welke URLs de middleware draait. We sluiten statische bestanden uit (`_next/static`, `_next/image`, `favicon.ico`) en de login en auth pagina's zelf — anders zou je nooit op de login pagina kunnen komen!" + +*Loop rond en help studenten die vastlopen. Veel voorkomende problemen:* +- *Bestand op de verkeerde plek (middleware.ts moet in `src/`, niet in `src/app/`)* +- *Typfouten in de imports* +- *Missende `.env.local` variabelen* + +> **Tim zegt:** +> "Is iedereen zover? Vier bestanden aangemaakt? Als je ergens vastloopt, steek je hand op. We gaan zo door met de login pagina." + +--- + +### 10:30 – 10:55 | Hands-on: Login & Registratie Bouwen (25 min) + +--- + +📊 **Slide 12 — Hands-on: Login & Registratie Bouwen** + +*Deze slide blijft zichtbaar met alle stappen.* + +> **Tim zegt:** +> "Nu gaan we de login pagina bouwen. We maken een pagina waar gebruikers kunnen inloggen met email en wachtwoord, of zich kunnen registreren." + +--- + +#### Stap 1: Auth Callback Route aanmaken + +> **Tim zegt:** +> "Eerst hebben we de auth callback route nodig. Die is nodig voor magic links. Maak de volgende map en bestand aan:" + +```bash +mkdir -p src/app/auth/callback +``` + +Maak `src/app/auth/callback/route.ts` aan: + +```typescript +import { createClient } from '@/lib/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) +} +``` + +> **Tim zegt:** +> "Dit is een API route — een route die geen pagina toont, maar een verzoek afhandelt. Als een gebruiker op een magic link klikt, stuurt Supabase ze naar `/auth/callback?code=abc123`. Deze route pakt die code, wisselt het om voor een sessie, en redirect de gebruiker naar de homepage." + +--- + +#### Stap 2: Login pagina aanmaken + +> **Tim zegt:** +> "Nu de login pagina zelf. Maak de map en het bestand aan:" + +```bash +mkdir -p src/app/login +``` + +Maak `src/app/login/page.tsx` aan: + +```typescript +'use client' + +import { createClient } from '@/lib/supabase/client' +import { useRouter } from 'next/navigation' +import { useState } from 'react' + +export default function LoginPage() { + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [loading, setLoading] = useState(false) + const [message, setMessage] = useState('') + const [isSignUp, setIsSignUp] = useState(false) + const router = useRouter() + const supabase = createClient() + + const handleEmailLogin = async (e: React.FormEvent) => { + e.preventDefault() + setLoading(true) + setMessage('') + + if (isSignUp) { + const { error } = await supabase.auth.signUp({ + email, + password, + }) + if (error) { + setMessage(error.message) + } else { + setMessage('Check je email voor een bevestigingslink!') + } + } else { + const { error } = await supabase.auth.signInWithPassword({ + email, + password, + }) + if (error) { + setMessage(error.message) + } else { + router.push('/') + router.refresh() + } + } + + setLoading(false) + } + + const handleMagicLink = async () => { + if (!email) { + setMessage('Vul eerst je email in') + return + } + setLoading(true) + setMessage('') + + const { error } = await supabase.auth.signInWithOtp({ + email, + }) + + if (error) { + setMessage(error.message) + } else { + setMessage('Check je email voor een magic link!') + } + + setLoading(false) + } + + return ( +
+ Poll App +
++ Poll App +
+1'bT:(MGHZGEI-em0^4R)Er9t)K=-Zu%!k9>(nLX1KAQL^!62^9GGG/upE+/UagI":dUq,6iaH?N8gnJtX^^9^chaTMm*HS"3Adg3ZHKbFia3GKKV=0iY`4R!c^$>n&f`_(8AR%N*Y9>&0K,$YReV-K9dmZ2e>:+<.tL?r,DXSHoJughI8&RD?p$&>Xs1M6(1N/aI">A=r&a[B!+2qg1QMRE3k5u)?u??h5MKB8;G%VSk%EBfqtjrf@S"-*=c#u/?7#t)'p[:FD_@3Hh^FV80h7BO-Wb[m==H_B@n>+3(hRIT*b(uo0Q*A0GZkaUACN]4o\f(Xcp.=HSt)4"*&i$YM(:cL0j20?r(jLNO*jh83uo)P]K.(,1(JUXPHsT3\3GA#bGoY/5!1FYd^11Ej#go/@M,id7%1qal=\Ci;@GckW:WXpb2\Ib@T"/i\:,4=a3X6ocVf endstream
+endobj
+xref
+0 33
+0000000000 65535 f
+0000000061 00000 n
+0000000122 00000 n
+0000000229 00000 n
+0000000341 00000 n
+0000000424 00000 n
+0000000629 00000 n
+0000000734 00000 n
+0000000939 00000 n
+0000001144 00000 n
+0000001349 00000 n
+0000001555 00000 n
+0000001761 00000 n
+0000001967 00000 n
+0000002173 00000 n
+0000002379 00000 n
+0000002585 00000 n
+0000002791 00000 n
+0000002997 00000 n
+0000003067 00000 n
+0000003348 00000 n
+0000003486 00000 n
+0000004518 00000 n
+0000006380 00000 n
+0000007762 00000 n
+0000009506 00000 n
+0000011194 00000 n
+0000012460 00000 n
+0000013852 00000 n
+0000016000 00000 n
+0000017638 00000 n
+0000019995 00000 n
+0000021689 00000 n
+trailer
+<<
+/ID
+[f5boNs,-#c]PrfnB.\OKB7>D!nVFC;GO_TI1V'')ONI`nNIHG!T6)jpn/Q*jaE1\=PtHr6[[L^![K'%j">s58;9/b6)QAeo3SPqDj1Zd>O+f_MK=.f66Ja^,TZ6E"sFJL[A@%Uga0ZkLb>ROs5VS[%rdh'\epI6/:kRY>S"k$>VbHXl%c%_U54_*L[[c?lQhroF]ptaM^u;^@75>%(Q>*JhTGa
@\Ub-iYD8^hY=RZp65.g1*]h>SRM5X61eBY\*-nh`X[;a.2M)L2k[$LY\-$(Xi^-Y2q@PtV;85e=0M]up^0Wc$I(/i2O9StSq\#QH5T3gR[X$!\U
3/OOgol&hLK_$aWr_3T`SD2CPh=%1SOCiM7n)_4kj1Dj\ZF;O%L%.5N]#!:b$lGEm6)!t[hT
E/<'EE2=6_j/.Tk;3+0N,_`X-EM";!A"u:,S'hT4UCVDZup6&5GP>VSLE'dn>g?jg>r2]D11g/DZ7!)1Z]gk=q