From b9ffee586f23270c2a5b6e8349f430b6776e2f12 Mon Sep 17 00:00:00 2001 From: Tim Rijkse Date: Tue, 31 Mar 2026 16:18:33 +0200 Subject: [PATCH] fix: add les 9 --- Les09-Docenttekst.md | 552 ++++++++++++++++++++++++++++++++ Les09-Lesopdracht.pdf | 263 ++++++++++++++++ Les09-Live-Coding-Guide.md | 622 +++++++++++++++++++++++++++++++++++++ Les09-Slide-Overzicht.md | 270 ++++++++++++++++ Les09-Slides.pptx | Bin 0 -> 157481 bytes 5 files changed, 1707 insertions(+) create mode 100644 Les09-Docenttekst.md create mode 100644 Les09-Lesopdracht.pdf create mode 100644 Les09-Live-Coding-Guide.md create mode 100644 Les09-Slide-Overzicht.md create mode 100644 Les09-Slides.pptx diff --git a/Les09-Docenttekst.md b/Les09-Docenttekst.md new file mode 100644 index 0000000..c457b60 --- /dev/null +++ b/Les09-Docenttekst.md @@ -0,0 +1,552 @@ +# Les 9 — Supabase Auth +## Docenttekst + +**Les:** 9 van 18 +**Onderwerp:** Supabase Authentication (signUp, signIn, signOut, middleware, RLS) +**Duur:** 120 minuten +**Vorige les:** Les 8 — Students hebben Supabase gekoppeld, /create pagina werkend, Server Component patroon, polls database + +--- + +## Leerdoelen +- Authenticatie vs autorisatie begrijpen +- Supabase Auth functies gebruiken: signUp, signInWithPassword, signOut, getUser +- Server client (SSR) vs Browser client onderscheiden +- Middleware voor session refresh implementeren +- Authenticated Navbar bouwen met getUser +- Row Level Security (RLS) voor authenticated users toepassen + +--- + +## Lesopbouw & Timing + +### 09:00–09:10 | Welkom + Terugblik (10 min) +**Slides:** 1, 2, 3 + +Ik start de les. Korte recap van Les 8: +- Supabase project aangemaakt +- NEXT_PUBLIC_SUPABASE_URL en ANON_KEY in .env +- /create pagina with VoteForm component +- Polls tabel in database met votes +- "Na vandaag kunnen jullie je app beveiligen met authenticatie" + +**Planning tonen (slide 3):** +- 09:10–10:00: Uitleg Auth concepten + Demo +- 10:00–10:15: Samen Middleware + Auth Callback bouwen +- 10:15–10:30: Pauze +- 10:30–11:30: Zelf Doen (signup, login, logout, Navbar) +- 11:30–11:45: Vragen +- 11:45–12:00: Huiswerk + Afsluiting + +--- + +### 09:10–10:00 | Deel 1a: Uitleg Auth Concepten (50 min) +**Slides:** 4, 5, 6 + +#### 09:10 | Slide 4: Wat is Auth? + +**Vertel:** +"Authenticatie is: wie ben jij? Login, password, je identiteit bewijzen. +Autorisatie is: wat mag je doen? Wie mag polls maken? Dit regelen we later met RLS. +Supabase Auth beheert alles: signUp, login, sessies, JWT tokens." + +**Demo:** Open https://supabase.com/dashboard +- Klik project → Authentication → Providers → Email +- Laat zien: Disable Email Confirmations is AAN (sneller testen) +- Zeg: "Students zien zelf deze checkbox na Le 9" + +#### 09:20 | Slide 5: Auth Functies + +**Vertel:** +"Vier kern functies in Supabase Auth: +1. signUp({ email, password }) — Nieuw account +2. signInWithPassword({ email, password }) — Inloggen +3. signOut() — Uitloggen +4. getUser() — Wie is ingelogd? + +Hieronder toon ik hoe we deze gebruiken in Next.js." + +**Code tonen (slide 5):** +```typescript +// signUp +const { error } = await supabase.auth.signUp({ email, password }); + +// signIn +const { error } = await supabase.auth.signInWithPassword({ email, password }); + +// signOut +await supabase.auth.signOut(); + +// getUser (server of browser) +const { data: { user } } = await supabase.auth.getUser(); +``` + +#### 09:30 | Slide 6: Server vs Browser Client + +**Vertel:** +"Supabase Auth werkt in twee omgevingen: +- **Server Client** (Node.js, SSR): via cookies, secure +- **Browser Client** (React, CSR): via localstorage, minder secure + +We gebruiken @supabase/ssr package. Dit handelt beide af." + +**Toon twee code blokken naast elkaar (slide 6):** + +**Server Client** (middleware, Navbar): +```typescript +import { createServerClient } from "@supabase/ssr"; +import { cookies } from "next/headers"; + +export async function createSupabaseServerClient() { + 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 { } + }, + }, + } + ); +} +``` + +**Browser Client** (signup, login, logout): +```typescript +import { createBrowserClient } from "@supabase/ssr"; + +export function createSupabaseBrowserClient() { + return createBrowserClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! + ); +} +``` + +**Zeg:** +"Cookies zijn beveiligd. localStorage in browser kan hack worden. Daarom: server client voor getUser in Navbar, browser client voor login forms." + +**📌 Slide 6 referentie voor Middleware:** + +Middleware zorgt dat de session word gerefresht op elke request: +```typescript +// middleware.ts +export async function middleware(request: NextRequest) { + let supabaseResponse = NextResponse.next({ request }); + const supabase = createServerClient(...); + await supabase.auth.getUser(); + return supabaseResponse; +} +``` + +"Dit zorgt dat je Session JWT token altijd up-to-date is." + +--- + +### 10:00–10:15 | Deel 1b: Samen Coderen (15 min) + +#### Stap 1: npm install +```bash +npm install @supabase/ssr +``` + +#### Stap 2: lib/supabase-server.ts aanmaken +Voeg dit in (hieronder exact): +```typescript +import { createServerClient } from "@supabase/ssr"; +import { cookies } from "next/headers"; + +export async function createSupabaseServerClient() { + 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 { } + }, + }, + } + ); +} +``` + +#### Stap 3: lib/supabase-browser.ts aanmaken +```typescript +import { createBrowserClient } from "@supabase/ssr"; + +export function createSupabaseBrowserClient() { + return createBrowserClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! + ); +} +``` + +#### Stap 4: middleware.ts (root project) +```typescript +import { createServerClient } from "@supabase/ssr"; +import { NextResponse, type NextRequest } from "next/server"; + +export async function middleware(request: NextRequest) { + let supabaseResponse = NextResponse.next({ request }); + const supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { return request.cookies.getAll(); }, + setAll(cookiesToSet) { + cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value)); + supabaseResponse = NextResponse.next({ request }); + cookiesToSet.forEach(({ name, value, options }) => + supabaseResponse.cookies.set(name, value, options) + ); + }, + }, + } + ); + await supabase.auth.getUser(); + return supabaseResponse; +} + +export const config = { + matcher: ["/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)"], +}; +``` + +#### Stap 5: auth/callback route +`app/auth/callback/route.ts`: +```typescript +import { NextResponse } from "next/server"; +import { createSupabaseServerClient } from "@/lib/supabase-server"; + +export async function GET(request: Request) { + const { searchParams, origin } = new URL(request.url); + const code = searchParams.get("code"); + if (code) { + const supabase = await createSupabaseServerClient(); + await supabase.auth.exchangeCodeForSession(code); + } + return NextResponse.redirect(origin); +} +``` + +**Zeg:** "Dit is standaard Supabase/Next.js boilerplate. Niet allemaal letterlijk begrijpen. Focus op: server vs browser client." + +--- + +### 10:15–10:30 | Pauze +**Slide 7** + +--- + +### 10:30–11:30 | Deel 2: Zelf Doen (60 min) +**Slide 8** + +Students bouwen nu zelf: +1. app/signup/page.tsx +2. app/login/page.tsx +3. components/LogoutButton.tsx +4. components/Navbar.tsx (met getUser) +5. Uitloggen in layout.tsx + +**Reference code:** + +#### app/signup/page.tsx +```typescript +'use client' +import { createSupabaseBrowserClient } from "@/lib/supabase-browser"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import Link from "next/link"; + +export default function SignUp() { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [message, setMessage] = useState(""); + const [loading, setLoading] = useState(false); + const router = useRouter(); + const supabase = createSupabaseBrowserClient(); + + const handleSignUp = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setMessage(""); + const { error } = await supabase.auth.signUp({ email, password }); + if (error) { setMessage(error.message); } + else { setMessage("Account aangemaakt!"); router.push("/login"); } + setLoading(false); + }; + + return ( +
+

