284 lines
6.4 KiB
Markdown
284 lines
6.4 KiB
Markdown
# 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 (
|
|
<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`:
|
|
```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 (
|
|
<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
|
|
|
|
```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`.
|