fix: add les 8
This commit is contained in:
BIN
Les08-Compleet-v2.zip
Normal file
BIN
Les08-Compleet-v2.zip
Normal file
Binary file not shown.
BIN
Les08-Compleet.zip
Normal file
BIN
Les08-Compleet.zip
Normal file
Binary file not shown.
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**
|
||||||
219
Les08-Supabase-Auth/Les08-Lesopdracht.pdf
Normal file
219
Les08-Supabase-Auth/Les08-Lesopdracht.pdf
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
%PDF-1.4
|
||||||
|
%<25><><EFBFBD><EFBFBD> ReportLab Generated PDF document (opensource)
|
||||||
|
1 0 obj
|
||||||
|
<<
|
||||||
|
/F1 2 0 R /F2 3 0 R /F3 6 0 R /F4 7 0 R
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
2 0 obj
|
||||||
|
<<
|
||||||
|
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
3 0 obj
|
||||||
|
<<
|
||||||
|
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
4 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 17 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
5 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 18 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
6 0 obj
|
||||||
|
<<
|
||||||
|
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
7 0 obj
|
||||||
|
<<
|
||||||
|
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
8 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 19 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
9 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 20 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
10 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 21 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
11 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 22 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
12 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 23 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
13 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 24 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
14 0 obj
|
||||||
|
<<
|
||||||
|
/PageMode /UseNone /Pages 16 0 R /Type /Catalog
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
15 0 obj
|
||||||
|
<<
|
||||||
|
/Author (\(anonymous\)) /CreationDate (D:20260331152247+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260331152247+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||||
|
/Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
16 0 obj
|
||||||
|
<<
|
||||||
|
/Count 8 /Kids [ 4 0 R 5 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R ] /Type /Pages
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
17 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 699
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gatm8b>R(K']&X:mLn5>=AMTGd@h(3)/+n_DR+6q2;%F,`rSB==*Epi),,f/E?%DgYKk$H1'JaQgAK5PJBoC/J#,CJKR\O[+s<DkQdC*KkdFDEU'rjQ7nKF(`F[ou`Ob,9rP:jop00+3jIe['))>bTVeA&XA#*?Aiu<U[i;ucWDsjddHa0sDq0c,90OlYaM^t/s>GZ>&T0eo1p]8\6f:hWsLc-T::dU/"pNbYHlBu+4O?g>L\R@`3GFo\qW7DC[Tq*.R\d:?YJa2#all,Lknm6<@ZIGE&a@@OL7Jd:R9RUXiKu<FWZJPt#Ca[j`&nn!3<q,LRN;c4\$T!ghrRsHe,[&#T\Cf=*]oW+LYP;M877*A]*uMVmiQ]1nN,YY?jd/j#.UNp%BiCLrl3LDE]T2/P*F#HMqcMLIh.:7A@bccI#.#Z<Tb1c?UDO+B872C(K\Qdi;<_p?lbKS'9q>FRS5(DS]Y;A0P8T(Me=[GR4(Je];%?P9iSUJ6"qC"^AlB^SneRYr@`.QN2kPJV"0TI[G'YtMf][!dnqF3;CfMP2`YQ<R>_@jDK*?6qVDc>fg1lu"GIoU>koF6QlZsZI(oCL3hmQ_AHi_\j4CA%<dM+k;50Krsm(=An]<h)2n&-I)3)acUrqbah#u=lfC]T.s350:;oS&d2E8))rPrgDPX=GtZ/E"_eVTf;$5KKIW,Q~>endstream
|
||||||
|
endobj
|
||||||
|
18 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 882
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
GauJ!>u03/'F*Lmrebc"2D)km=0nsD!.iWC[V!.=\8ftbC[/jSG09USEtYbh<XFC*2c=_EV!"^ujS+m(BFiP+QPI;U!Jb:j#_h7B80#QEbi,prgeGlMPpm0jSE9s[,R[/7k4V"9EBmHk`<c[p/<"TV5ot/%;!Wu::@^oCU9Z'nd(VpB5)8Ak]oh\Sb@]"BGQ>]7'Q:RgTC&neF;+m%NRG!%rX6g6f*-AJ"6'eUP-/WA+J`KM,d]dir,YnSR%>E-+?c`3Rf8$j>Z7*-U2APGBnWU))AH9U@l7NN%hma`kC6!jB22.[^kBGWrmS`g#"V"NTM*,+2B_cm1%bOZeqOK^$>TlmQMRE+JUUek\<;!bZ$/l][M5Ze3*Ng"4%Xh*U\u]\S"sLt-Z9^roYuNmG\HHL[`s[I-R=Or,*N%AE@`d^2Q1`.;>bV#@SLn.e!^ctF'2qhf%<.]L2$$#c%o/02Ft$I&/PO$-tUrMg30NL1^SS&B7oiG1jC@tbG6uh.F!@G.BLlM0#2W2YF1#"FA&k<'b0+e4hA3)-!Q#+P>Ztf=0'K?=lYoSOuWb"I3roo>-e692$1BDkV<OKVr(A,^kp#+qS7ZPqFRopg^%0"n&:U894k]8D:d>b2GX-b*.&)G3fXLgo]'*sC9:f#*5p?:C=.#\=J4BAHDgT('gWdo*]&b8K>FgUp\NG&B]_%mkVsQ0iiA?[H`"eG!k%G7>2bH-]l@GX*eTH4j$LLhPdZ:4XP=;4S-(YBQu$l7()D:'[uGH.7)WT6d;`!M9<3+ldbJ?&d;_RY7f/^:>@VK!^4f4g>;G;jbrktWUZ7RmB+GdD5'>>m/\bO7pUoVa*n>F`k^HKE8WV3OY2blE!ZEM4amnVf>aYEljM\hL~>endstream
|
||||||
|
endobj
|
||||||
|
19 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1216
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gau0CmrR2B&H0m]Z0`pm3J?MO-BjijEnjBm1sHcaC$0NS.%jBa`cbq0-.D,YIOj*%"1U3?#7LYbC#;+f[qdB/pn[b71']cLU&-nTC-m.Qe0[),"6bNBcRi>GFA:a!AjF1R4@0-/^iamiR',],%M6fBXM=rO=R#orf"sRa,tp;=&13c*KR+_*q_uf1TKO,E13UQU%ej\'nX(]>=9=ie6'AkX"b_(2^/T(N_WO*RQ<Ao0%K&P$Hea?d(Q^=dWj?k*Ud;BKE03njAC_kWp%0UHGjbde\(24],;WD3;-,"626rod),NZYg,-JPAWo+5M1dk"&PP;RPiS"H0c)tJ5(;*@IK85Ria.n$\UZ-d_#'<Ifl/.09^X*Y3t@ZLn+]<E)M#50's&K*`PM9CmZ/-'i85:V>f6/MfH)c8"?O8CftKh$>R%^9i,JZna$#Lf$Na%SV+F4`q<A+@f=^1;]DBlg,%fGK#kdL0jTK[JrCDf$$Us`PAL,]7Y0L_&7)WtPT'L2P*btj83Ng+_[`_[EPDsJFYs\D:b%F<e)jLdp7:6^cF-%hPimE9u:3sN1iQ78$l%I%;PuXK#"*$>e>@I2\g9St\[[rRI4jAjaYYscM>GPNt5D8'tdJ;_N`HXAR3]fPFp;`qPK@<UoApLT7I7tHO;]:Op6q8T':1*+tIuCYV\!Q.,rK!k&4\O5!F3bpiT<O&,+H?ZEoppE>)W'a?&o/6<@=*AO=6r8M?>Z?ien`YchK,r4a\:5$9H131+AS;FWMID+)?-iU9M%TO399tif\0RYS%<]Xec!Vs8$\bp^S#'AS?r)>^9]E'K'DHWpP,sM*G8g"?IpkTo5/h'W!t:i3C]:F,C1oFlTBpr"G_7RRV/U]n3uSr8UaO,`E0Pako?;\,hr!G]j8oC=Z]BjDN\H@L"57HLuD=tF2INR(Tf`.5MPP2[Ds/RoJLBu_*0An5MH7Y+*=AUpVdTBM-m_BTRYXTIf89%_5Wnf0at8"T:(8DSo>rFfnH3<`s`p>,+E%*%;]R$Zi]=OCc'R+GDmd^U2H2hq85B<WCh?!iiN:uZF?6\dKKH;jPlKa&N=Z2i2Qp-n+[Bf=Ma=WQtm]*Yu5l5e;Bd2Cp[O<[XX+14ijJ8g[gV@$U-4gY,FI%=^csIR)oFHVY]6A??q-Ps&3(pFfts-7$NK+Io2=Ocf9XZ8c0NNi0%Y$;6Z-5ju6Q3jM0(r[/L/g169l~>endstream
|
||||||
|
endobj
|
||||||
|
20 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 967
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gb"/%gMWKG&:N^lVL?=aMcjouVlcsiBc<sKPK%MaWX(-^&I,nLRms[_]t4Y3LAF5G2f5A5Q3T(dS26Kf/-;)bOSJK=!!bmTImSZB(l!PL`IQn`3#kP#$'dS(9sG\_6*P!iMQ5nVHVQV-\j8jX%:_sq.WR'C9hVW7#)_P*)"NK&ci@4#K?Y:TB@P*).42Qn[KgH/be*Lf%-%X@&^DOl+Rl>-d(BIu)]3`YkL10Q<-s0s%GTgnaQ/.$$]$T@9cW6?R,n6(M/GWk^k0%?8rf[irZ\6FLGFfBr@Nc?B'+hX/1/kiCfP\PBtkZ[/5Na<i`PK;D*ZRUKI?9?k$Q"a'q7>jR`<o5*-;@'/LSVZ6rrq(.loYZOqNb[PYelF]F&W_+\>2n>(e:SrTZlB^KfV3L1fG\_atce4ntJn-qtUd55+5HL@Z-4KH'hf\FK.%`.r;ppqk8jVVH%Vb&1hl-d6.$0mQh;If'&;\GcL&&%mcD0/n?KjIA!/q#E11ciZs48f7*mi8&\kKj*CR?*5sjc\3n$_AAlQ't:LOU`@G-V[]_F%oQ7?QH*kLZW#A<"IM#2I=aV>KC9k`/b`B$^"gY"&1(B[#ecW"SD.bb_`G:U"BQAhE]\+g^8W("(b:<`H5)./.7-`,CW<hsPsHXfP03dt9F.L/P/-l^94,%!8Ln))!+7jiA'A-#1L!k$e'M116$1#5DqOHm6\`!hi+_D;")mRs3Oh-\>WE,qOAj$]]l*&L'Tu$-D`rCF&"Y@Ln[O,e"V#H@_/@,"h%4)q)LC\A.L!n?VPUj?,l9R'l8lP_c';0qRTePILQ<e*Q22`%R*3ejNI3jTC=R%=)oZ$Q:o756jfN*a<bJ#e;/dt`B@8J0p`I<*XPBhIIp'Oi$6Du@E4nsm=hmg$0>C;?5[=+kW%kIYTSS*eL?X-[,($,%J%)bk?/!NfWl3A#ZV=V^D9#[NKPXW3D302IdN[UU~>endstream
|
||||||
|
endobj
|
||||||
|
21 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1211
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gau`SD3*C1&H9tY(nL(^[Lu0%pKY>\?17V:mBq9mf.Wsu9e_Zl32pDRmB-0P*BT?/]G+Dip&r7<4e?q2T(.%J@,m%+h]rGOecY4A!TH@_"N3)ZkZu!q?KZ*Z/0tD=$t:S2&-g&uE=h.jgbt"K'*=Yu(jAe(C!<70*)/CX!c9,Z#)r;>Q9`XoqMl6+X!21&*AaX]\7,N&+tj>L\3,E#([+"[JH!C2/l6Gm#-)7f;jN9DltI8mZG.J>)#omh.N)op)R2J7d'C"m3`B)2bIs0!")(_PU&OF5CnJ(D)9%a*`iqJ;HeC-%R!6b6Yf"P7bn4I1Tc9u7(\qZra1u[^&.G-ndY?LU&5<@Kl]AUf+,WUH*XN-30P9m]aO&NBN'rfG0A8kTG:Q;k(.O;7oB#OQ4fAYAC/DrX99sL1BYmeQ/"$L^/lFlM.H0Vs+qd)qF&3rjfD=uuNT_pW5c\fcJ^KE(1+g"2_7n;]K:?(dFXHGrP(HaJ8fa+:TQ?RE-:gbTj5\Vp->.oth`+C/8&LO:(oCArA@/And%$Z9!]'5)Am#=Lf4us&]t`qZE(ujNYU$O+FfmM;'jPne75fgHYS3f%QN,"jTXT#I'Bc:'Ehc?u3A$A&6VT7CQHhTU$&SBbZG%2Tp?c*02:LLc-A%6&Da%B?_>:QBs1J+U&\Fr*:!leACSqqUrU7g/Bm7shS-K8I5O4pn:0&PiX:]Z.h(!%pNZYuQ?5/m;is+dkgF_@\NPLBSM6LkDRZ.(6p`@9si>S/N8J9pgM>3RgkuJ1sj,MgH:%XDj92Y&aQbWZd1PmbG+#8uL0kA6_:#N$"";I\+=f(gn4rP^\$(b&.e:<G!4MlBu0ms[iHb"0LW"D!$=&I5mLI/9_Kt%dtY%t57H!mdU!0?:M>9T@V?VMFsRkSJKN#<R+@(bI!6CF?`TVDf^L9WDB8guH!\U=:gLU;Q%1$g\UV#i2OVRh^ff#Mn-km204&stTIpN,Xg:*X^%DWBJu#_+R#dgRkn39Bcd>fZHed-T]!CR]JaL)s+PpEs/=p$,OVdG3X<pPmGsI`_2cDFO%5:+4@$X_Oh,lE*9*[R+gfBlF3)NY>p.@sn%@7q04F3"^YYXpP$dnhn'1As+0hM3P-9G_eG\W+ot[,nr6_Cl9rH/mgg2_<MN0Mj6#uL%Q.JS22mbTW]D;<0UV!OP#X2fT@CMRc$L.A^%N/:%foJd.n.mjMH!~>endstream
|
||||||
|
endobj
|
||||||
|
22 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1369
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gau`RD0+E#&H9tYf_,tRD&L#2foo19j*!P)>USH)gL8)H`#;UN4*#P43$/+om[XKt(i6%V>@9FPp3*'Vp:4J^d%\!d^69PpW!E2agL*+i]*@#r4Qr?P8[%Zp@@*:2@g.u.g!KHIQ5p,@mA&F%'0'jT#KmQ\DWH<_\bZJ=e0(CW8sip[+WPGp#lT/Uc$+s]3![g$:#eMj&NV^*L,>N7Qm5NXOR5=%0Bg*s?p<"c'-Pn0"_>++Nt(F@3ZtU+6oj!KBAW^#9N?flp]:B4ll+aN>4M!gq5*+i1^HElBi#nEZXCue%)?fci.3&/(eTuoV49ig=b9RP#B&J.rW*p]1LI$gL8fb:%]PTu@2-`jjp:Ja49WXnL>rOWkh@1lBem6W=_>K3g,hQR'=*=#X;qbg/=iGqn23q[MenhB-S#B&8#]HSlN6:6ZAajOgDeFlhYn6u11Q9G8;0&qaJu$n89(R.NHLQG0HDaYCUAHZQM+^(?mM7<;NqPK$@!$nV:=*Fb5mV\"KMa[G%A!#2qY,>hldGPY:2<VPSZ_,*>meF(CZc$;6Zl0%LfqK@OBR<h/5T*A)X]G7<*rh:SpL4,YP*Pl[,,j)_k$nON!:g)A@Zi,bG-DOCgQ_73#5dqkZA6k4t6?.?rVAcG_4<5#Wm,43$r:0Lp`@jgUpTR>*FE2HDetc/o`\cc5sG%YkqI@t@iNN_sM)o:0>CP;,n*/B-:IMi*a)9&t8#phjc!I`pIuMjCh/ppViMX5YR)MhJh)[,EB8*f,4n"]6Y4/nV4TlVSa.!>S)8jhprkl'I.d_n9DH6)u#PYeC``#(*'CGbK&0ZjirQ$&#'4-LNg9E#0^^XD<u1dsR;-_c1EI(UPdG$iI+qnQ^9Pn3TQ`MP<3sJMlB>XVE[RR9;?V]11iU&BBu)V]e\<q[3W&MF281q<23_90[YBXj`/.\U-p8&<KLH%sY=r^bF^_9U&.pM?nP#i'P7#,Q_J07:"&*EY$cRk.p^<g#cda29S(:<L9]?Plp-A%u+H@rYXZ=c+>P[N#V\(gr/cL-#c"W8MTfELaA![W;U=X#OX&Y7;4\61b[p!i,ob^-Jq-Pf,_6:XIUB_o-I5X"P^^t^KJn!;p//0K_ha'm)D(Nq7UupEHuYLH@48V]]^>X\gLRGT?b1*r=@n0c@8!lH@PM+9*AHKj+'NQ6Ij2`U3Np#Z7:f5h*6Si(C:#i,0O(7]H6O9W\:C(=Hp=t%[+Y<?#suu-Y$V=Lh(Lc]/58K)(g69NsM:8_Ygjps5X+arTWoT'pqP\2`KjGh_?tnHCUk1N]Xb#h-B@s<6YsbHGf^QF23eU/+fA!6e:tKV83NK=go6R)kJTri-"u533PJVb@fIr@hou4""T`7k5~>endstream
|
||||||
|
endobj
|
||||||
|
23 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1160
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
GauI5Df=Ag&B<W);dB&hkrQtOos_hh`BdB9:"-JX[h\KGcn]HRO[YP2ljQ=tqbEBP33aL9W*:MZL%'>7`8>V].CAW#RV+_fkk;j,TKjO]:Op$pL&NBgpKO7nF;=%%`/gDo?NQ;8aH(9[G*=TegRSTk[i\qaL^2*=M>VN.](!4pdoH[fK]TBVH6gF;3-uSiL[OcbHd%G3BVR?RTK`\8.:q0K*Jd=YKXO_`h.YmaXCRn1WSoYOOu(@p;CiQLB5,4=%:<Ke`>j=fJc<DlrN;$JNL8_6;R6/NWomI45l\CjZhU*53@V79-R)ckm]ArCQss_<:Z-:Mgk%$7eja[caD@?mTljIlVj^kJQn(UM[NJZ7J+[`_#MmSTB/dN?okF0-GCD@CE;Ak>&")PPlJ:"8S+D]e%\%6YEkhJ%(\<N&BZ^"g*LjM06M"73EdN>s2?E$rS_eX*4K%hBIGk2!9kf9W"0J/,3B1+e6cTMkD)*01I*!Yk0@L=-2&-Y!U64uU=*YdDOmY%$+<R@iWWb0sbm8+AkTO-33pUl'/;0,#$8(C6-Rt^-:rod2o7G.:iPdRoVD.2Ep,A=rg5lI:&:@Z2AR,kPaHUsFOt$<`22@SSYC$Y+'12]7EmoW("4ef<#RttkoK$F:IjBPN"n8nG1Pf2&N`^Z:;AAs=P17*Fo3b2bCH9+?lun"*bB"UWc0SW$D1lanb#h\QI:p9)P)?(:aRA++bGP22N;80h]o$mA2]Y5[h'RU7mA/_;IbfK!)Lb,MQV=_0LR_)&N:#6]3oRc4D_.iX+I(8aUS\[>h$/G('j!mu/F_T>b.[\j+IEEpYLr>)92a!,e5FBX$a^etF]1p*)+7*XUmu=aRmR>3*WXC&1Rj,tAf%REfPUbNKIa!?>mfY70WgB$XQfLd\C+:(&#Jq[q_QX+lW*jaOR"k,nUnX18^</R9u)n4Kag&t;7_J4\G?.5=)9U#Ekc)(o@=%m%tkB-AZolsOsM_?c^r9YIGTu(arGH300WPC$dZ*Q<^VE=TJY,O.]:Z\TR//T<EfR$8Tq."9If5`Ei);dN%4&s/?;DM%6@9%TcGrj7l!E<1'7?VPZjI?7(-GP%G`lfhfe_\\AO[`2R9b(_qY'UY?9U^EI#k19X_`0$;qe!"[4XnSgEW'ndPrQqB%/<9'Q~>endstream
|
||||||
|
endobj
|
||||||
|
24 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 661
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gat%^a_oie'LhcqMS!sK<&*J`nhN\_(,-*F(:#@$U#d)[Ys^&]D"R0h;B?J$78XA[W&L5n*uTD(AFWu*^dF-o-O,k\IfUK6"6;7#k+/sk4%>7YA<`o._g".9=>n<LT)Y@WUNAU8,3e"ZqH8\$l\>Rge+#<K]/Frr>GOf"Mf>;^qpc!o+MF7PWejqHbr7u[)s"3D56ZQZrk&H_^Q3:6id*lY'(GT,Kk5gh%0Jm1blUbZH<q?`j/G!$4t(UTO[K<[.[lmq>nO?&bGS7oUC8,%f+en-ZWiSn'L$[LB1Nt4613Y7X0OS73@TB%c=Ot=,a4Dmh.P.]N$UL[[jT)mpofj9KE'K7AE,E,ZN\'k?TcX*!%Nk5&/#@Po+a3;]i+]q[YPjlA!$rZ4_[Si<@b9$T%p<di^N+3(&m9\^l*eZ9;F05Re$u+;$e[$c:as0[2%2/F6)tDNMj6?9M#?0=&bJ2>7#@S$Vc>/oOGa=<TM!;J&+,%hX#Y.6Cb9QF)JMEm=8HkB%!.JV8X6SBd\'Xd=T\6XQa2$;2X:$FE,QBZ`%Wer//)MQAStT(WLL]9G#pB7<*Kog#\gYmj'&8<2tpr(FQN$BpF&-ARH+UFq'!-^ah9CS-b0t:>8h7:Z]&U1O?)OmO^P(^ZUUN([G:p(7j@Z]0cJ~>endstream
|
||||||
|
endobj
|
||||||
|
xref
|
||||||
|
0 25
|
||||||
|
0000000000 65535 f
|
||||||
|
0000000061 00000 n
|
||||||
|
0000000122 00000 n
|
||||||
|
0000000229 00000 n
|
||||||
|
0000000341 00000 n
|
||||||
|
0000000546 00000 n
|
||||||
|
0000000751 00000 n
|
||||||
|
0000000856 00000 n
|
||||||
|
0000000971 00000 n
|
||||||
|
0000001176 00000 n
|
||||||
|
0000001381 00000 n
|
||||||
|
0000001587 00000 n
|
||||||
|
0000001793 00000 n
|
||||||
|
0000001999 00000 n
|
||||||
|
0000002205 00000 n
|
||||||
|
0000002275 00000 n
|
||||||
|
0000002556 00000 n
|
||||||
|
0000002662 00000 n
|
||||||
|
0000003452 00000 n
|
||||||
|
0000004425 00000 n
|
||||||
|
0000005733 00000 n
|
||||||
|
0000006791 00000 n
|
||||||
|
0000008094 00000 n
|
||||||
|
0000009555 00000 n
|
||||||
|
0000010807 00000 n
|
||||||
|
trailer
|
||||||
|
<<
|
||||||
|
/ID
|
||||||
|
[<71e7c7d830850d86ed44e0355ffd582a><71e7c7d830850d86ed44e0355ffd582a>]
|
||||||
|
% ReportLab generated PDF document -- digest (opensource)
|
||||||
|
|
||||||
|
/Info 15 0 R
|
||||||
|
/Root 14 0 R
|
||||||
|
/Size 25
|
||||||
|
>>
|
||||||
|
startxref
|
||||||
|
11559
|
||||||
|
%%EOF
|
||||||
2472
Les08-Supabase-Auth/Les08-Live-Coding-Guide.md
Normal file
2472
Les08-Supabase-Auth/Les08-Live-Coding-Guide.md
Normal file
File diff suppressed because it is too large
Load Diff
174
Les08-Supabase-Auth/Les08-Slide-Overzicht.md
Normal file
174
Les08-Supabase-Auth/Les08-Slide-Overzicht.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
# Les 8 — Slide Overzicht
|
||||||
|
## Supabase Auth: Inloggen & Registreren
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Slide 1: Titelslide
|
||||||
|
**Layout:** Split (cream links, blauw rechts) — Keynote stijl
|
||||||
|
- NOVI Hogeschool logo
|
||||||
|
- "AI leerlijn"
|
||||||
|
- **Next.js**
|
||||||
|
- **Les 8**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Slide 2: Terugblik vorige les
|
||||||
|
**Layout:** Cream + blauw blob rechts
|
||||||
|
- **Titel:** Terugblik vorige les
|
||||||
|
- Links: Wat we gebouwd hebben
|
||||||
|
- Stemmen werkend gemaakt
|
||||||
|
- Supabase account + project aangemaakt
|
||||||
|
- Polls + options tabellen
|
||||||
|
- Foreign keys & CASCADE
|
||||||
|
- RLS policies (SELECT/UPDATE voor anon)
|
||||||
|
- Testdata via Table Editor
|
||||||
|
- Rechts: Wat nog mist
|
||||||
|
- Supabase niet gekoppeld aan Next.js
|
||||||
|
- Geen login/registratie
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Slide 3: Planning
|
||||||
|
**Layout:** Gele achtergrond + decoratieve blobs
|
||||||
|
- **Titel:** Planning
|
||||||
|
- Deel 1: Live Coding Supabase koppelen — 65 min
|
||||||
|
- Client setup, environment variables
|
||||||
|
- Data layer herschrijven
|
||||||
|
- Components aanpassen
|
||||||
|
- Data persisten testen
|
||||||
|
- Pauze — 15 min
|
||||||
|
- Deel 2: Uitleg + Zelf Doen — 60 min
|
||||||
|
- 30 min: Uitleg Auth functies
|
||||||
|
- 30 min: Zelf /create pagina bouwen
|
||||||
|
- Afsluiting — 30 min
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Slide 4: Van Array naar Database
|
||||||
|
**Layout:** Cream + blauw blob rechts
|
||||||
|
- **Titel:** Van Array naar Database
|
||||||
|
- Links: code block met de oude in-memory code
|
||||||
|
```
|
||||||
|
let polls: Poll[] = [
|
||||||
|
{ id: "1", question: "...", ... }
|
||||||
|
];
|
||||||
|
```
|
||||||
|
- Rechts: Supabase query
|
||||||
|
```
|
||||||
|
const { data } = await supabase
|
||||||
|
.from("polls")
|
||||||
|
.select("*, options(*)");
|
||||||
|
```
|
||||||
|
- Pijl van links naar rechts: "Zelfde functies, andere data source"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Slide 5: Live Coding Deel 1
|
||||||
|
**Layout:** Blauw volledig + cream blob links
|
||||||
|
- **Titel:** Live Coding
|
||||||
|
- **Subtitel:** Deel 1: Supabase × Next.js
|
||||||
|
- Stappen:
|
||||||
|
- @supabase/supabase-js installeren
|
||||||
|
- .env.local configureren
|
||||||
|
- Supabase client maken
|
||||||
|
- TypeScript types definiëren
|
||||||
|
- data.ts herschrijven
|
||||||
|
- Components aanpassen
|
||||||
|
- Testen: data moet bewaard blijven
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Slide 6: Het Patroon
|
||||||
|
**Layout:** Cream + blauw blob rechts
|
||||||
|
- **Titel:** Het Patroon
|
||||||
|
- Server Component:
|
||||||
|
```
|
||||||
|
async function PollList() {
|
||||||
|
const { data } = await supabase.from("polls").select("*");
|
||||||
|
return <div>{/* renderen */}</div>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Client Component:
|
||||||
|
```
|
||||||
|
export default function VoteButton() {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
// interactie, fetch naar API
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Uitleg: "Dit patroon verandert NIET — alleen de data source"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Slide 7: Pauze
|
||||||
|
**Layout:** Cream + grote blauwe cirkel
|
||||||
|
- **Titel:** Pauze
|
||||||
|
- **Subtitel:** 15 minuten
|
||||||
|
- "Supabase is gekoppeld! Na de pauze: Authentication"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Slide 8: Wat is Auth?
|
||||||
|
**Layout:** Cream + blauw blob rechts
|
||||||
|
- **Titel:** Wat is Auth?
|
||||||
|
- Authenticatie = wie ben je? (login, registratie)
|
||||||
|
- Autorisatie = wat mag je? (RLS policies, protected routes)
|
||||||
|
- Supabase Auth biedt:
|
||||||
|
- Email/password
|
||||||
|
- OAuth (Google, GitHub)
|
||||||
|
- Magic links
|
||||||
|
- Session management
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Slide 9: Auth Functies
|
||||||
|
**Layout:** Cream + blauw blob rechts
|
||||||
|
- **Titel:** Auth Functies
|
||||||
|
- Code examples:
|
||||||
|
```
|
||||||
|
// Sign Up
|
||||||
|
await supabase.auth.signUp({ email, password });
|
||||||
|
|
||||||
|
// Sign In
|
||||||
|
await supabase.auth.signInWithPassword({ email, password });
|
||||||
|
|
||||||
|
// Sign Out
|
||||||
|
await supabase.auth.signOut();
|
||||||
|
|
||||||
|
// Get User
|
||||||
|
const { data } = await supabase.auth.getUser();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Slide 10: Zelf Doen
|
||||||
|
**Layout:** Blauw volledig + cream blob links
|
||||||
|
- **Titel:** Zelf Doen
|
||||||
|
- **Subtitel:** Bouw Auth in je project
|
||||||
|
- Stappen:
|
||||||
|
- @supabase/ssr package installeren
|
||||||
|
- Auth helpers configureren
|
||||||
|
- Sign-up & login pagina's
|
||||||
|
- Middleware voor sessies
|
||||||
|
- /create pagina bouwen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Slide 11: Huiswerk
|
||||||
|
**Layout:** Cream + blauw blob rechts
|
||||||
|
- **Titel:** Huiswerk
|
||||||
|
- Opdracht: /create pagina bouwen
|
||||||
|
1. Alleen ingelogde users kunnen polls maken
|
||||||
|
2. Poll wordt gekoppeld aan user.id
|
||||||
|
3. Test: zet je eigen polls online
|
||||||
|
- Extra:
|
||||||
|
- Google OAuth integreren
|
||||||
|
- Profiel pagina maken
|
||||||
|
- Dark mode
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Slide 12: Afsluiting
|
||||||
|
**Layout:** Blauw volledig + cream/roze/zwart blobs links
|
||||||
|
- **Titel:** Tot volgende week!
|
||||||
|
- Volgende les: Deployment + meer features
|
||||||
|
- "Je hebt nu een echte app met login, database en auth!"
|
||||||
BIN
Les08-Supabase-Auth/Les08-Slides.pptx
Normal file
BIN
Les08-Supabase-Auth/Les08-Slides.pptx
Normal file
Binary file not shown.
Reference in New Issue
Block a user