Registreren

+
+
+ + setEmail(e.target.value)} + className="w-full p-2 border rounded" required /> +
+
+ + setPassword(e.target.value)} + className="w-full p-2 border rounded" minLength={6} required /> +
+ +
+ {message &&

{message}

} +

+ Al een account? Inloggen +

+
+ ); +} +``` + +#### app/login/page.tsx +```typescript +'use client' +import { createSupabaseBrowserClient } from "@/lib/supabase-browser"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import Link from "next/link"; + +export default function Login() { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [message, setMessage] = useState(""); + const [loading, setLoading] = useState(false); + const router = useRouter(); + const supabase = createSupabaseBrowserClient(); + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setMessage(""); + const { error } = await supabase.auth.signInWithPassword({ email, password }); + if (error) { setMessage(error.message); } + else { router.push("/"); router.refresh(); } + setLoading(false); + }; + + return ( +
+

Inloggen

+
+
+ + setEmail(e.target.value)} + className="w-full p-2 border rounded" required /> +
+
+ + setPassword(e.target.value)} + className="w-full p-2 border rounded" required /> +
+ +
+ {message &&

{message}

} +

+ Nog geen account? Registreren +

+
+ ); +} +``` + +#### components/LogoutButton.tsx +```typescript +'use client' +import { createSupabaseBrowserClient } from "@/lib/supabase-browser"; +import { useRouter } from "next/navigation"; + +export function LogoutButton() { + const router = useRouter(); + const supabase = createSupabaseBrowserClient(); + const handleLogout = async () => { + await supabase.auth.signOut(); + router.push("/"); + router.refresh(); + }; + return ( + + ); +} +``` + +#### components/Navbar.tsx +```typescript +import Link from "next/link"; +import { createSupabaseServerClient } from "@/lib/supabase-server"; +import { LogoutButton } from "./LogoutButton"; + +export async function Navbar() { + const supabase = await createSupabaseServerClient(); + const { data: { user } } = await supabase.auth.getUser(); + return ( + + ); +} +``` + +#### app/layout.tsx (updated) +```typescript +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; +import { Navbar } from "@/components/Navbar"; + +const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"] }); +const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"] }); + +export const metadata: Metadata = { title: "QuickPoll", description: "Stem op je favoriete opties" }; + +export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { + return ( + + + + {children} + + + ); +} +``` + +**Instructies voor students:** +1. Maak app/signup/page.tsx — form met email/password inputs +2. Maak app/login/page.tsx — inlog form +3. Maak components/LogoutButton.tsx — knop die signOut() aanroept +4. Maak components/Navbar.tsx — toon email als ingelogd, login/signup links anders +5. Update layout.tsx — voeg `` toe + +**Ik loop rond en help. Studenten kunnen stuck raken op:** + +#### Veelvoorkomende problemen + +| Probleem | Oorzaak | Oplossing | +|----------|---------|----------| +| "Module not found: @supabase/ssr" | npm install niet gedaan | `npm install @supabase/ssr` | +| Navbar toont altijd "Inloggen" | getUser() returns null | Check cookies middleware, browser dev tools | +| Login werkt niet | Verkeerde credentials | Check Supabase dashboard → Auth Users | +| "Invalid PKCE flow" | Browser client misconfigured | Zorg dat .env keys correct zijn | +| Logout werkt niet | signOut() niet wacht | `await supabase.auth.signOut()` | +| Layout.tsx error: Navbar is async | Navbar is Server Component | `async` is ok, use await in getUser() | + +--- + +#### 11:00 | Check-in: Navbar + +Ik check of iedereen Navbar werkend heeft. Zeg: +"Navbar is een **Server Component** (async). Daarom kunnen we direct getUser() callen zonder hooks. Dit is uniek voor Next.js." + +Toon: `const { data: { user } } = await supabase.auth.getUser();` + +#### 11:15 | RLS Update + +**Vertel:** +"Nu authenticatie werkt, beveiligen we polls. Wie mag die maken? +- Anoniem (niet ingelogd): mag zien en stemmen +- Authenticated (ingelogd): mag polls maken EN zien EN stemmen" + +**Stap 1:** Open Supabase dashboard → SQL Editor + +**Stap 2:** Voer uit: +```sql +ALTER TABLE polls ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "polls_select_all" ON polls + FOR SELECT USING (true); + +CREATE POLICY "polls_insert_authenticated" ON polls + FOR INSERT WITH CHECK (auth.uid() IS NOT NULL); + +CREATE POLICY "polls_update_owner" ON polls + FOR UPDATE USING (auth.uid() = created_by); +``` + +(Zeg: "Auth.uid() is de ID van ingelogde user. NULL als anoniem.") + +**Stap 3:** Test in /create: +- Niet ingelogd: Knop grijs / gedeactiveerd +- Ingelogd: Knop blauw, poll aanmaken werkt +- Na uitloggen: Weer grijs + +--- + +### 11:30–11:45 | Vragen & Debugging + +Ik loop rond. Studenten kunnen vragen: +- "Hoe debug ik auth?" + - Supabase dashboard → Auth Users + - Browser dev tools → Application → Cookies (zoek sb-*) +- "Hoe reset ik mijn account?" + - Dashboard → Auth Users → delete user → registreer opnieuw + +--- + +### 11:45–12:00 | Huiswerk + Afsluiting (15 min) +**Slides:** 9, 10 + +**Slide 9: Huiswerk** + +1. **Google OAuth (optioneel, moeilijk)** + - Supabase dashboard → Auth → Providers → Google + - Copy Client ID, Secret + - Voeg signInWithOAuth button toe + +2. **Profiel pagina (les 10)** + - app/profile/page.tsx + - Toon user.email, user.id + - Update password / email form (kan les 10 zijn) + +3. **Maker tonen bij poll (les 10)** + - Voeg `created_by` toe aan polls tabel + - Toon bij elke poll wie het maakte + - Autorisatie: alleen maker mag aanpassen + +**Slide 10: Afsluiting** + +"Volgende les: Deployment! We zetten je app live op Vercel. Daarna: Google OAuth, profiel, meer RLS." + +--- + +## Extra: Supabase Auth Docs +https://supabase.com/docs/guides/auth/server-side/nextjs diff --git a/Les09-Lesopdracht.pdf b/Les09-Lesopdracht.pdf new file mode 100644 index 0000000..f0f2318 --- /dev/null +++ b/Les09-Lesopdracht.pdf @@ -0,0 +1,263 @@ +%PDF-1.4 +%“Œ‹ž ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 4 0 R /F4 6 0 R /F5 14 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 +<< +/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +5 0 obj +<< +/Contents 20 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 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 /F4 /Subtype /Type1 /Type /Font +>> +endobj +7 0 obj +<< +/Contents 21 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +8 0 obj +<< +/Contents 22 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +9 0 obj +<< +/Contents 23 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +10 0 obj +<< +/Contents 24 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +11 0 obj +<< +/Contents 25 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +12 0 obj +<< +/Contents 26 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +13 0 obj +<< +/Contents 27 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +14 0 obj +<< +/BaseFont /Symbol /Name /F5 /Subtype /Type1 /Type /Font +>> +endobj +15 0 obj +<< +/Contents 28 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +16 0 obj +<< +/Contents 29 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +17 0 obj +<< +/PageMode /UseNone /Pages 19 0 R /Type /Catalog +>> +endobj +18 0 obj +<< +/Author (\(anonymous\)) /CreationDate (D:20260331161655+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260331161655+02'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False +>> +endobj +19 0 obj +<< +/Count 10 /Kids [ 5 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R 15 0 R 16 0 R ] /Type /Pages +>> +endobj +20 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 367 +>> +stream +Gatn`h+kg@(^Api?H%8IB%6fuo1TfT-D3Zi"Vb!G7OJ!uDr5Hj&d[EPD:7mGDu2\33EVZ]>N[Npf&GtdK+$%,AsEO\++'M,`]<[8]68FL--H=*r_=R\_[+I.!I$ZfmDGnMendstream +endobj +21 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1444 +>> +stream +GauHKD/\E'&H88.0g2KN[)@4f%TSc@3HBE+HGDG^C-V3_<1p+[^7'mG!*eI5A1q;A)"L(*ugMYq2Pi;*LM#n!i'>)(Wa]!^6U>jo3U,j`gh$/mH(mO2U$EeP)8&k4jC>^T%GZ985fg[m+a3S^p*!g[G1E\1O,)-glY(Q$Te5A,hBHC$q)2,E`Nu)4FBVJQpa:X5R)cl`Aa:am2kn/#LPfN8,bc+JjZtM^C0rIC=(bD*9A'S$UI_5$#(3d9$_Nuff^d9a^@Q7q]hKWXEn6q_WOV1B$0HN/0W:FJ7AR4E3('qeP,WU2QB&XIijlk8XH,,T@Y+%q^7XR^F-A>Op`s"diJ26c92qTt\QeR,$$6C/@nLa'@*fNI@5H,N%!j2;*JPuDQ[h$n.e#k]W0a2_Cjd%k/iODQ=ICcBkE@B7Ik5'R&'$NDC''8Q<(L+/QF3'Pj'.4']Y>*WP&pbRT>">J'%MNj0qWr[qm:."+.5Y[c\G2D^"MeU-og(SQ5bp:!\7Q+cW^,7N[C&T':Yl+6"%?fAUUYG?\#jb^5](gD>_Yu\qdtU[q8`73\*AhE9c=2S`L*28R-b[JSPu)Pp[k)ELMTXJeKS6WL'M;a2+I&fXQ6EGdbbe8nXLE20M/Zd:^lhVuO"Ur%(F*Nc0?+Y[hIRjN?T2'endstream +endobj +22 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 581 +>> +stream +GasalIr!dm&H1LYi`[0YUJkVpp/8oMP2a;[aKes"%1GPmPh)=@V398DP^N4rm3HNeBB^:,+ME-rlcFc#IU9^b"i-3epO`^B%t"C!,7O>Q,4=5Ih?HOhTe'^tKHVWBUD3Zs(DAA:8f,?Jq-tVUE-aBH?TZ?=FnK^I+n,PC0^.+3r$m4):[D9=6(THAdXcbld]RZuWD^Jd)-`p`H+8>+l\c8BT>4eTd[1KFEA_"?dT2T&*@eDlmg2BnO!tC$*f9Lno5X5m'SX2M4-?^c;bNUZd_Hd=^3t3gomsZQ'+7@d2Su82OpcX.?d'Ed@FH03nG4R$88h^mpGj;8hVFi`^9e4rn48UHZ:[[T!'f*:DB7_lrbE8LTm]&qJR$XGSnP8&CSCR&\OOfNddh52263R@$R9$'[JFa9-J%PFfU>4ddp3U4Q@$C(VY7)n.W)H\WKtED@?\Z1T#oF?=;G'TE;kendstream +endobj +23 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1164 +>> +stream +GatU2qetI`&H4hB`>n.F:?`Z]![LXsP)*)8VRc>ldFAnkFULSDJ'-LnjD4VY2://-HHdM].+--Nt9K@_ITV'@U>gX[=T^H:tm*B(SSiLa-:``a=;'Vj%KZi.Ju:'@+O\$'8'iJdLD2S;\Om"lr@#Kl&q\d-fGNUIOuEs@R&&?A'^tm>Zb&DN3nj&9)V5d#c[sLP2`E$\r&6G'oOMh7q`AmbbX3j+)rFLfCH[Vo22tpSg4EYQa*sDo>f%IccDY8@`mM\*J^5MH9:53.KZ:2>:$;Zo9G<":cI_t$h-9Rs5;A;=O^Q3P?PA`DXhqH]M/Z6(.glO'$AlHtOJmD`>aV=L.od,gY;jchjC`%3VZlZ$)8HtpLt>n`I,L_cO!kW9F2#pC=?>;q%*EH;]]dJ.T\!ObW.4uD6K;I`H!gt*jXV+Cp1,u_;lV5GD_\?-kQnCJS$`0R0j@-r4GB*/NV.>SC?1%i"!eGc%uD=rts``A[0<0EY,L9k2%%qOIargcpQF;'@N?:AS#KnPqt1%bXWA!Y6B4?3!?)(`6;F2Ogk+UW"8J8Sj-@?Z\QJ5IHRt6XO[*TnB(76d[TP`F0@ri[aj1q1sA^A,Ohg6K-lGP4Kc6j0\/3Y[^V5-6SE\sH%tNpkMQatQdt&t^GC*)S>`&E>2VVYla6KP.ji>U"1pBNp5P%__rfUq.!MT3!MR:Qj1a\H8E'[pp'fTg2c(d]62fGTj0@l&AL(7d::$Wh)V[m`"B=h0ZeD"]1PrDPg[DE'P1s'5m)>ODaCA:bq`=2^@6b1YG1bXMDse&hs>8hS!.(QeA!d/~>endstream +endobj +24 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1414 +>> +stream +Gb!SjD/Z1=&H88.0h(Au;\^uE[Zd5PlfI6(8njP;Xt$jILBY\b"r;)Z@],OlHY4-^S:GZ@l0_1\al[/@Sp=pkFr]_ojlg&9!B?R#rQtYD#Lb[mY3!Om$8/4]T+7BnADafkma)ukkigY;\G\VUd:]L=m#Bd$=T,"4s0r068%.rSi]f4V*`$;I"=uND36Q6j`i-#AWGbT9>^tr,#,lJac7UlcF5R+1f7=>EZQ=T6*aQuu5B?coSFO_Xa`?5YbVam$.EW.e8!:^LBkl"i[dl]Hm(7uF#Qpf;"ME>u*BOOo"LUoJYGmug,#1g$i@$S1,#&]p:A;cNg6g5JLB'sFpAlL"rcrRT[4c/'q)H-2uJV(pZmPk#9<;(dRfqdY!3lnP@U-)CIIn<-=;f"3fLfl`KkbX_9@FSDH.@b^o_9>o&lGh9)7Vu@_#+S)=e+i,9N&f$+1SEff'[^Ft)Ce$T<,fkb"bj'4)i1@mk:=f8U6XcQ><#nZ`<);U&WYu_fYTsl*8k\Ko%\&1AHo86tR6C&<\uB19Jai^BK1b*$&IAl]IXsKU7HV;fYCA6U+dXX/rGlqZ9jI9o^">hpe`smib=TXUOG@j2=V!GuKlaGZOq,>ai`pC4"+hq6?Au^2]ff>$`o"`&Kr.@kS&9-Q9[7Yjd6Xp%g;X2PoH(tC,rd*bb3MVkRkU!9k+MUg/l\m'#OfJg_geH.Z7H)Q^tendstream +endobj +25 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1156 +>> +stream +Gb!#Z;/b2I&:XAW3!`XF%$P3^E]0`%$`$FH!a1X3#5U%BJWe:k=,Bm*.X#G!^[F]rYnP5npFou3CeN9++7Hp**YlJH5(WZl$\58]p`BPC`irQ2&:,KNDW#gqpc8]SQKOrc*aXL90V1tN8U,\Z\A8]b$R::f22-9$m(gCZ-Jk8Ll"g&BR06W2W=^]=/;CW,7>^V*fOp*fH2qlUBhKaGPMn`Zi,FIp22qBb#'ruBbX'3H19*$t,j^L5TT#%kC*s.F(MFtc.=>6?H";U3;F7gp)TW[cj1QBSOj"/O5p7Y)ZPpQQ:%e>:>C2c]u5?)*^&9\.O>hrN5'/J%k;h*esTV0<.#CP\^&o>08G3pC8WWff_$i3=!i+.DG'Dd7sI"lc"Bg,[ko$I*ZW_6HVGjg",`;5(.X+LaFKdtBqNS5[4q=Fi8@mB5_:LOsBu[9FnQCg>C]6Kqf9o+'J0AAL!OVVUU!2`[t8cu(+MieS$dt,2]$&-5sLZ'CQ$>d#F\SirVrs><,52u>n^^ZpO`PSY?7$!1>D6CNRp>]H5i\u7%q1.UJ[58FiH;m+SOYZ-6H&i[Y@#1@Ctg$Ui"0FUH9@,ZL?"Nh1Kq0>iFR&ofUQO95rT#i1+tendstream +endobj +26 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1392 +>> +stream +Gatm:D/\E'&H88.0sXba;Oq\&&kG!TdJIb4%BkMQIZS`%p$k;(BpM"Cg[HJ:7tN+\Np#;<#RkQIe)&B_WJbbl>1'+.bO$VD,H0+/C)8Zr8HP#V?gXC2PO_.R0Sm,hI3b)MB$D^riVmWLWFj`;pJ]6BU4N]#P6U7X*,b'BFR[ZA8,5W&H)-*8-9BHp!uF"Vd4k!f/nE;mNL*=7*\jq/2DUTX(YSe8W'\NqTZA+#(Pf,=HhB2aijgW7u_nTubJY;'/uS$lFA,&Y)bL6ArC\RN[Hd+-D^UiMHf'n$SDE/:>@8;JJg8pn#6XLH$7>YM-n,29Ka#\d,FlI:'a-Y8l:`'m"H*C4+h0ZiP\*_WK83cmisHp)GVdi2aTRaXFf0K?@u:"fOf[FS+dV(Z?Ua,-'_$Go'J7<5>HDhsDA:s+!]Vj*9Ni&4DJr7%R3ki5lp,7&u:\WZLqNgU[P6#SdNo(.i#=jDjiGT1>FZ,>UE-&>'0%<5hH`/JlKkU$rpA0='<]L'X1a+tDbJOu[/IRF;pk^Jp`NX@XC/ala-m%sVlK@V3SFP`a9ad#OEh;E8h]&IYRe)?rdYXf:MdC+HD*O#2hSn<8")?K.fgej5RQ7iKTfB-YG*:nTr'+4&&rendstream +endobj +27 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 317 +>> +stream +Garo=;+ne\'SYH9.h4pTo:^$P=<)3B#l:?,@NP9JgG\@IBo6UE8EkU_X`Gn[2fKpqF7+6>3$NkId'F(hQO"Znp_7`V/cl(LKEoWU)*^)j&]>k*$'Nsm\/')NF6mpYe[Dn$3N)E_=^ocd[fu@:J"k`-."\N_6f;QA9RFPl^8u>semLY45e]NjIB.e@GHTB@A;\@IkgA4C@5+O#reXendstream +endobj +28 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 757 +>> +stream +Gatn#h/8]I&;BTO'M"98ftANj+1LNhj),c7A37#u'a_u+Y_d'JU^P[ET>%p1*'(!f\nXN5L=r!dc#O"/6ZMDV2r+IH'W:+TF9D.>\trQ&!=8(@lfjH\9hR8oa)^*a,L*Yj;rie>AiPN\g!EE2BT_=B&E1>Qnhau:=F!j8P[V2,OOcX&:dgJi%aQ0\SP.=bcB5p-W^lL1?;+qclE0Pfu0.at@e[B#d(#?k,UWhQ%pIt,[;U-Q^lM=pb;`Wg`HT""2(g?@rNA*2\(*#Q4qU:+dh+'^j@4=Z3(fZFtCYo$CUUFh&8cR%3`k(KQB,T`gQ+.KkfGX-J/oK]uB9-Q:$b#*n$c9D%,X>\mZD+qT(g>n%)L$%$Z5qC@\Pl=qR>um)]`.sXBV[IdgTtcZ*-;[T4IsTCiBco[JQ%$d~>endstream +endobj +29 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 735 +>> +stream +GatUq?#SFN'Sc)P($Dae<12D7E8YW!g9+>_Yr1i"`\\QR#VJlB`XuKb4hAp\4lUd=(B=sN`:mNgp9S2E!ldAFL`8DCJ,pOto`IY2ZX_E(R4,HU$u"9aKXJVkLJ"!M(5iQVSfi>\.t4bj2-'&W/)\sXY:"RBS'W16DR%u?jpfld(:fUWGmWO&HSe!Yn\q^7,9,tl3)+X)OL9\Ke4q70)F6u'<9T`<"h@07-=G%7#Ee3J]`,+=DuV%2c#X*u8rpoH[*G="=:e/[<1!!]N7T0#!_%t902ZnCE#c_^rq:XA+kn=`7`bp`'FQIbe/;jPO\_'SV>edT"Q#9(NuA\a9RlU7<@)-WBVeW8JW7*Fb2$*i(#&:QI%q0<0*LUg0bp1jm;He,FJp#!-5+,8O';n)Fbc73>5[*DSa`ftPSeN-P5TAN`3]\V*Kendstream +endobj +xref +0 30 +0000000000 65535 f +0000000061 00000 n +0000000133 00000 n +0000000240 00000 n +0000000352 00000 n +0000000467 00000 n +0000000672 00000 n +0000000777 00000 n +0000000982 00000 n +0000001187 00000 n +0000001392 00000 n +0000001598 00000 n +0000001804 00000 n +0000002010 00000 n +0000002216 00000 n +0000002294 00000 n +0000002500 00000 n +0000002706 00000 n +0000002776 00000 n +0000003057 00000 n +0000003178 00000 n +0000003636 00000 n +0000005172 00000 n +0000005844 00000 n +0000007100 00000 n +0000008606 00000 n +0000009854 00000 n +0000011338 00000 n +0000011746 00000 n +0000012594 00000 n +trailer +<< +/ID +[] +% ReportLab generated PDF document -- digest (opensource) + +/Info 18 0 R +/Root 17 0 R +/Size 30 +>> +startxref +13420 +%%EOF diff --git a/Les09-Live-Coding-Guide.md b/Les09-Live-Coding-Guide.md new file mode 100644 index 0000000..b0e5183 --- /dev/null +++ b/Les09-Live-Coding-Guide.md @@ -0,0 +1,622 @@ +# Les 9 — Supabase Auth +## Live Coding Guide voor Docent + +This is your cheat sheet for the full lesson. Follow the timing in Les09-Docenttekst.md. + +--- + +## DEEL 1A: UITLEG AUTH (09:10–10:00) + +### 09:10 | SLIDE 4: Wat is Auth? + +**Demo:** Open https://supabase.com/dashboard + +Vertel: +"Authenticatie is: wie ben jij? Login en password, je identiteit bewijzen. +Autorisatie is: wat mag je doen? Wie mag polls maken? Dit regelen we met RLS policies. +Supabase Auth beheert alles: signUp, login, sessies, JWT tokens." + +**Stap 1:** Dashboard openen, project selecteren +**Stap 2:** Klik Authentication → Providers → Email +**Stap 3:** Toon: "Disable Email Confirmations" is AAN + +Vertel: +"Deze checkbox is cruciaal voor testen. Normaal zouden users een confirmation email krijgen voordat ze inloggen. Dat slaan we over voor deze les. In productie zet je dit uit." + +--- + +### 09:20 | SLIDE 5: Auth Functies + +**Vertel:** +"Supabase Auth heeft vier kern functies:" + +**Code tonen (copy-paste in terminal of code editor):** + +```typescript +// 1. signUp — Nieuw account +const { error } = await supabase.auth.signUp({ + email: "user@example.com", + password: "secure123" +}); +if (error) console.error(error.message); + +// 2. signInWithPassword — Inloggen +const { error } = await supabase.auth.signInWithPassword({ + email: "user@example.com", + password: "secure123" +}); +if (error) console.error(error.message); + +// 3. signOut — Uitloggen +await supabase.auth.signOut(); + +// 4. getUser — Wie is ingelogd? +const { data: { user } } = await supabase.auth.getUser(); +console.log(user.email); // "user@example.com" +console.log(user.id); // "abc-123-def" +``` + +Vertel: +"Diese vier functies zijn alles wat je nodig hebt. Error handling: altijd checken of `error` null is." + +--- + +### 09:30 | SLIDE 6: Server vs Browser Client + +**Vertel:** +"Supabase Auth werkt op twee plaatsen: +1. **Server (Node.js)** — Secure, via cookies +2. **Browser (React)** — Less secure, via localStorage + +We gebruiken @supabase/ssr. Dit switcht automatisch." + +**Show left code block (Server Client):** + +```typescript +// lib/supabase-server.ts +import { createServerClient } from "@supabase/ssr"; +import { cookies } from "next/headers"; + +export async function createSupabaseServerClient() { + 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 { } + }, + }, + } + ); +} +``` + +Vertel: +"Server client gebruikt cookies. Cookies kunnen beveiligd worden (httpOnly, secure-only). Dit is safer." + +**Show right code block (Browser Client):** + +```typescript +// lib/supabase-browser.ts +import { createBrowserClient } from "@supabase/ssr"; + +export function createSupabaseBrowserClient() { + return createBrowserClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! + ); +} +``` + +Vertel: +"Browser client werkt in React. localStorage is minder secure (scripts kunnen het uitlezen), maar nodig voor login forms." + +**Key difference:** +"Server component (Navbar) → Server client (getUser) +Client component (LoginForm) → Browser client (signUp, signIn, signOut)" + +--- + +## DEEL 1B: SAMEN CODEREN (10:00–10:15) + +Students volgen mee terwijl je dit live codeert (of ze kopieren uit les09-live-coding-guide.md). + +### Stap 1: npm install + +```bash +npm install @supabase/ssr +``` + +Wacht tot dit klaar is. + +### Stap 2: lib/supabase-server.ts + +Maak dit bestand aan: + +```typescript +import { createServerClient } from "@supabase/ssr"; +import { cookies } from "next/headers"; + +export async function createSupabaseServerClient() { + 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 { } + }, + }, + } + ); +} +``` + +### Stap 3: lib/supabase-browser.ts + +```typescript +import { createBrowserClient } from "@supabase/ssr"; + +export function createSupabaseBrowserClient() { + return createBrowserClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! + ); +} +``` + +### Stap 4: middleware.ts (ROOT of project, naast app/) + +```typescript +import { createServerClient } from "@supabase/ssr"; +import { NextResponse, type NextRequest } from "next/server"; + +export async function middleware(request: NextRequest) { + let supabaseResponse = NextResponse.next({ request }); + const supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { return request.cookies.getAll(); }, + setAll(cookiesToSet) { + cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value)); + supabaseResponse = NextResponse.next({ request }); + cookiesToSet.forEach(({ name, value, options }) => + supabaseResponse.cookies.set(name, value, options) + ); + }, + }, + } + ); + await supabase.auth.getUser(); + return supabaseResponse; +} + +export const config = { + matcher: ["/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)"], +}; +``` + +Vertel: +"Middleware runt op elke request. `await supabase.auth.getUser()` refresht de session token. Dit zorgt dat je niet uitgelogd wordt als je token expired." + +### Stap 5: app/auth/callback/route.ts + +```typescript +import { NextResponse } from "next/server"; +import { createSupabaseServerClient } from "@/lib/supabase-server"; + +export async function GET(request: Request) { + const { searchParams, origin } = new URL(request.url); + const code = searchParams.get("code"); + if (code) { + const supabase = await createSupabaseServerClient(); + await supabase.auth.exchangeCodeForSession(code); + } + return NextResponse.redirect(origin); +} +``` + +Vertel: +"Dit is voor OAuth (Google, GitHub). Supabase stuur je hier naartoe na login. De `code` wordt ge-exchanged voor een session. Voor nu: boilerplate, niet essentieel." + +### Test middleware + +Vertel: +"Test of middleware werkt: open http://localhost:3000. Je zou geen errors moeten zien. In browser dev tools → Application → Cookies → zoek naar `sb-*`. Die cookies beteken dat middleware goed werkt." + +--- + +## PAUZE (10:15–10:30) + +--- + +## DEEL 2: ZELF DOEN (10:30–11:30) + +Students bouwen nu zelf. Jij loopt rond, helpt, en toont code op beamer als studenten stuck zijn. + +### Zelf Doen Checklist (wat moet elke student doen): + +- [ ] app/signup/page.tsx +- [ ] app/login/page.tsx +- [ ] components/LogoutButton.tsx +- [ ] components/Navbar.tsx +- [ ] app/layout.tsx updated +- [ ] Test signup → login → poll maken → logout + +--- + +### 1. app/signup/page.tsx + +Dit is een 'use client' component met form. Students schrijven dit zelf, maar hier is de reference: + +```typescript +'use client' +import { createSupabaseBrowserClient } from "@/lib/supabase-browser"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import Link from "next/link"; + +export default function SignUp() { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [message, setMessage] = useState(""); + const [loading, setLoading] = useState(false); + const router = useRouter(); + const supabase = createSupabaseBrowserClient(); + + const handleSignUp = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setMessage(""); + const { error } = await supabase.auth.signUp({ email, password }); + if (error) { setMessage(error.message); } + else { setMessage("Account aangemaakt!"); router.push("/login"); } + setLoading(false); + }; + + return ( +
+

Registreren

+
+
+ + setEmail(e.target.value)} + className="w-full p-2 border rounded" required /> +
+
+ + setPassword(e.target.value)} + className="w-full p-2 border rounded" minLength={6} required /> +
+ +
+ {message &&

{message}

} +

