Files
novi-lessons/Les09-Live-Coding-Guide.md
2026-03-31 16:18:33 +02:00

20 KiB
Raw Blame History

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

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

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

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

Students volgen mee terwijl je dit live codeert (of ze kopieren uit les09-live-coding-guide.md).

Stap 1: npm install

npm install @supabase/ssr

Wacht tot dit klaar is.

Stap 2: lib/supabase-server.ts

Maak dit bestand aan:

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

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

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

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


DEEL 2: ZELF DOEN (10:3011: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:

'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>
    );
}

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

'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>
    );
}

Key difference van signup:

  • signInWithPassword() i.p.v. signUp()
  • router.refresh() om Navbar te update
  • Error styling: text-red-600

3. 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>
    );
}

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:

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>
    );
}

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:

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>
    );
}

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

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

Huiswerk (slides 9):

  1. Profiel pagina (Les 10)

    // 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 <div>{user?.email}</div>;
    }
    
  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