fix: update lessons
This commit is contained in:
283
Les18-Supabase-Auth-RLS/Les18-Lesopdracht.md
Normal file
283
Les18-Supabase-Auth-RLS/Les18-Lesopdracht.md
Normal file
@@ -0,0 +1,283 @@
|
||||
# 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`.
|
||||
Reference in New Issue
Block a user