+ Al een account? Inloggen +

+
+ ); +} +``` + +**Students moeten begrijpen:** +- `'use client'` = React component +- `createSupabaseBrowserClient()` = browser auth +- `supabase.auth.signUp({ email, password })` = nieuwe user +- `if (error)` = error handling +- `router.push("/login")` = redirect na success + +--- + +### 2. app/login/page.tsx + +```typescript +'use client' +import { createSupabaseBrowserClient } from "@/lib/supabase-browser"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import Link from "next/link"; + +export default function Login() { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [message, setMessage] = useState(""); + const [loading, setLoading] = useState(false); + const router = useRouter(); + const supabase = createSupabaseBrowserClient(); + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setMessage(""); + const { error } = await supabase.auth.signInWithPassword({ email, password }); + if (error) { setMessage(error.message); } + else { router.push("/"); router.refresh(); } + setLoading(false); + }; + + return ( +
+

Inloggen

+
+
+ + setEmail(e.target.value)} + className="w-full p-2 border rounded" required /> +
+
+ + setPassword(e.target.value)} + className="w-full p-2 border rounded" required /> +
+ +
+ {message &&

{message}

} +

+ Nog geen account? Registreren +

+
+ ); +} +``` + +**Key difference van signup:** +- `signInWithPassword()` i.p.v. `signUp()` +- `router.refresh()` om Navbar te update +- Error styling: `text-red-600` + +--- + +### 3. components/LogoutButton.tsx + +```typescript +'use client' +import { createSupabaseBrowserClient } from "@/lib/supabase-browser"; +import { useRouter } from "next/navigation"; + +export function LogoutButton() { + const router = useRouter(); + const supabase = createSupabaseBrowserClient(); + const handleLogout = async () => { + await supabase.auth.signOut(); + router.push("/"); + router.refresh(); + }; + return ( + + ); +} +``` + +**Dit is een klein client component. Belangrijk:** +- `'use client'` (event handler) +- `signOut()` — geen params +- `router.refresh()` — update Navbar + +--- + +### 4. components/Navbar.tsx + +Dit is het interessantste component. Server Component met `async`: + +```typescript +import Link from "next/link"; +import { createSupabaseServerClient } from "@/lib/supabase-server"; +import { LogoutButton } from "./LogoutButton"; + +export async function Navbar() { + const supabase = await createSupabaseServerClient(); + const { data: { user } } = await supabase.auth.getUser(); + return ( + + ); +} +``` + +**Vertel:** +"Navbar is een **Server Component** (geen 'use client'). Dit betekent `async` is ok. We callen `getUser()` direct — geen hooks nodig! + +`getUser()` gebruikt server client + cookies. Dit is beveiligd en efficient." + +**Logica:** +- Als `user` bestaat: toon email + LogoutButton +- Anders: toon Inloggen + Registreren links + +--- + +### 5. app/layout.tsx (update) + +Voeg Navbar toe: + +```typescript +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; +import { Navbar } from "@/components/Navbar"; + +const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"] }); +const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"] }); + +export const metadata: Metadata = { title: "QuickPoll", description: "Stem op je favoriete opties" }; + +export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { + return ( + + + + {children} + + + ); +} +``` + +--- + +### TROUBLESHOOTING (10:30–11:30) + +Als studenten stuck zijn, gebruik deze tabel: + +| Symptoom | Oorzaak | Oplossing | +|----------|---------|----------| +| "Module not found: @supabase/ssr" | npm install niet gedaan | `npm install @supabase/ssr` | +| Navbar toont altijd "Inloggen" (ook na login) | getUser() returns null | Check: cookies middleware working? Browser dev tools → Cookies (zoek sb-*) | +| Login werkt, maar redirect loopt vast | router.refresh() niet in handleLogin | Voeg `router.refresh()` toe na success | +| "Invalid PKCE flow" | Browser client not configured | Check .env: NEXT_PUBLIC_SUPABASE_URL en ANON_KEY kloppen | +| Logout knop werkt niet | signOut() niet awaited | Zorg: `await supabase.auth.signOut()` | +| Navbar.tsx error: "cannot use async in component" | Navbar is client component | Zorg: geen `'use client'` aan top van Navbar! | + +--- + +### 11:00 | CHECK-IN: NAVBAR DEMO + +Toon op beamer je eigen Navbar. Vertel: + +"Navbar is een **Server Component**. Dit is uniek voor Next.js. In React kan je geen `async` functions gebruiken als components. + +Hier kan het wel, omdat Next.js bij build-time Server Components render. Cookies zijn secure. getUser() is beveiligd. Dit is beter dan client-side auth check." + +Toon: `const { data: { user } } = await supabase.auth.getUser();` + +"Dat een lijn doet alles: leest cookies → vraagt Supabase → geeft user object." + +--- + +### 11:15 | RLS UPDATE + +Voer in Supabase dashboard SQL uit: + +**SQL Editor → New Query:** + +```sql +-- Enable RLS on polls table +ALTER TABLE polls ENABLE ROW LEVEL SECURITY; + +-- Anyone can read polls +CREATE POLICY "polls_select_all" ON polls + FOR SELECT USING (true); + +-- Only authenticated users can create polls +CREATE POLICY "polls_insert_authenticated" ON polls + FOR INSERT WITH CHECK (auth.uid() IS NOT NULL); + +-- Only the creator can update their own poll +CREATE POLICY "polls_update_owner" ON polls + FOR UPDATE USING (auth.uid() = created_by); +``` + +Vertel: +"RLS = Row Level Security. Dit enforces wie wat kan doen op database level. +- Iedereen (anoniem) kan polls zien (SELECT) +- Alleen ingelogde users (auth.uid() NOT NULL) mogen polls maken (INSERT) +- Alleen de maker mag hun eigen poll updaten (UPDATE) + +auth.uid() is de user ID van Supabase. NULL als je niet ingelogd bent." + +**Test:** +1. Open http://localhost:3000/create +2. Niet ingelogd → knop grijs / form gedeactiveerd +3. Inloggen +4. Poll aanmaken → werkt! +5. Uitloggen +6. /create opnieuw → weer grijs (RLS blokkeert INSERT) + +--- + +### 11:30–11:45 | VRAGEN & DEBUGGING + +Loopround. Antwoord vragen: + +**Q: Hoe debug ik auth?** +A: Supabase dashboard → Auth → Users. Daar zie je alle users. Of: Browser dev tools → Application → Cookies (zoek `sb-*` prefix). + +**Q: Hoe reset ik mijn test account?** +A: Dashboard → Auth → Users → klik user → delete → registreer opnieuw. + +**Q: Waarom zie ik geen email na login?** +A: Middleware werkt niet. Zorg middleware.ts in root project staat. Check: `matcher` is correct. + +**Q: Kan ik multiple providers (Google, GitHub) toevoegen?** +A: Ja, later. Dashboard → Auth → Providers. Voor nu: email-password is genoeg. + +--- + +## HUISWERK & AFSLUITING (11:45–12:00) + +**Huiswerk (slides 9):** + +1. **Profiel pagina (Les 10)** + ```typescript + // app/profile/page.tsx + import { createSupabaseServerClient } from "@/lib/supabase-server"; + + export default async function ProfilePage() { + const supabase = await createSupabaseServerClient(); + const { data: { user } } = await supabase.auth.getUser(); + return
{user?.email}
; + } + ``` + +2. **Maker tonen bij poll (Les 10)** + - Voeg `created_by uuid` kolom toe polls tabel + - Update INSERT in /create om `created_by: user.id` toe te voegen + - Toon "Gemaakt door: [email]" op homepage + +3. **Google OAuth (Bonus)** + - Supabase dashboard → Auth → Providers → Google + - Copy Client ID en Secret van Google Cloud + - Voeg button toe: `signInWithOAuth({ provider: 'google' })` + +**Afsluiting (slide 10):** + +"Volgende les: **Deployment**. We zetten je app live op Vercel. Dan kunnen je vrienden echt je polls gebruiken! + +Daarna: Google OAuth, profiel updaten, meer security features. + +Vandaag hebben we de kern van auth gebouwd. Goed gedaan!" + +--- + +## DOCS + +Supabase Auth docs: https://supabase.com/docs/guides/auth/server-side/nextjs + +Next.js Server Components: https://nextjs.org/docs/app/building-your-application/rendering/server-components diff --git a/Les09-Slide-Overzicht.md b/Les09-Slide-Overzicht.md new file mode 100644 index 0000000..4596f76 --- /dev/null +++ b/Les09-Slide-Overzicht.md @@ -0,0 +1,270 @@ +# Les 9 — Supabase Auth +## Slide Overzicht + +--- + +## Slide 1: Title +### Les 9 — Supabase Auth + +**Visual:** Large centered title with QuickPoll icon +- Background: CREAM +- "Les 9" in BLUE +- "Supabase Auth" in BLACK +- Subtitle: "signUp, signIn, signOut, Navbar, RLS" + +--- + +## Slide 2: Terugblik (Recap) +### Waar staan we? + +**Content:** +- Supabase project aangemaakt en gekoppeld +- /create pagina gebouwd +- Server Component + VoteForm patroon +- Polls werkend in database +- Real-time votes +- "Nu: beveiligde login" + +**Visual:** +- Left: screenshot van huidige app +- Right: checkmarks of badges + +--- + +## Slide 3: Planning +### Vandaag — 120 minuten + +| Tijd | Onderwerp | Duur | +|------|-----------|------| +| 09:00–09:10 | Welkom + Terugblik | 10 min | +| 09:10–10:00 | Uitleg Auth | 50 min | +| 10:00–10:15 | Samen Middleware bouwen | 15 min | +| 10:15–10:30 | **Pauze** | 15 min | +| 10:30–11:30 | Zelf Doen (signup, login, Navbar) | 60 min | +| 11:30–11:45 | Vragen & Debugging | 15 min | +| 11:45–12:00 | Huiswerk + Afsluiting | 15 min | + +**Visual:** Timeline with YELLOW background, icons per blok + +--- + +## Slide 4: Wat is Auth? +### Authenticatie vs Autorisatie + +**Authenticatie (WHO):** +- Wie ben jij? +- Email + password +- Supabase verifies en geeft JWT token +- User object: email, id, created_at + +**Autorisatie (WHAT):** +- Wat mag je doen? +- Wie mag polls maken? +- Later: RLS policies + +**Features van Supabase Auth:** +- Email/password signup & signin +- Session management (cookies) +- JWT tokens +- Password reset +- Multi-factor auth (later) +- OAuth (Google, GitHub, etc.) + +**Visual:** +- Left: "Authentication" icon (person + key) +- Right: "Authorization" icon (person + checkmark) +- Supabase logo + +--- + +## Slide 5: Auth Functies +### Vier Core Operations + +**signUp** +```typescript +const { error } = await supabase.auth.signUp({ + email: "user@example.com", + password: "secure123" +}); +``` +→ Account aanmaken + +**signInWithPassword** +```typescript +const { error } = await supabase.auth.signInWithPassword({ + email: "user@example.com", + password: "secure123" +}); +``` +→ Inloggen + +**signOut** +```typescript +await supabase.auth.signOut(); +``` +→ Uitloggen + +**getUser** +```typescript +const { data: { user } } = await supabase.auth.getUser(); +// user.email, user.id, user.email_confirmed_at +``` +→ Huidige user + +**Visual:** Code blocks in BLUE boxes, icons above each + +--- + +## Slide 6: Server vs Browser Client +### Two Clients, One Auth + +**Server Client** (@supabase/ssr) +```typescript +import { createServerClient } from "@supabase/ssr"; +import { cookies } from "next/headers"; + +export async function createSupabaseServerClient() { + 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) { /* ... */ }, + }, + } + ); +} +``` + +**Use in:** +- Middleware (refresh token) +- Server Components (Navbar, getUser) +- API routes + +**Browser Client** (@supabase/ssr) +```typescript +import { createBrowserClient } from "@supabase/ssr"; + +export function createSupabaseBrowserClient() { + return createBrowserClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! + ); +} +``` + +**Use in:** +- Client Components ('use client') +- Login forms +- Logout buttons + +**Visual:** Two side-by-side code blocks +- Left: Server (BLUE bg), lock icon +- Right: Browser (PINK bg), web icon + +**Key difference:** +- Server: Cookies (secure, secure-only, httpOnly) +- Browser: localStorage (accessible, but less safe) + +--- + +## Slide 7: Pauze +### Pauze! + +**Visual:** Relaxed illustration, "15 minuten", clock + +--- + +## Slide 8: Zelf Doen — Auth Bouwen +### Nu jij — 60 minuten + +**To-Do:** +- [ ] app/signup/page.tsx (form) +- [ ] app/login/page.tsx (form) +- [ ] components/LogoutButton.tsx +- [ ] components/Navbar.tsx (Server Component + getUser) +- [ ] app/layout.tsx (add ``) +- [ ] Update RLS policies (authenticated only!) + +**Reference code beschikbaar** (docent toont op beamer) + +**Process:** +1. Start simpel: form met email + password inputs +2. Voeg supabase.auth.signUp / signInWithPassword toe +3. Test in browser +4. Navbar: toon email of login link +5. RLS: polls INSERT nur voor authenticated users + +**Expected result:** +- Registreren → inloggen → poll maken → uitloggen +- Na logout: kan geen poll meer maken (RLS!) + +**Visual:** Big BLUE background, "Bouw Auth" header, checklist + +--- + +## Slide 9: Huiswerk +### Volgende Stap + +**Verplicht (Les 10):** +1. Profiel pagina (app/profile/page.tsx) + - Toon user.email, user.id + - Later: password update form + +2. Maker tonen bij poll + - Voeg `created_by` kolom toe polls tabel + - Toon "Gemaakt door: [email]" bij elke poll + - RLS: alleen maker mag aanpassen (UPDATE) + +**Bonus (optioneel):** +1. Google OAuth signup + - Supabase dashboard → Auth → Providers → Google + - Voeg "Sign in with Google" knop toe + +2. Password reset + - Email link naar reset form + - supabase.auth.resetPasswordForEmail() + +**Visual:** Checklist, bonus items in PINK + +--- + +## Slide 10: Afsluiting +### Volgende Les — Deployment + +**Wat hebben we gedaan vandaag:** +- Auth concepten: authenticatie vs autorisatie +- Supabase Auth functies: signUp, signIn, signOut, getUser +- Server vs browser client +- Middleware voor session refresh +- Navbar met authenticated user +- RLS policies + +**Volgende keer:** +- Vercel deployment +- Google OAuth +- Profiel pagina +- Meer security! + +**Vragen? Feedback?** + +**Visual:** Vercel logo, rocket icon, "Deployment!" in YELLOW + +--- + +## Slide Summary + +| # | Title | Duration | Key Content | +|---|-------|----------|-------------| +| 1 | Title | Opening | Les 9 — Supabase Auth | +| 2 | Recap | 09:10 | Where we are | +| 3 | Plan | 09:05 | 120-min schedule | +| 4 | Auth Concepts | 09:10 | Auth vs AuthN, Supabase features | +| 5 | Functions | 09:20 | signUp, signIn, signOut, getUser | +| 6 | Clients | 09:30 | Server vs Browser (@supabase/ssr) | +| 7 | Break | 10:15 | 15 min pauze | +| 8 | Build | 10:30 | Students implement auth | +| 9 | Homework | 11:45 | Profile, maker, Google OAuth | +| 10 | Closing | 11:55 | Next: Deployment | diff --git a/Les09-Slides.pptx b/Les09-Slides.pptx new file mode 100644 index 0000000000000000000000000000000000000000..0934b2d242c9fc17714fe2ce526938d363f0db0e GIT binary patch literal 157481 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 literal 0 HcmV?d00001