Files
novi-lessons/Les09-Supabase-Auth/Les09-Docenttekst.md
2026-03-31 16:34:28 +02:00

18 KiB
Raw Permalink Blame History

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:0009: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:1010:00: Uitleg Auth concepten + Demo
  • 10:0010:15: Samen Middleware + Auth Callback bouwen
  • 10:1510:30: Pauze
  • 10:3011:30: Zelf Doen (signup, login, logout, Navbar)
  • 11:3011:45: Vragen
  • 11:4512:00: Huiswerk + Afsluiting

09:1010: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):

// 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):

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):

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:

// 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:0010:15 | Deel 1b: Samen Coderen (15 min)

Stap 1: npm install

npm install @supabase/ssr

Stap 2: lib/supabase-server.ts aanmaken

Voeg dit in (hieronder exact):

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

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)

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:

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:1510:30 | Pauze

Slide 7


10:3011: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

'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 (
        <div className="w-full max-w-md mx-auto p-6">
            <h1 className="text-2xl font-bold mb-6">Registreren</h1>
            <form onSubmit={handleSignUp} className="space-y-4">
                <div>
                    <label className="block text-sm font-medium mb-1">Email</label>
                    <input type="email" value={email} onChange={(e) => setEmail(e.target.value)}
                        className="w-full p-2 border rounded" required />
                </div>
                <div>
                    <label className="block text-sm font-medium mb-1">Wachtwoord</label>
                    <input type="password" value={password} onChange={(e) => setPassword(e.target.value)}
                        className="w-full p-2 border rounded" minLength={6} required />
                </div>
                <button type="submit" disabled={loading}
                    className="w-full bg-blue-600 text-white p-2 rounded hover:bg-blue-700 disabled:opacity-50">
                    {loading ? "Bezig..." : "Registreren"}
                </button>
            </form>
            {message && <p className="mt-4 text-sm text-center">{message}</p>}
            <p className="mt-4 text-sm text-center">
                Al een account? <Link href="/login" className="text-blue-600 hover:underline">Inloggen</Link>
            </p>
        </div>
    );
}

app/login/page.tsx

'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 (
        <div className="w-full max-w-md mx-auto p-6">
            <h1 className="text-2xl font-bold mb-6">Inloggen</h1>
            <form onSubmit={handleLogin} className="space-y-4">
                <div>
                    <label className="block text-sm font-medium mb-1">Email</label>
                    <input type="email" value={email} onChange={(e) => setEmail(e.target.value)}
                        className="w-full p-2 border rounded" required />
                </div>
                <div>
                    <label className="block text-sm font-medium mb-1">Wachtwoord</label>
                    <input type="password" value={password} onChange={(e) => setPassword(e.target.value)}
                        className="w-full p-2 border rounded" required />
                </div>
                <button type="submit" disabled={loading}
                    className="w-full bg-blue-600 text-white p-2 rounded hover:bg-blue-700 disabled:opacity-50">
                    {loading ? "Bezig..." : "Inloggen"}
                </button>
            </form>
            {message && <p className="mt-4 text-sm text-red-600 text-center">{message}</p>}
            <p className="mt-4 text-sm text-center">
                Nog geen account? <Link href="/signup" className="text-blue-600 hover:underline">Registreren</Link>
            </p>
        </div>
    );
}

components/LogoutButton.tsx

'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 (
        <button onClick={handleLogout} className="text-sm text-gray-600 hover:text-gray-900">
            Uitloggen
        </button>
    );
}

components/Navbar.tsx

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 (
        <nav className="w-full border-b p-4 flex justify-between items-center">
            <Link href="/" className="text-xl font-bold">QuickPoll</Link>
            <div className="flex items-center gap-4">
                {user ? (
                    <>
                        <span className="text-sm text-gray-600">{user.email}</span>
                        <LogoutButton />
                    </>
                ) : (
                    <>
                        <Link href="/login" className="text-sm hover:underline">Inloggen</Link>
                        <Link href="/signup" className="text-sm bg-blue-600 text-white px-3 py-1 rounded hover:bg-blue-700">Registreren</Link>
                    </>
                )}
            </div>
        </nav>
    );
}

app/layout.tsx (updated)

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 (
        <html lang="nl">
            <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
                <Navbar />
                {children}
            </body>
        </html>
    );
}

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 <Navbar /> 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:

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:3011: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:4512: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