diff --git a/Les08-Compleet-v2.zip b/Les08-Compleet-v2.zip new file mode 100644 index 0000000..b07bc97 Binary files /dev/null and b/Les08-Compleet-v2.zip differ diff --git a/Les08-Compleet.zip b/Les08-Compleet.zip new file mode 100644 index 0000000..dda5cc6 Binary files /dev/null and b/Les08-Compleet.zip differ diff --git a/Les08-Supabase-Auth/Les08-Docenttekst.md b/Les08-Supabase-Auth/Les08-Docenttekst.md new file mode 100644 index 0000000..6b8a976 --- /dev/null +++ b/Les08-Supabase-Auth/Les08-Docenttekst.md @@ -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 { + 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 { + 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 ( +
+

QuickPoll

+ +
+ {polls.map((poll) => ( + + ))} +
+ + {polls.length === 0 && ( +

Geen polls beschikbaar.

+ )} +
+ ) +} +``` + +**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 ( +
+

{poll.question}

+ +
+ {options.map((option) => ( + + ))} +
+
+ ) +} +``` + +**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 ( +
+
+

Sign Up

+ + {error &&
{error}
} + + setEmail(e.target.value)} + className="w-full px-4 py-2 border rounded mb-4" + required + /> + + setPassword(e.target.value)} + className="w-full px-4 py-2 border rounded mb-6" + required + /> + + +
+
+ ) +} +``` + +**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 ( +
+
+

Login

+ + {error &&
{error}
} + + setEmail(e.target.value)} + className="w-full px-4 py-2 border rounded mb-4" + required + /> + + setPassword(e.target.value)} + className="w-full px-4 py-2 border rounded mb-6" + required + /> + + + +

+ Nog geen account? Sign up +

+
+
+ ) +} +``` + +#### 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 ( + + ) +} +``` + +**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 ( + + ) +} +``` + +**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 ( + + + + {children} + + + ) +} +``` + +Voeg gewoon `` 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** diff --git a/Les08-Supabase-Auth/Les08-Lesopdracht.pdf b/Les08-Supabase-Auth/Les08-Lesopdracht.pdf new file mode 100644 index 0000000..eb83a68 --- /dev/null +++ b/Les08-Supabase-Auth/Les08-Lesopdracht.pdf @@ -0,0 +1,219 @@ +%PDF-1.4 +% 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[+sbTVeA&XA#*?AiuGZ>&T0eo1p]8\6f:hWsLc-T::dU/"pNbYHlBu+4O?g>L\R@`3GFo\qW7DC[Tq*.R\d:?YJa2#all,Lknm6<@ZIGE&a@@OL7Jd:R9RUXiKuFRS5(DS]Y;A0P8T(Me=[GR4(Je];%?P9iSUJ6"qC"^AlB^SneRYr@`.QN2kPJV"0TI[G'YtMf][!dnqF3;CfMP2`YQ_@jDK*?6qVDc>fg1lu"GIoU>koF6QlZsZI(oCL3hmQ_AHi_\j4CA%endstream +endobj +18 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 882 +>> +stream +GauJ!>u03/'F*Lmrebc"2D)km=0nsD!.iWC[V!.=\8ftbC[/jSG09USEtYbh]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$1BDkVb2GX-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*RQf6/MfH)c8"?O8CftKh$>R%^9i,JZna$#Lf$Na%SV+F4`qe>@I2\g9St\[[rRI4jAjaYYscM>GPNt5D8'tdJ;_N`HXAR3]fPFp;`qPK@)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^U2H2hq85Bendstream +endobj +20 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 967 +>> +stream +Gb"/%gMWKG&:N^lVL?=aMcjouVlcsiBc-d(BIu)]3`YkL10Q<-s0s%GTgnaQ/.$$]$T@9cW6?R,n6(M/GWk^k0%?8rf[irZ\6FLGFfBr@Nc?B'+hX/1/kiCfP\PBtkZ[/5NajR`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-`,CWWE,qOAj$]]l*&L'Tu$-D`rCF&"Y@Ln[O,e"V#H@_/@,"h%4)q)LC\A.L!n?VPUj?,l9R'l8lP_c';0qRTePILQC;?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:9T@V?VMFsRkSJKN#fZHed-T]!CR]JaL)s+PpEs/=p$,OVdG3Xp.@sn%@7q04F3"^YYXpP$dnhn'1As+0hM3P-9G_eG\W+ot[,nr6_Cl9rH/mgg2_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:2meF(CZc$;6Zl0%LfqK@OBR*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^^XDXVE[RR9;?V]11iU&BBu)V]e\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%[+Yendstream +endobj +23 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1160 +>> +stream +GauI5Df=Ag&B7`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=%:j=fJc&")PPlJ:"8S+D]e%\%6YEkhJ%(\s2?E$rS_eX*4K%hBIGk2!9kf9W"0J/,3B1+e6cTMkD)*01I*!Yk0@L=-2&-Y!U64uU=*YdDOmY%$+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^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=>nRge+#GOf"Mf>;^qpc!o+MF7PWejqHbr7u[)s"3D56ZQZrk&H_^Q3:6id*lY'(GT,Kk5gh%0Jm1blUbZHnO?&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%p7#@S$Vc>/oOGa=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 diff --git a/Les08-Supabase-Auth/Les08-Live-Coding-Guide.md b/Les08-Supabase-Auth/Les08-Live-Coding-Guide.md new file mode 100644 index 0000000..8c5159c --- /dev/null +++ b/Les08-Supabase-Auth/Les08-Live-Coding-Guide.md @@ -0,0 +1,2472 @@ +# Les 8 — Live Coding Guide +## Supabase × Next.js + Auth + +> **Jouw spiekbriefje.** Dit bestand staat op je privéscherm. Op de beamer draait Cursor. +> Volg stap voor stap. Typ exact wat hier staat. Leg uit met de "Vertel:" blokken. + +--- + +## VOOR DE LES BEGINT + +**Zorg dat je dit hebt klaargemaakt:** +- [ ] Het poll-demo project geopend in Cursor +- [ ] feature/les-8 branch checked out (VOOR de nieuwe commits) +- [ ] De code hieronder klopt met wat je ziet (BEFORE-state) +- [ ] Supabase project aangemaakt en database klaar (polls + options tabellen) +- [ ] Twee demo polls in de database +- [ ] Slides geopend (voor Deel 2) + +--- + +# DEEL 1: LIVE CODING — Supabase Koppelen (09:10–10:15) + +**Doel:** De poll-app offline maken en live verbinden met Supabase. Alle data gaat via het Supabase API. + +--- + +## STAP 1: Installeer @supabase/supabase-js + +```bash +npm install @supabase/supabase-js +``` + +**Vertel:** +"We gaan de Supabase client library installeren. Dit is de JavaScript SDK waarmee we rechtstreeks kunnen communiceren met Supabase — zonder een eigen backend API te hoeven schrijven." + +--- + +## STAP 2: Maak .env.local aan + +Bestand: `.env.local` (in de root van je project) + +``` +NEXT_PUBLIC_SUPABASE_URL=https://xxxxx.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +``` + +**Vertel:** +"Deze environment variables bevatten je Supabase project URL en de anonieme API key. Ze beginnen beide met NEXT_PUBLIC_, dus ze worden in de browser zichtbaar — maar dat is oké want dit zijn public credentials. Je private key bewaar je nooit in de client code." + +**Waar vind je deze?** +- Ga naar je Supabase project dashboard +- Settings → API +- Kopieer Project URL en anon key + +--- + +## STAP 3: Maak lib/supabase.ts aan + +Bestand: `lib/supabase.ts` + +```typescript +import { createClient } from "@supabase/supabase-js"; + +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!; +const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!; + +export const supabase = createClient(supabaseUrl, supabaseAnonKey); +``` + +**Vertel:** +"Dit is onze Supabase client — één plek waar we de verbinding opzetten. We gebruiken `createClient()` om één client aan te maken die we overal importeren. De `!` betekent 'deze mag nooit undefined zijn'." + +--- + +## STAP 4: Update types/index.ts + +**BEFORE:** +```typescript +export interface Poll { + id: string; + question: string; + options: string[]; + votes: number[]; +} +``` + +**AFTER:** +```typescript +export interface Poll { + id: string; + question: string; + created_at: string; + options: Option[]; +} + +export interface Option { + id: string; + poll_id: string; + text: string; + votes: number; +} +``` + +**Vertel:** +"De database structuur is anders dan wat we hadden. Nu hebben we twee aparte tabellen: `polls` en `options`. Elke option is een eigen record met een `poll_id` referentie. Dit is genormaliseerde data — beter voor grote apps. De votes tellen we per option, niet meer als een array." + +--- + +## STAP 5: Rewrite lib/data.ts + +**BEFORE:** +```typescript +import { Poll } from "@/types"; + +let polls: Poll[] = [ + { id: "1", question: "Ik ben een vraag", options: ["optie 1", "optie 2", "optie 3", "optie 4"], votes: [1,1,1,1] }, + { id: "2", question: "Ik ben een vraag 2", options: ["optie 1", "optie 2", "optie 3", "optie 4"], votes: [1,1,1,1] } +] + +export function getPolls(): Poll[] { return polls } +export function getPollById(id: string): Poll | undefined { return polls.find((poll) => poll.id === id) } +export function votePoll(id: string, optionIndex: number) { + const poll = polls.find((p) => p.id === id) + if (!poll) return undefined; + if (optionIndex < 0 || optionIndex >= poll.options.length) return undefined; + poll.votes[optionIndex]++; + return poll; +} +``` + +**AFTER:** +```typescript +import { supabase } from "./supabase"; +import { Poll, Option } from "@/types"; + +export async function getPolls(): Promise { + const { data: polls, error } = await supabase + .from("polls") + .select("*, options(*)") + .order("created_at", { ascending: false }); + + if (error) { + console.error("Error fetching polls:", error); + return []; + } + + return polls || []; +} + +export async function getPollById(id: string): Promise { + const { data: poll, error } = await supabase + .from("polls") + .select("*, options(*)") + .eq("id", id) + .single(); + + if (error) { + console.error("Error fetching poll:", error); + return null; + } + + return poll; +} + +export async function votePoll(pollId: string, optionId: string): Promise