894 lines
26 KiB
Markdown
894 lines
26 KiB
Markdown
# Les 8 — Docenttekst
|
||
## Supabase × Next.js + Auth
|
||
|
||
---
|
||
|
||
## Lesoverzicht
|
||
|
||
| Gegeven | Details |
|
||
|---------|---------|
|
||
| **Les** | 8 van 18 |
|
||
| **Onderwerp** | Supabase koppelen + Auth introductie |
|
||
| **Duur** | 3 uur (09:00 – 12:00) |
|
||
| **Voorbereiding** | Werkend QuickPoll project, Supabase project met polls/options tabellen, RLS ingesteld |
|
||
| **Benodigdheden** | Laptop, Cursor/VS Code, browser, Supabase account |
|
||
|
||
---
|
||
|
||
## Leerdoelen
|
||
|
||
Na deze les kunnen studenten:
|
||
1. De Supabase JavaScript client gebruiken in een Next.js project
|
||
2. Data ophalen via Supabase queries (select met relaties, eq, single)
|
||
3. Het Server Component + Client Component patroon toepassen
|
||
4. Uitleggen wat authenticatie vs autorisatie is
|
||
5. Supabase Auth functies gebruiken (signUp, signIn, signOut, getUser)
|
||
6. Een login/registratie flow bouwen in Next.js
|
||
|
||
---
|
||
|
||
## Lesvoorbereiding (voor docent)
|
||
|
||
Zorg dat je volgende zaken hebt voorbereiding:
|
||
- Een werkend Supabase project met `polls` en `options` tabellen (uit Les 7)
|
||
- RLS ingeschakeld op beide tabellen met policies voor SELECT (anon) en UPDATE (anon op options)
|
||
- De Next.js QuickPoll app uit Les 7 werkend op je machine
|
||
- De slides gereed voor uitleg authenticatie vs autorisatie
|
||
- Test je eigen Supabase credentials vooraf
|
||
|
||
---
|
||
|
||
## 09:00–09:10 | Welkom & Terugblik (10 min)
|
||
|
||
**Doel:** Studenten krijgen duidelijk wat we vandaag doen en waar we van vorige week waren.
|
||
|
||
### Wat we hebben gedaan in Les 7:
|
||
- ✅ Stemmen werkend gemaakt (votePoll functie, state update in poll detail page)
|
||
- ✅ Supabase introductie: account aangemaakt, project gemaakt
|
||
- ✅ Database: polls + options tabellen aangemaakt
|
||
- ✅ Foreign keys + CASCADE ingesteld
|
||
- ✅ RLS policies ingesteld (SELECT voor anon, UPDATE voor anon op options)
|
||
- ✅ Testdata ingevoerd via Table Editor
|
||
|
||
### Wat we NIET hebben afgemaakt in Les 7:
|
||
- ❌ Supabase is NIET aan het Next.js project gekoppeld
|
||
- ❌ Data wordt nog niet uit Supabase opgehaald
|
||
- ❌ Geen authenticatie
|
||
|
||
### Vandaag gaan we:
|
||
1. **DEEL 1 (65 min):** Supabase client installeren en opzetten → data uit database halen in Next.js
|
||
2. **DEEL 2a (30 min):** Uitleg over authenticatie, autorisatie en Supabase Auth features
|
||
3. **DEEL 2b (30 min):** Studenten bouwen auth zelf in hun project (signup, login, logout)
|
||
|
||
**Motivatie:** "Tot nu toe zijn je polls hardcoded in geheugen. Straks halen we echte data uit Supabase en kunnen people inloggen. Dat is een echt web app!"
|
||
|
||
---
|
||
|
||
## 09:10–10:15 | DEEL 1: Supabase Koppelen — Live Coding (65 min)
|
||
|
||
Dit deel volgt een stap-voor-stap aanpak met live coding. Alle studenten coderen mee.
|
||
|
||
### 09:10–09:15 | Installatie (5 min)
|
||
|
||
Open terminal in het QuickPoll project en run:
|
||
|
||
```bash
|
||
npm install @supabase/supabase-js
|
||
```
|
||
|
||
**Teacher Tip:** Controleer dat de installatie slaagt. Als students `npm ERR!` zien, laat ze eerst `npm clean-install` doen en daarna opnieuw proberen.
|
||
|
||
### 09:15–09:25 | Environment Variables (10 min)
|
||
|
||
Zorg dat alle studenten hun Supabase credentials veilig opslaan.
|
||
|
||
1. Open in Supabase Dashboard: **Settings** → **API**
|
||
2. Kopieer:
|
||
- `Project URL` (eindigt op `.supabase.co`)
|
||
- `anon` public key
|
||
|
||
3. Maak/open `.env.local` in je Next.js project root:
|
||
|
||
```env
|
||
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
|
||
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here
|
||
```
|
||
|
||
**Belangrijk:**
|
||
- `.env.local` staat al in `.gitignore` (check even)
|
||
- Keys die beginnen met `NEXT_PUBLIC_` zijn zichtbaar in browser (maar anon keys zijn daarvoor bedoeld)
|
||
- **ALTIJD de dev server herstarten na wijzigen van `.env.local`** (Ctrl+C, dan `npm run dev`)
|
||
|
||
**Teacher Tip:** Dit is een veelvoorkomende fout. Zeg hardop: "Als jullie een leeg array zien in plaats van polls, check EERST of je dev server herstarten hebt!"
|
||
|
||
### 09:25–09:35 | Supabase Client aanmaken (10 min)
|
||
|
||
Maak `lib/supabase.ts`:
|
||
|
||
```typescript
|
||
import { createClient } from '@supabase/supabase-js'
|
||
|
||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
|
||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||
|
||
export const supabase = createClient(supabaseUrl, supabaseKey)
|
||
```
|
||
|
||
**Wat gebeurt hier:**
|
||
- We importeren `createClient` uit `@supabase/supabase-js`
|
||
- We halen URL en key uit environment variables
|
||
- We geven deze aan `createClient`
|
||
- We exporteren de client zodat we het overal kunnen gebruiken
|
||
|
||
**Teacher Tip:** TypeScript geeft mogelijk een warning over "null assertion (!)" — dat is OK. Dit zeggen we tegen TypeScript: "Deze values bestaan echt, vertrouw me."
|
||
|
||
### 09:35–09:45 | Database Types (10 min)
|
||
|
||
Maak `lib/types.ts` handmatig:
|
||
|
||
```typescript
|
||
export interface Poll {
|
||
id: string
|
||
created_at: string
|
||
question: string
|
||
}
|
||
|
||
export interface Option {
|
||
id: string
|
||
poll_id: string
|
||
text: string
|
||
votes: number
|
||
}
|
||
```
|
||
|
||
**Waarom:** Dit helpt TypeScript begrijpen welke data we uit Supabase krijgen.
|
||
|
||
**Teacher Tip:** In een echt project zou je `npx supabase gen types typescript` gebruiken, maar dat kost extra setup. Voor deze les is handmatig OK.
|
||
|
||
### 09:45–10:00 | Async Data functies (15 min)
|
||
|
||
Update `lib/data.ts` — alle functies worden nu async en halen data uit Supabase:
|
||
|
||
```typescript
|
||
import { supabase } from './supabase'
|
||
import { Poll, Option } from './types'
|
||
|
||
export async function getPolls(): Promise<Poll[]> {
|
||
const { data, error } = await supabase
|
||
.from('polls')
|
||
.select('*')
|
||
.order('created_at', { ascending: false })
|
||
|
||
if (error) {
|
||
console.error('Error fetching polls:', error)
|
||
return []
|
||
}
|
||
|
||
return data || []
|
||
}
|
||
|
||
export async function getOptions(pollId: string): Promise<Option[]> {
|
||
const { data, error } = await supabase
|
||
.from('options')
|
||
.select('*')
|
||
.eq('poll_id', pollId)
|
||
.order('votes', { ascending: false })
|
||
|
||
if (error) {
|
||
console.error('Error fetching options:', error)
|
||
return []
|
||
}
|
||
|
||
return data || []
|
||
}
|
||
```
|
||
|
||
**Wat betekent dit:**
|
||
- `.from('polls')` — welke tabel
|
||
- `.select('*')` — alle kolommen
|
||
- `.eq('poll_id', pollId)` — filter op poll_id
|
||
- `.order()` — sorteer op
|
||
- `await` — wacht op het resultaat van de database call
|
||
- Error handling — log en return empty array
|
||
|
||
**Teacher Tip:** Veel students maken hier fouten met async/await:
|
||
```typescript
|
||
// ❌ FOUT: promise niet awaited!
|
||
const data = supabase.from('polls').select('*')
|
||
|
||
// ✅ GOED:
|
||
const data = await supabase.from('polls').select('*')
|
||
```
|
||
|
||
### 10:00–10:10 | Homepage als Server Component (10 min)
|
||
|
||
Update `app/page.tsx` — dit wordt een Server Component:
|
||
|
||
```typescript
|
||
import { getPolls } from '@/lib/data'
|
||
import PollItem from '@/components/PollItem'
|
||
|
||
export default async function Home() {
|
||
const polls = await getPolls()
|
||
|
||
return (
|
||
<div className="container mx-auto py-8">
|
||
<h1 className="text-3xl font-bold mb-8">QuickPoll</h1>
|
||
|
||
<div className="space-y-4">
|
||
{polls.map((poll) => (
|
||
<PollItem key={poll.id} poll={poll} />
|
||
))}
|
||
</div>
|
||
|
||
{polls.length === 0 && (
|
||
<p className="text-gray-500">Geen polls beschikbaar.</p>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
```
|
||
|
||
**Belangrijk:** Page.tsx is nu een **Server Component** — geen `'use client'` directive! We kunnen hier `async/await` rechtstreeks gebruiken.
|
||
|
||
**Teacher Tip:** Students vragen: "Maar hoe krijgen we de options?" — Goed punt! Die halen we in PollItem.
|
||
|
||
### 10:10–10:15 | PollItem Component (5 min)
|
||
|
||
Update `components/PollItem.tsx` — ook een Server Component:
|
||
|
||
```typescript
|
||
import { getOptions } from '@/lib/data'
|
||
import VoteForm from './VoteForm'
|
||
import { Poll } from '@/lib/types'
|
||
|
||
export default async function PollItem({ poll }: { poll: Poll }) {
|
||
const options = await getOptions(poll.id)
|
||
|
||
return (
|
||
<div className="border rounded-lg p-4">
|
||
<h2 className="text-lg font-semibold mb-3">{poll.question}</h2>
|
||
|
||
<div className="space-y-2">
|
||
{options.map((option) => (
|
||
<VoteForm
|
||
key={option.id}
|
||
option={option}
|
||
pollId={poll.id}
|
||
/>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
```
|
||
|
||
**Waarom twee Server Components?**
|
||
- `page.tsx` ziet alleen alle polls (geen details)
|
||
- `PollItem` wordt per poll gerenderd en haalt zelf de options op (parallel!)
|
||
- Dit patroon is efficient en schaalbaar
|
||
|
||
**Teacher Tip:** Dit is het "Suspended Components" patroon van React 18 — Server Components voeren dit automatisch in parallel uit.
|
||
|
||
---
|
||
|
||
## 10:15–10:30 | PAUZE (15 min)
|
||
|
||
Goed moment om even weg te lopen. Tussendoor kun jij:
|
||
- Rondlopen en kijken wie nog errors heeft
|
||
- Checken of iedereen env vars juist ingesteld heeft
|
||
- Dev servers herstarten voor wie vergeten zijn
|
||
- Voorbereiding treffen voor DEEL 2
|
||
|
||
---
|
||
|
||
## 10:30–11:00 | DEEL 2a: Uitleg Auth (30 min)
|
||
|
||
Dit is uitleg — geen live coding nog. Zorg dat alle laptops dicht zijn, focus op slides en beamer.
|
||
|
||
### Authenticatie vs Autorisatie
|
||
|
||
**Authenticatie (Authentication):**
|
||
- "Wie ben je?" — identity verification
|
||
- Voorbeeld: Je logt in met email + password
|
||
- Supabase Auth zorgt hiervoor
|
||
|
||
**Autorisatie (Authorization):**
|
||
- "Wat mag je?" — permissions
|
||
- Voorbeeld: Je mag alleen je eigen polls aanpassen
|
||
- RLS (Row Level Security) in Supabase zorgt hiervoor
|
||
|
||
**Voorbeeld:**
|
||
- Auth: "Je email en password kloppen, je bent Alice."
|
||
- RLS: "Alice mag haar eigen polls zien en updaten, maar niet die van Bob."
|
||
|
||
### Supabase Auth Features
|
||
|
||
Demo op beamer:
|
||
1. Open Supabase Dashboard → **Authentication** → **Providers**
|
||
2. Toon dat **Email/Password** is ingeschakeld
|
||
3. Toon de instelling **"Confirm email"** (nu UIT voor dev)
|
||
4. Ga naar **Users** tab — hier zie je ingelogde users
|
||
|
||
**Supabase Auth ondersteunt:**
|
||
- Email/Password (wat we vandaag gebruiken)
|
||
- OAuth (Google, GitHub, etc.) — volgende week
|
||
- Magic Links (passwordless login)
|
||
- Session management (Supabase beheert cookies automatisch)
|
||
|
||
### @supabase/ssr vs @supabase/supabase-js
|
||
|
||
**@supabase/supabase-js:**
|
||
- Browser-side client
|
||
- Voor onClick handlers, forms, interactie
|
||
|
||
**@supabase/ssr:**
|
||
- Server-side client (SSR = Server-Side Rendering)
|
||
- Voor middleware, cookies, server actions
|
||
- Handelt sessions automatisch af
|
||
|
||
**Waarom twee?**
|
||
- Browser kan niet veilig geheimen beheren
|
||
- Server kan veilig cookies zetten
|
||
- Supabase SSR packages zorgen dat beide veilig werken
|
||
|
||
### Supabase Auth Functies
|
||
|
||
**signUp(email, password)** — nieuwe account aanmaken
|
||
```typescript
|
||
const { data, error } = await supabase.auth.signUp({
|
||
email: 'user@example.com',
|
||
password: 'secure-password'
|
||
})
|
||
```
|
||
|
||
**signInWithPassword(email, password)** — inloggen
|
||
```typescript
|
||
const { data, error } = await supabase.auth.signInWithPassword({
|
||
email: 'user@example.com',
|
||
password: 'secure-password'
|
||
})
|
||
```
|
||
|
||
**signOut()** — uitloggen
|
||
```typescript
|
||
await supabase.auth.signOut()
|
||
```
|
||
|
||
**getUser()** — huidge user ophalen
|
||
```typescript
|
||
const { data: { user } } = await supabase.auth.getUser()
|
||
// user is null als niemand ingelogd, anders is het een User object
|
||
```
|
||
|
||
### Server vs Browser Client
|
||
|
||
**Browser Client (createBrowserClient):**
|
||
- Voor 'use client' components
|
||
- Kan useState gebruiken
|
||
- Kan useRouter gebruiken
|
||
- Kan user events luisteren
|
||
|
||
**Server Client (createServerClient):**
|
||
- Voor server components en middleware
|
||
- Leest/schrijft cookies
|
||
- Kan getUser() veilig aanroepen
|
||
- Geen access tot browser APIs
|
||
|
||
### Middleware & Session Refresh
|
||
|
||
**Wat doet middleware?**
|
||
- Draait op elke request naar je app
|
||
- Refreshed de Supabase session
|
||
- Zorgt dat user state altijd up-to-date is
|
||
|
||
**Voorbeeld flow:**
|
||
1. User logt in op `/login` page
|
||
2. Cookie wordt gezet
|
||
3. Middleware ziet op volgende request: "Er is een session cookie!"
|
||
4. Middleware refreshed de session
|
||
5. App ziet dat user ingelogd is
|
||
|
||
### Handige links
|
||
|
||
Toon op slides:
|
||
- [Supabase Auth docs](https://supabase.com/docs/guides/auth/server-side/nextjs)
|
||
- [Next.js Server Components docs](https://nextjs.org/docs/getting-started/react-essentials)
|
||
|
||
---
|
||
|
||
## 11:00–11:30 | DEEL 2b: Zelf Doen — Auth Implementeren (30 min)
|
||
|
||
Nu gaan studenten zelf auth bouwen in hun project. Dit is niet meer live coding — docent loopt rond en helpt.
|
||
|
||
**Instructie voor studenten:**
|
||
|
||
Volg deze stappen. Docent loopt rond als je vragen hebt.
|
||
|
||
#### Stap 1: SSR Package Installeren (2 min)
|
||
|
||
```bash
|
||
npm install @supabase/ssr
|
||
```
|
||
|
||
#### Stap 2: Server Client (3 min)
|
||
|
||
Maak `lib/supabase-server.ts`:
|
||
|
||
```typescript
|
||
import { cookies } from 'next/headers'
|
||
import { createServerClient } from '@supabase/ssr'
|
||
|
||
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 {
|
||
// Handle error
|
||
}
|
||
},
|
||
},
|
||
}
|
||
)
|
||
}
|
||
```
|
||
|
||
**Wat is dit?** Dit is een helper zodat Supabase cookies kan beheren in Next.js. Copy-paste voor nu.
|
||
|
||
#### Stap 3: Browser Client (1 min)
|
||
|
||
Maak `lib/supabase-browser.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!
|
||
)
|
||
}
|
||
```
|
||
|
||
**Wat is dit?** Dit gebruiken we in 'use client' components.
|
||
|
||
#### Stap 4: Middleware (5 min)
|
||
|
||
Maak `middleware.ts` in project root:
|
||
|
||
```typescript
|
||
import { type NextRequest, NextResponse } from 'next/server'
|
||
import { createServerClient } from '@supabase/ssr'
|
||
|
||
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, options }) => {
|
||
supabaseResponse.cookies.set(name, value, options)
|
||
})
|
||
},
|
||
},
|
||
}
|
||
)
|
||
|
||
// Refresh user session
|
||
await supabase.auth.getUser()
|
||
|
||
return supabaseResponse
|
||
}
|
||
|
||
export const config = {
|
||
matcher: [
|
||
'/((?!_next/static|_next/image|favicon.ico|.*\\.svg|.*\\.png|.*\\.jpg|.*\\.jpeg).*)',
|
||
],
|
||
}
|
||
```
|
||
|
||
**Wat is dit?** Dit draait op elke request en refreshed de session. Copy-paste, don't worry.
|
||
|
||
#### Stap 5: Signup Page (5 min)
|
||
|
||
Maak `app/auth/signup/page.tsx`:
|
||
|
||
```typescript
|
||
'use client'
|
||
|
||
import { useState } from 'react'
|
||
import { useRouter } from 'next/navigation'
|
||
import { createClient } from '@/lib/supabase-browser'
|
||
|
||
export default function SignUpPage() {
|
||
const router = useRouter()
|
||
const supabase = createClient()
|
||
const [email, setEmail] = useState('')
|
||
const [password, setPassword] = useState('')
|
||
const [error, setError] = useState('')
|
||
const [loading, setLoading] = useState(false)
|
||
|
||
const handleSignUp = async (e: React.FormEvent) => {
|
||
e.preventDefault()
|
||
setLoading(true)
|
||
setError('')
|
||
|
||
const { error } = await supabase.auth.signUp({
|
||
email,
|
||
password,
|
||
})
|
||
|
||
if (error) {
|
||
setError(error.message)
|
||
setLoading(false)
|
||
} else {
|
||
router.push('/auth/login')
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="flex items-center justify-center min-h-screen">
|
||
<form onSubmit={handleSignUp} className="w-full max-w-md p-6 border rounded-lg">
|
||
<h1 className="text-2xl font-bold mb-6">Sign Up</h1>
|
||
|
||
{error && <div className="text-red-600 mb-4">{error}</div>}
|
||
|
||
<input
|
||
type="email"
|
||
placeholder="Email"
|
||
value={email}
|
||
onChange={(e) => setEmail(e.target.value)}
|
||
className="w-full px-4 py-2 border rounded mb-4"
|
||
required
|
||
/>
|
||
|
||
<input
|
||
type="password"
|
||
placeholder="Password"
|
||
value={password}
|
||
onChange={(e) => setPassword(e.target.value)}
|
||
className="w-full px-4 py-2 border rounded mb-6"
|
||
required
|
||
/>
|
||
|
||
<button
|
||
type="submit"
|
||
disabled={loading}
|
||
className="w-full px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
|
||
>
|
||
{loading ? 'Signing up...' : 'Sign Up'}
|
||
</button>
|
||
</form>
|
||
</div>
|
||
)
|
||
}
|
||
```
|
||
|
||
**Belangrijk:** `'use client'` directive bovenaan — dit is een interactive component!
|
||
|
||
#### Stap 6: Login Page (5 min)
|
||
|
||
Maak `app/auth/login/page.tsx`:
|
||
|
||
```typescript
|
||
'use client'
|
||
|
||
import { useState } from 'react'
|
||
import { useRouter } from 'next/navigation'
|
||
import { createClient } from '@/lib/supabase-browser'
|
||
import Link from 'next/link'
|
||
|
||
export default function LoginPage() {
|
||
const router = useRouter()
|
||
const supabase = createClient()
|
||
const [email, setEmail] = useState('')
|
||
const [password, setPassword] = useState('')
|
||
const [error, setError] = useState('')
|
||
const [loading, setLoading] = useState(false)
|
||
|
||
const handleLogin = async (e: React.FormEvent) => {
|
||
e.preventDefault()
|
||
setLoading(true)
|
||
setError('')
|
||
|
||
const { error } = await supabase.auth.signInWithPassword({
|
||
email,
|
||
password,
|
||
})
|
||
|
||
if (error) {
|
||
setError(error.message)
|
||
setLoading(false)
|
||
} else {
|
||
router.push('/')
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="flex items-center justify-center min-h-screen">
|
||
<form onSubmit={handleLogin} className="w-full max-w-md p-6 border rounded-lg">
|
||
<h1 className="text-2xl font-bold mb-6">Login</h1>
|
||
|
||
{error && <div className="text-red-600 mb-4">{error}</div>}
|
||
|
||
<input
|
||
type="email"
|
||
placeholder="Email"
|
||
value={email}
|
||
onChange={(e) => setEmail(e.target.value)}
|
||
className="w-full px-4 py-2 border rounded mb-4"
|
||
required
|
||
/>
|
||
|
||
<input
|
||
type="password"
|
||
placeholder="Password"
|
||
value={password}
|
||
onChange={(e) => setPassword(e.target.value)}
|
||
className="w-full px-4 py-2 border rounded mb-6"
|
||
required
|
||
/>
|
||
|
||
<button
|
||
type="submit"
|
||
disabled={loading}
|
||
className="w-full px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
|
||
>
|
||
{loading ? 'Logging in...' : 'Login'}
|
||
</button>
|
||
|
||
<p className="mt-4 text-center text-sm">
|
||
Nog geen account? <Link href="/auth/signup" className="text-blue-600 hover:underline">Sign up</Link>
|
||
</p>
|
||
</form>
|
||
</div>
|
||
)
|
||
}
|
||
```
|
||
|
||
#### Stap 7: Logout Button (3 min)
|
||
|
||
Maak `components/LogoutButton.tsx`:
|
||
|
||
```typescript
|
||
'use client'
|
||
|
||
import { useRouter } from 'next/navigation'
|
||
import { createClient } from '@/lib/supabase-browser'
|
||
|
||
export default function LogoutButton() {
|
||
const router = useRouter()
|
||
const supabase = createClient()
|
||
|
||
const handleLogout = async () => {
|
||
await supabase.auth.signOut()
|
||
router.refresh()
|
||
}
|
||
|
||
return (
|
||
<button
|
||
onClick={handleLogout}
|
||
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
|
||
>
|
||
Logout
|
||
</button>
|
||
)
|
||
}
|
||
```
|
||
|
||
**Belangrijk:** `router.refresh()` na logout zorgt dat page de nieuwe state ziet!
|
||
|
||
#### Stap 8: Navbar met Auth State (3 min)
|
||
|
||
Update `components/Navbar.tsx`:
|
||
|
||
```typescript
|
||
import { createClient } from '@/lib/supabase-server'
|
||
import Link from 'next/link'
|
||
import LogoutButton from './LogoutButton'
|
||
|
||
export default async function Navbar() {
|
||
const supabase = await createClient()
|
||
const { data: { user } } = await supabase.auth.getUser()
|
||
|
||
return (
|
||
<nav className="bg-gray-800 text-white p-4">
|
||
<div className="container mx-auto flex justify-between items-center">
|
||
<Link href="/" className="text-2xl font-bold">
|
||
QuickPoll
|
||
</Link>
|
||
|
||
<div className="flex gap-4 items-center">
|
||
{user ? (
|
||
<>
|
||
<span className="text-sm">{user.email}</span>
|
||
<LogoutButton />
|
||
</>
|
||
) : (
|
||
<>
|
||
<Link href="/auth/login" className="px-4 py-2 bg-blue-600 rounded hover:bg-blue-700">
|
||
Login
|
||
</Link>
|
||
<Link href="/auth/signup" className="px-4 py-2 bg-green-600 rounded hover:bg-green-700">
|
||
Sign Up
|
||
</Link>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
)
|
||
}
|
||
```
|
||
|
||
**Logica:**
|
||
- Als `user` bestaat (ingelogd): toon email + Logout button
|
||
- Anders: toon Login + Sign Up buttons
|
||
|
||
#### Stap 9: Layout updaten (2 min)
|
||
|
||
Update `app/layout.tsx`:
|
||
|
||
```typescript
|
||
import type { Metadata } from 'next'
|
||
import Navbar from '@/components/Navbar'
|
||
import './globals.css'
|
||
|
||
export const metadata: Metadata = {
|
||
title: 'QuickPoll',
|
||
description: 'Vote on polls',
|
||
}
|
||
|
||
export default function RootLayout({
|
||
children,
|
||
}: {
|
||
children: React.ReactNode
|
||
}) {
|
||
return (
|
||
<html lang="en">
|
||
<body>
|
||
<Navbar />
|
||
{children}
|
||
</body>
|
||
</html>
|
||
)
|
||
}
|
||
```
|
||
|
||
Voeg gewoon `<Navbar />` toe.
|
||
|
||
**Teacher Tip: Studenten vastlopen?**
|
||
- Na 5-10 minuten vastzitten: toon de referentie code op beamer
|
||
- Zeg: "Dit is complex, copy-paste is OK. Focus op begrijpen, niet op typen."
|
||
- Help met debuggen (console.log, errors lezen)
|
||
|
||
---
|
||
|
||
## 11:30–11:45 | Vragen & Reflectie (15 min)
|
||
|
||
Hier zijn veelvoorkomende vragen:
|
||
|
||
### V: "Wat is het verschil tussen `createClient()` in server.ts en browser.ts?"
|
||
**A:**
|
||
- `server.ts`: kan cookies veilig beheren (server-side)
|
||
- `browser.ts`: kan UI events afhandelen (onClick, forms)
|
||
- Supabase kiest automatisch het juiste moment om te gebruiken
|
||
|
||
### V: "Waarom twee environment variables bovenaan?"
|
||
**A:**
|
||
- `NEXT_PUBLIC_SUPABASE_URL`: URL is public, iedereen ziet het
|
||
- `NEXT_PUBLIC_SUPABASE_ANON_KEY`: anon key is public (maar kan geen private data lezen)
|
||
- Private keys (service role) zetten we NIET in .env.local, die gaan in server.ts als geheim
|
||
|
||
### V: "Mijn login werkt niet, ik krijg error"
|
||
**A:** Check:
|
||
1. Klopt je email/password echt?
|
||
2. Is je account in Supabase Dashboard → Authentication → Users?
|
||
3. Is Email provider ingeschakeld?
|
||
4. Zit "Confirm email" uit? (check dashboard)
|
||
|
||
### V: "Logout werkt niet, user staat nog ingelogd"
|
||
**A:** Vergeten `router.refresh()` na `signOut()`?
|
||
|
||
### V: "Middleware error: 'createServerClient is not defined'"
|
||
**A:** Check je import: moet `import { createServerClient } from '@supabase/ssr'` zijn
|
||
|
||
### V: "Kan ik als anonieme user stemmen?"
|
||
**A:** Ja! RLS policy staat op `FOR SELECT, UPDATE TO authenticated` — maar je Navbar toont Login/Signup want je bent nog niet ingelogd. Dat is OK. Volgende les doen we RLS policies correct.
|
||
|
||
---
|
||
|
||
## 11:45–12:00 | Huiswerk & Afsluiting (15 min)
|
||
|
||
### Huiswerk (voor Les 9):
|
||
|
||
**Verplicht:**
|
||
1. **/create pagina bouwen** — studenten voegen nieuwe polls toe via een form
|
||
- Maak `app/create/page.tsx` (Server Component met form als Client Component)
|
||
- Form met: vraag + array van 2-3 opties
|
||
- `supabase.from('polls').insert()` en `supabase.from('options').insert()`
|
||
- Zorg dat je eigen `user_id` meestuurt
|
||
|
||
2. **RLS INSERT policy** — alleen authenticated users mogen polls toevoegen
|
||
- Supabase Dashboard → Authentication → Policies
|
||
- Voeg policy toe: `INSERT` voor authenticated users
|
||
- `user_id = auth.uid()`
|
||
|
||
3. **Optional extras (challenge):**
|
||
- Toon poll creator in PollItem
|
||
- Google OAuth inschakelen (zie Supabase docs)
|
||
- Edit/Delete buttons (alleen voor je eigen polls)
|
||
|
||
### Afsluitingsboodschap:
|
||
|
||
"Gefeliciteerd! Vandaag hebben jullie:
|
||
- Supabase gekoppeld aan Next.js
|
||
- Real data uit een database geladen
|
||
- Login/logout gebouwd
|
||
- Server & browser clients begrepen
|
||
|
||
Volgende week voegen we RLS policies toe zodat iedereen alleen zijn eigen polls kan aanpassen. Dat is waar authenticatie écht nuttig wordt!"
|
||
|
||
---
|
||
|
||
## Veelvoorkomende Problemen
|
||
|
||
| Probleem | Oorzaak | Oplossing |
|
||
|----------|---------|-----------|
|
||
| `Error: Cannot find module '@supabase/supabase-js'` | Package niet geïnstalleerd | `npm install @supabase/supabase-js` en dev server herstarten |
|
||
| Supabase returns leeg array | .env.local niet juist of dev server niet herstarten | Check .env.local, restart dev server (Ctrl+C + `npm run dev`) |
|
||
| TypeScript complains over `null assertion (!)` | Normale TS warning | Dit is OK, we vertellen TS dat env vars bestaan |
|
||
| `'use client' vergeten in signup/login page` | Component is interactief maar geen directive | Voeg `'use client'` bovenaan toe |
|
||
| Login page blank/geen content | Conflict met server components | Zorg ALL pages onder /auth zijn `'use client'` |
|
||
| Logout werkt niet, user nog ingelogd | `router.refresh()` niet aangeroepen | Voeg `await router.refresh()` toe na `signOut()` |
|
||
| Middleware error: "wrong params" | Onjuiste URL of key in middleware | Copy-paste van .env.local, check Format |
|
||
| "Invalid token" bij Supabase calls | Token verlopen of anon key fout | Restart dev server, check API credentials |
|
||
| User niet in Authentication → Users | Signup failed, geen account aangemaakt | Check browser console op errors, probeer opnieuw met ander email |
|
||
| `router.refresh()` werkt niet in component | Router niet geïmporteerd | `import { useRouter } from 'next/navigation'` (niet 'next/router'!) |
|
||
| Cors/network error | Supabase URL fout | Check dat URL eindigt op `.supabase.co` en https:// bevat |
|
||
| Password te kort / validation error | Supabase vereist min 6 chars | Zeg studenten: "Test met password123" |
|
||
|
||
---
|
||
|
||
## Didactische Tips
|
||
|
||
- **Pair Programming:** Zet snelle studenten samen met tragere — kennis spreidt zich uit
|
||
- **Show & Tell:** Toon je eigen werkend QuickPoll op beamer — studenten zien het doel
|
||
- **Error-driven Learning:** Zeg niet meteen het antwoord, vraag: "Wat zegt de error?"
|
||
- **Debug together:** Als iemand vastlopen, use browser console.log + devtools
|
||
- **Save time** — als >3 students dezelfde error hebben, stop even en toon op beamer
|
||
- **Celebrate wins** — als iemand eerste Signup working heeft, geef thumbs up!
|
||
|
||
---
|
||
|
||
## Referentiematerialen voor Studenten
|
||
|
||
- [Supabase Auth docs](https://supabase.com/docs/guides/auth/server-side/nextjs)
|
||
- [Next.js Server Components](https://nextjs.org/docs/getting-started/react-essentials)
|
||
- [Environment Variables in Next.js](https://nextjs.org/docs/basic-features/environment-variables)
|
||
- Alle code snippets uit deze docenttekst
|
||
|
||
---
|
||
|
||
**Einde docenttekst Les 8**
|