# Les 18 — Lesopdracht ## Magic link login + protected home page **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 ```bash 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`: ```typescript 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`: ```typescript 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`: ```typescript 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): ```typescript 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`: ```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 (

Check je email!

We hebben een magic link gestuurd naar {email}.

); return (

Login

setEmail(e.target.value)} placeholder="jouw@email.nl" className="border p-2 w-full mb-3" />
); } ``` --- ## Stap 5 — Callback route `app/auth/callback/route.ts`: ```typescript 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`: ```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 (

Welkom {user.email}

{ "use server"; const sb = await createClient(); await sb.auth.signOut(); redirect("/login"); }}>
); } ``` --- ## Stap 7 — Test ```bash 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`.