# 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