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 0000000..0934b2d Binary files /dev/null and b/Les09-Slides.pptx differ