fix: add les 8
This commit is contained in:
893
Les08-Supabase-Auth/Les08-Docenttekst.md
Normal file
893
Les08-Supabase-Auth/Les08-Docenttekst.md
Normal file
@@ -0,0 +1,893 @@
|
||||
# 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**
|
||||
Reference in New Issue
Block a user