6.4 KiB
6.4 KiB
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
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
- Open http://localhost:3000 → redirect naar /login
- Vul echte email in (jouw eigen)
- Klik Magic Link → "Check je email!"
- Check je inbox (en spam!) — magic link mail
- Klik link → redirect → home page met "Welkom {email}"
- 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.