# 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 (
{isSignUp ? 'Account aanmaken' : 'Inloggen'}
Poll App
)
}
```
> **Tim zegt:**
> "Dit is een best groot bestand, maar het is eigenlijk vrij simpel. Laten we het doorlopen:"
> **Tim zegt:**
> "Bovenaan staan onze state variabelen: email, password, loading, message, en of we in sign-up of login modus zitten."
> **Tim zegt:**
> "De `handleEmailLogin` functie doet twee dingen: als `isSignUp` true is, roepen we `supabase.auth.signUp()` aan. Anders roepen we `supabase.auth.signInWithPassword()` aan. Bij succes redirecten we naar de homepage."
> **Tim zegt:**
> "De `handleMagicLink` functie roept `supabase.auth.signInWithOtp()` aan — die stuurt een magic link naar het email adres."
> **Tim zegt:**
> "De rest is gewoon een formulier met Tailwind styling. Twee input velden, twee buttons, en een toggle om te wisselen tussen inloggen en registreren."
*Loop rond terwijl studenten het overtikken of kopiëren.*
---
#### Stap 3: Email bevestiging uitschakelen (voor ontwikkeling)
> **Tim zegt:**
> "Een belangrijk ding! Standaard stuurt Supabase een bevestigingsmail als je je registreert. Dat is goed voor productie, maar vervelend voor development. We gaan dat even uitschakelen."
**LIVE DEMO in Supabase Dashboard:**
1. Ga naar **Authentication** > **Providers** > **Email**
2. Schakel **Confirm email** uit (toggle)
3. Klik op **Save**
> **Tim zegt:**
> "Nu kunnen jullie je registreren zonder een email te hoeven bevestigen. In een echte app laat je dit aan staan!"
---
#### Stap 4: Testen
> **Tim zegt:**
> "Start je dev server als die nog niet draait:"
```bash
npm run dev
```
> **Tim zegt:**
> "Ga naar `http://localhost:3000`. Je zou nu automatisch naar `/login` gestuurd moeten worden — dat is de middleware die je redirect! Registreer een account met je eigen email en een wachtwoord. Gebruik minimaal 6 tekens voor het wachtwoord."
*Doe de registratie zelf LIVE voor op het scherm.*
> **Tim zegt:**
> "Als het goed is, ben je nu ingelogd en zie je je Poll App. Ga terug naar het Supabase dashboard, naar Authentication > Users, en je ziet je nieuwe gebruiker staan!"
*Laat in het dashboard de nieuwe user zien.*
*Voorkomende problemen:*
- *"Invalid login credentials" — wachtwoord te kort (min 6 tekens)*
- *"User already registered" — gebruik een ander emailadres*
- *Redirect loop — middleware config klopt niet, check de matcher*
- *404 op /login — bestand op verkeerde plek*
---
### 10:55 – 11:10 | Hands-on: Sessie & Beschermde Routes (15 min)
---
📊 **Slide 13 — Hands-on: Sessie & Beschermde Routes**
*Slide blijft zichtbaar met alle stappen.*
> **Tim zegt:**
> "We zijn ingelogd! Maar de gebruiker weet dat niet — er is niets in de UI dat laat zien dat je bent ingelogd. Laten we dat toevoegen: een welkomstbericht met het emailadres en een uitlog-knop."
---
#### Stap 1: Navbar component aanmaken
> **Tim zegt:**
> "We gaan een simpele navbar maken die de gebruikersinformatie laat zien. Maak een nieuw bestand aan:"
```bash
mkdir -p src/components
```
Maak `src/components/Navbar.tsx` aan:
```typescript
'use client'
import { createClient } from '@/lib/supabase/client'
import { useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'
import type { User } from '@supabase/supabase-js'
export default function Navbar() {
const [user, setUser] = useState(null)
const router = useRouter()
const supabase = createClient()
useEffect(() => {
const getUser = async () => {
const { data: { user } } = await supabase.auth.getUser()
setUser(user)
}
getUser()
}, [])
const handleSignOut = async () => {
await supabase.auth.signOut()
router.push('/login')
router.refresh()
}
return (
)
}
```
> **Tim zegt:**
> "Dit is een Client Component — het draait in de browser. We gebruiken `useEffect` om de ingelogde user op te halen als de component laadt. Dan tonen we het emailadres en een uitlog-knop."
> **Tim zegt:**
> "De `handleSignOut` functie roept `supabase.auth.signOut()` aan en stuurt de gebruiker terug naar de login pagina."
---
#### Stap 2: Navbar toevoegen aan de layout
> **Tim zegt:**
> "Nu moeten we de Navbar toevoegen aan onze app. Open `src/app/layout.tsx` en importeer de Navbar component."
Pas `src/app/layout.tsx` aan:
```typescript
import Navbar from '@/components/Navbar'
```
En voeg de Navbar toe aan de body, boven `{children}`:
```tsx
{children}
```
> **Tim zegt:**
> "Let op: als je al een layout hebt met wat Tailwind classes, pas dan alleen de Navbar import en het component toe. Verwijder niet je bestaande styling."
> **Tim zegt:**
> "We willen de Navbar niet tonen op de login pagina. Er zijn meerdere manieren om dat op te lossen, maar de simpelste is: de Navbar checkt zelf of er een user is. Als er geen user is, toont hij niets. En dat doen we al — we tonen de user info alleen als `user` niet null is."
---
#### Stap 3: Testen
> **Tim zegt:**
> "Ga naar je browser en refresh de pagina. Je zou nu bovenaan je emailadres moeten zien en een uitlog-knop."
*Doe het LIVE voor:*
1. Laat de pagina zien met Navbar
2. Klik op "Uitloggen"
3. Laat zien dat je naar de login pagina wordt gestuurd
4. Probeer naar `http://localhost:3000` te gaan — je wordt terug gestuurd naar login
5. Log opnieuw in
> **Tim zegt:**
> "Kijk, de middleware beschermt alles! Als je niet bent ingelogd, kun je niet bij de app. Dat is beschermde routes. Probeer het zelf: log uit en probeer direct naar localhost:3000 te navigeren. Je wordt automatisch naar de login gestuurd."
*Loop rond en help studenten. Voorkomende problemen:*
- *Navbar verschijnt niet — check de import en de layout.tsx*
- *Witte pagina — check de browser console voor errors*
- *User email toont niet — `getUser()` kan even duren, daardoor kort `null`*
---
### 11:10 – 11:25 | Hands-on: Basis RLS (15 min)
---
📊 **Slide 14 — Hands-on: Basis RLS**
*Slide blijft zichtbaar met alle stappen.*
> **Tim zegt:**
> "De laatste stap vandaag: Row Level Security. We gaan ervoor zorgen dat onze database beveiligd is. Op dit moment kan iedereen met de anon key alles doen met onze data. Dat gaat nu veranderen."
---
#### Stap 1: RLS inschakelen
> **Tim zegt:**
> "We gaan dit doen in het Supabase dashboard. Ga naar de SQL Editor — dat is het icoon met het database-symbool in de linkerzijbalk."
**LIVE DEMO in Supabase Dashboard:**
1. Ga naar **SQL Editor**
2. Klik op **New query**
> **Tim zegt:**
> "We gaan eerst RLS inschakelen op beide tabellen. Type het volgende SQL:"
```sql
-- Stap 1: RLS inschakelen op polls tabel
ALTER TABLE polls ENABLE ROW LEVEL SECURITY;
-- Stap 2: RLS inschakelen op options tabel
ALTER TABLE options ENABLE ROW LEVEL SECURITY;
```
> **Tim zegt:**
> "Klik op Run. Als het goed is, zie je 'Success' zonder errors."
*Voer de query LIVE uit.*
> **Tim zegt:**
> "Nu is er iets belangrijks: als je nu naar je app gaat en probeert polls te laden, zul je NIETS zien. Dat komt omdat RLS standaard alles blokkeert. Als er geen policies zijn die iets toestaan, mag niemand iets. Probeer het maar even — refresh je app."
*Laat zien dat de polls verdwenen zijn.*
> **Tim zegt:**
> "Zie je? Geen polls meer! Dat is RLS in actie. We moeten nu policies aanmaken die bepalen wie wat mag."
---
#### Stap 2: Policies aanmaken voor polls
> **Tim zegt:**
> "Maak een nieuwe query aan en type het volgende:"
```sql
-- Iedereen kan polls lezen (ook niet-ingelogde gebruikers)
CREATE POLICY "Polls are viewable by everyone"
ON polls FOR SELECT
USING (true);
-- Alleen ingelogde gebruikers kunnen polls aanmaken
CREATE POLICY "Authenticated users can create polls"
ON polls FOR INSERT
TO authenticated
WITH CHECK (true);
```
> **Tim zegt:**
> "De eerste policy zegt: voor SELECT queries op de polls tabel, mag iedereen alles zien. `USING (true)` betekent: geen beperking."
> **Tim zegt:**
> "De tweede policy zegt: voor INSERT queries, alleen de `authenticated` rol mag dat. `TO authenticated` is de sleutel — dat is de rol die je krijgt als je bent ingelogd via Supabase Auth. Als je niet bent ingelogd, ben je `anon`, en dan mag je geen polls aanmaken."
*Voer de query LIVE uit.*
---
#### Stap 3: Policies aanmaken voor options
> **Tim zegt:**
> "Nu hetzelfde voor de options tabel:"
```sql
-- Iedereen kan options lezen
CREATE POLICY "Options are viewable by everyone"
ON options FOR SELECT
USING (true);
-- Ingelogde gebruikers kunnen stemmen (update)
CREATE POLICY "Authenticated users can vote"
ON options FOR UPDATE
TO authenticated
USING (true);
```
*Voer de query LIVE uit.*
> **Tim zegt:**
> "Nu kan iedereen polls en opties zien, maar alleen ingelogde gebruikers kunnen nieuwe polls aanmaken en stemmen."
---
#### Stap 4: Optioneel — INSERT policy voor options
> **Tim zegt:**
> "We hebben ook een INSERT policy nodig voor options, anders kunnen we geen nieuwe opties aanmaken bij een poll:"
```sql
-- Ingelogde gebruikers kunnen opties aanmaken
CREATE POLICY "Authenticated users can create options"
ON options FOR INSERT
TO authenticated
WITH CHECK (true);
```
*Voer de query LIVE uit.*
---
#### Stap 5: Testen
> **Tim zegt:**
> "Ga terug naar je app en refresh. Je polls zouden er nu weer moeten zijn!"
*Doe het LIVE voor:*
1. Refresh de app — polls verschijnen weer
2. Maak een nieuwe poll aan — werkt!
3. Stem op een optie — werkt!
> **Tim zegt:**
> "Mooi! Alles werkt weer, maar nu met beveiliging. Laten we even testen wat er gebeurt als je niet bent ingelogd."
**LIVE DEMO — RLS testen:**
1. Ga naar het Supabase dashboard
2. Ga naar **Table Editor** > **polls**
3. Wijs op het groene vinkje bij de tabel (RLS is nu ingeschakeld)
4. Ga naar **Authentication > Policies** — laat alle policies zien
> **Tim zegt:**
> "Hier in het dashboard kun je al je policies zien en beheren. Je kunt ze hier ook bewerken of verwijderen als dat nodig is."
> **Tim zegt:**
> "Een belangrijk punt: de policies die we nu hebben zijn vrij simpel. In je eindopdracht kun je veel specifiekere policies schrijven. Bijvoorbeeld: een gebruiker mag alleen zijn eigen data bewerken. Dan gebruik je `auth.uid()` in je policy. Dat ziet er dan zo uit:"
```sql
-- Voorbeeld voor de eindopdracht (niet nodig voor poll app):
CREATE POLICY "Users can only edit own posts"
ON posts FOR UPDATE
TO authenticated
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
```
> **Tim zegt:**
> "Hier zegt `auth.uid() = user_id` dat de ingelogde gebruiker alleen rijen mag updaten waar het `user_id` veld gelijk is aan zijn eigen gebruikers-ID. Super krachtig!"
---
#### Stap 6: Policies bekijken in het dashboard
> **Tim zegt:**
> "Laat me even laten zien hoe je alle policies kunt bekijken."
**LIVE DEMO:**
1. Ga naar **Authentication** > **Policies**
2. Laat de lijst van policies zien per tabel
3. Klik op een policy om de details te zien
> **Tim zegt:**
> "Hier zie je per tabel welke policies er zijn. Je kunt ze ook via dit dashboard aanmaken door op 'New Policy' te klikken. Er zijn templates beschikbaar die het makkelijker maken. Maar voor nu is de SQL Editor prima."
---
### 11:25 – 11:45 | Doorwerken (20 min)
---
> **Tim zegt:**
> "Oké, de rest van de tijd is om alles af te maken en te testen. Veel van jullie zullen nog bezig zijn met het overnemen van de code. Neem de tijd en zorg dat alles werkt. Ik loop rond om te helpen."
*Laat slides 11-14 op het scherm staan zodat studenten kunnen terugkijken.*
**Checklist voor studenten (noem op):**
1. Zijn alle bestanden aangemaakt?
- `src/lib/supabase/client.ts`
- `src/lib/supabase/server.ts`
- `src/middleware.ts`
- `src/app/auth/callback/route.ts`
- `src/app/login/page.tsx`
- `src/components/Navbar.tsx`
2. Kun je registreren met email + wachtwoord?
3. Kun je inloggen?
4. Zie je je emailadres in de Navbar?
5. Werkt uitloggen?
6. Word je geredirect naar login als je niet bent ingelogd?
7. Kun je nog steeds polls zien en aanmaken?
8. Is RLS ingeschakeld op beide tabellen?
> **Tim zegt:**
> "Als je alles werkend hebt en nog tijd over hebt, probeer dan het volgende: log uit en open een incognito venster. Ga naar je app. Kun je polls ZIEN? Kun je polls AANMAKEN? Wat verwacht je dat er gebeurt?"
*Het verwachte resultaat: je wordt geredirect naar de login pagina door de middleware, dus je kunt niets doen. Als je de middleware even uitzet en direct de API probeert, zou SELECT werken maar INSERT niet dankzij RLS.*
*Loop rond en help actief. Veel voorkomende problemen:*
- *Studenten die de middleware op de verkeerde plek hebben gezet*
- *Typfouten in de import paden (bijv. `@/lib/supabase/client` vs `@/lib/supabase/server`)*
- *Vergeten om email bevestiging uit te schakelen in het dashboard*
- *Wachtwoord te kort bij registratie*
- *RLS ingeschakeld maar geen policies — dan werkt niets*
---
## BLOK 3 — Afsluiting (11:45 – 12:00)
---
### 11:45 – 12:00 | Samenvatting + Huiswerk (15 min)
---
📊 **Slide 15 — Samenvatting + Huiswerk**
> **Tim zegt:**
> "Laten we even samenvatten wat we vandaag geleerd hebben."
> **Tim zegt:**
> "We hebben het gehad over het verschil tussen **authenticatie** — wie ben je — en **autorisatie** — wat mag je. We hebben geleerd dat Supabase een ingebouwd authenticatiesysteem heeft met drie methodes: email + wachtwoord, magic links, en social login."
> **Tim zegt:**
> "We hebben `@supabase/ssr` geinstalleerd en drie belangrijke bestanden aangemaakt: de browser client, de server client, en de middleware. De middleware is de bewaker die bij elk verzoek checkt of je bent ingelogd."
> **Tim zegt:**
> "We hebben een login pagina gebouwd met registratie, inloggen, en magic link functionaliteit. We hebben een Navbar toegevoegd die het emailadres toont en een uitlog-knop."
> **Tim zegt:**
> "En als laatste hebben we Row Level Security ingeschakeld. We hebben policies geschreven die bepalen: iedereen mag lezen, maar alleen ingelogde gebruikers mogen aanmaken en bewerken."
> **Tim zegt:**
> "Dit zijn de bouwstenen die je nodig hebt voor je eindopdracht. Elke echte app heeft gebruikers en beveiliging nodig."
---
#### Huiswerk
> **Tim zegt:**
> "Voor het huiswerk:"
Benoem de volgende punten:
1. **Zorg dat je Poll App volledig werkt** met authenticatie en RLS. Alles wat we vandaag gedaan hebben moet werken: registreren, inloggen, uitloggen, beschermde routes, en RLS.
2. **Voeg een `user_id` kolom toe aan de `polls` tabel.** Ga naar de Table Editor in Supabase, voeg een kolom toe van type `uuid`, en koppel die aan `auth.users(id)`. Als je dan een poll aanmaakt, sla je op wie de poll heeft gemaakt. Pas daarna je INSERT code aan om `user_id` mee te sturen.
3. **Begin na te denken over je eindopdracht.** Wat voor app wil je bouwen? Welke tabellen heb je nodig? Welke pagina's? Schrijf een korte beschrijving van je idee — maximaal een half A4.
> **Tim zegt:**
> "Punt 2 is een uitdaging, maar het is een goede oefening. Je moet de tabel aanpassen in Supabase, en je code aanpassen in Next.js. Tip: gebruik `auth.uid()` in je RLS policy om ervoor te zorgen dat gebruikers alleen hun eigen polls kunnen bewerken."
> **Tim zegt:**
> "En punt 3: begin echt na te denken over je eindopdracht. Volgende les gaan we het er uitgebreider over hebben en kun je je idee pitchen."
---
#### Vragen
> **Tim zegt:**
> "Zijn er nog vragen over vandaag?"
*Neem de tijd voor vragen. Veel voorkomende vragen:*
**V: Moet ik magic link ook werkend hebben voor de eindopdracht?**
> **Tim zegt:**
> "Nee, email + wachtwoord is voldoende. Magic link is een extra optie die handig is, maar niet verplicht."
**V: Hoe werkt `auth.uid()` precies?**
> **Tim zegt:**
> "`auth.uid()` is een functie in PostgreSQL die Supabase toevoegt. Het geeft het UUID terug van de ingelogde gebruiker. Als niemand is ingelogd, geeft het NULL terug. Je kunt het gebruiken in RLS policies om te checken of een rij van de ingelogde gebruiker is."
**V: Kan ik Google login gebruiken voor mijn eindopdracht?**
> **Tim zegt:**
> "Ja, dat kan! Maar het is iets complexer om op te zetten. Je moet een project aanmaken in de Google Cloud Console en de credentials toevoegen in Supabase. Als je dat wilt doen, help ik je er graag mee buiten de les."
**V: Wat als ik vergeet RLS in te schakelen?**
> **Tim zegt:**
> "Dan is je data niet beveiligd. Iedereen met je Supabase URL en anon key kan al je data lezen, aanmaken, verwijderen... alles. In je eindopdracht is RLS verplicht."
---
> **Tim zegt:**
> "Goed werk vandaag allemaal! Jullie hebben in drie uur een compleet authenticatiesysteem gebouwd. Dat is niet niks. Volgende les gaan we verder met de eindopdracht-planning. Zorg dat je huiswerk af is en neem je eindopdracht-idee mee. Fijne dag!"
---
## Referentie: Alle Code in Overzicht
Onderstaande code is een compleet overzicht van alle bestanden die in deze les worden aangemaakt. Gebruik dit als naslagwerk of als studenten achterop raken.
---
### Bestand 1: `src/lib/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!
)
}
```
---
### Bestand 2: `src/lib/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) {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
)
},
},
}
)
}
```
---
### Bestand 3: `src/middleware.ts`
```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)
)
},
},
}
)
const {
data: { user },
} = await supabase.auth.getUser()
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).*)',
],
}
```
---
### Bestand 4: `src/app/auth/callback/route.ts`
```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)
}
```
---
### Bestand 5: `src/app/login/page.tsx`
```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 (
{isSignUp ? 'Account aanmaken' : 'Inloggen'}
Poll App
)
}
```
---
### Bestand 6: `src/components/Navbar.tsx`
```typescript
'use client'
import { createClient } from '@/lib/supabase/client'
import { useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'
import type { User } from '@supabase/supabase-js'
export default function Navbar() {
const [user, setUser] = useState(null)
const router = useRouter()
const supabase = createClient()
useEffect(() => {
const getUser = async () => {
const { data: { user } } = await supabase.auth.getUser()
setUser(user)
}
getUser()
}, [])
const handleSignOut = async () => {
await supabase.auth.signOut()
router.push('/login')
router.refresh()
}
return (
)
}
```
---
### SQL: RLS Policies
```sql
-- RLS inschakelen
ALTER TABLE polls ENABLE ROW LEVEL SECURITY;
ALTER TABLE options ENABLE ROW LEVEL SECURITY;
-- Polls policies
CREATE POLICY "Polls are viewable by everyone"
ON polls FOR SELECT
USING (true);
CREATE POLICY "Authenticated users can create polls"
ON polls FOR INSERT
TO authenticated
WITH CHECK (true);
-- Options policies
CREATE POLICY "Options are viewable by everyone"
ON options FOR SELECT
USING (true);
CREATE POLICY "Authenticated users can create options"
ON options FOR INSERT
TO authenticated
WITH CHECK (true);
CREATE POLICY "Authenticated users can vote"
ON options FOR UPDATE
TO authenticated
USING (true);
```
---
## Notities voor de Docent
### Voorbereiding voor de les
- Test of het Supabase project bereikbaar is
- Zorg dat je eigen Poll App werkend is met auth (als demo)
- Zet een fallback account klaar in het dashboard voor als live registratie niet lukt
- Check of je de Email provider aan hebt staan in het dashboard
- Schakel email bevestiging alvast uit voor de demo
### Veel voorkomende problemen
| Probleem | Oorzaak | Oplossing |
|----------|---------|-----------|
| Redirect loop op /login | Middleware matched ook de login route | Check de `matcher` config in middleware.ts |
| "Invalid login credentials" | Wachtwoord te kort of verkeerd | Minimaal 6 tekens, probeer opnieuw |
| "User already registered" | Email al in gebruik | Gebruik een ander emailadres of log in |
| Polls verdwenen na RLS | RLS aan, maar geen policies | Voeg de SELECT policies toe |
| 404 op /login | Bestand op verkeerde plek | Moet in `src/app/login/page.tsx` |
| Middleware werkt niet | Bestand op verkeerde plek | Moet in `src/middleware.ts` (niet in `src/app/`) |
| Cookie errors in terminal | `cookies()` niet ge-awaited | Zorg dat `createClient()` in server.ts `async` is |
| Magic link komt niet aan | Supabase rate limit of email config | Gebruik email+wachtwoord als fallback |
### Tips voor het rondlopen
- Begin bij studenten die er stil bij zitten — zij zitten vaak vast
- Check altijd eerst of de bestanden op de juiste plek staan
- Veel errors komen door typfouten in import paden
- Als een student helemaal vast zit: laat ze de code uit de referentie-sectie kopiëren
- Moedig het gebruik van AI-tools aan (ChatGPT, Copilot) om errors op te lossen
### Verbinding met de eindopdracht
Benoem regelmatig de verbinding met de eindopdracht:
- "Dit heb je ook nodig in je eindopdracht"
- "In je eigen app kun je dit uitbreiden met..."
- "RLS is verplicht voor je eindopdracht"
- "Denk alvast na over welke tabellen JIJ nodig hebt en welke policies daarbij horen"