Files
novi-lessons/Les18-Supabase-Auth-RLS/Les18-Lesopdracht.md
2026-06-03 16:58:25 +02:00

6.4 KiB

Les 18 — Lesopdracht

Duur: 30 min in-class


Doel

Werkende Next.js app met Supabase magic link login. Niet ingelogd → redirect naar /login. Ingelogd → home page met "Welkom {email}".


Stap 1 — Setup

pnpm create next-app@latest auth-demo \
  --typescript --tailwind --app --no-src-dir --import-alias "@/*"
cd auth-demo
pnpm add @supabase/supabase-js @supabase/ssr

.env.local:

NEXT_PUBLIC_SUPABASE_URL=https://...supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...

In Supabase Dashboard:

  • Auth → Email → enable "Magic Links"
  • Auth → URL Configuration → Site URL: http://localhost:3000

Stap 2 — Supabase clients

utils/supabase/client.ts:

import { createBrowserClient } from "@supabase/ssr";

export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
  );
}

utils/supabase/server.ts:

import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";

export async function createClient() {
  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 — Middleware

utils/supabase/middleware.ts:

import { createServerClient } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";

export async function updateSession(request: NextRequest) {
  let response = 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)
          );
          response = NextResponse.next({ request });
          cookiesToSet.forEach(({ name, value, options }) =>
            response.cookies.set(name, value, options)
          );
        },
      },
    },
  );

  await supabase.auth.getUser();
  return response;
}

middleware.ts (root):

import { type NextRequest } from "next/server";
import { updateSession } from "@/utils/supabase/middleware";

export async function middleware(request: NextRequest) {
  return await updateSession(request);
}

export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};

Stap 4 — Login page

app/login/page.tsx:

"use client";
import { createClient } from "@/utils/supabase/client";
import { useState } from "react";

export default function Login() {
  const [email, setEmail] = useState("");
  const [sent, setSent] = useState(false);

  async function signIn() {
    const supabase = createClient();
    await supabase.auth.signInWithOtp({
      email,
      options: { emailRedirectTo: `${location.origin}/auth/callback` },
    });
    setSent(true);
  }

  if (sent) return (
    <main className="p-8 max-w-md mx-auto">
      <h1 className="text-2xl mb-4">Check je email!</h1>
      <p>We hebben een magic link gestuurd naar <b>{email}</b>.</p>
    </main>
  );

  return (
    <main className="p-8 max-w-md mx-auto">
      <h1 className="text-2xl mb-4">Login</h1>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="jouw@email.nl"
        className="border p-2 w-full mb-3"
      />
      <button
        onClick={signIn}
        className="bg-blue-600 text-white px-4 py-2 rounded"
      >
        Magic Link
      </button>
    </main>
  );
}

Stap 5 — Callback route

app/auth/callback/route.ts:

import { createClient } from "@/utils/supabase/server";
import { NextResponse } from "next/server";

export async function GET(request: Request) {
  const { searchParams, origin } = new URL(request.url);
  const code = searchParams.get("code");

  if (code) {
    const supabase = await createClient();
    await supabase.auth.exchangeCodeForSession(code);
  }

  return NextResponse.redirect(`${origin}/`);
}

Stap 6 — Protected home

app/page.tsx:

import { createClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";

export default async function Home() {
  const supabase = await createClient();
  const { data: { user } } = await supabase.auth.getUser();
  if (!user) redirect("/login");

  return (
    <main className="p-8">
      <h1 className="text-3xl">Welkom {user.email}</h1>
      <form action={async () => {
        "use server";
        const sb = await createClient();
        await sb.auth.signOut();
        redirect("/login");
      }}>
        <button className="mt-4 underline">Uitloggen</button>
      </form>
    </main>
  );
}

Stap 7 — Test

pnpm dev
  1. Open http://localhost:3000 → redirect naar /login
  2. Vul echte email in (jouw eigen)
  3. Klik Magic Link → "Check je email!"
  4. Check je inbox (en spam!) — magic link mail
  5. Klik link → redirect → home page met "Welkom {email}"
  6. Klik Uitloggen → terug naar login

Eisen

  • Supabase project met Magic Links ingeschakeld
  • Niet ingelogd → redirect naar /login
  • Magic link email komt aan
  • Klikken op link logt user in
  • Home page toont user email
  • Uitloggen werkt

Tijdsindeling (30 min)

Stap Tijd
1 — Setup 4 min
2-3 — Clients + middleware 8 min
4 — Login page 5 min
5 — Callback 3 min
6 — Protected home 5 min
7 — Test 5 min

Veelvoorkomende problemen

Symptoom Oplossing
Email komt niet aan Check spam. Productie: eigen SMTP via Resend
Invalid redirect_url Auth → URL Configuration → Site URL match
Callback URL 404 Maak app/auth/callback/route.ts aan
Loop tussen login + home Middleware werkt niet — check matcher
cookies is not a function await cookies() in Next 15+

Klaar?

Verder met huiswerk: voeg RLS toe + tasks tabel + multi-user. Zie Les18-Huiswerk.md.