Files
novi-lessons/Les09-Supabase-Auth/Les09-Docenttekst.md
2026-03-31 16:34:28 +02:00

553 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Les 9 — Supabase Auth
## Docenttekst
**Les:** 9 van 18
**Onderwerp:** Supabase Authentication (signUp, signIn, signOut, middleware, RLS)
**Duur:** 120 minuten
**Vorige les:** Les 8 — Students hebben Supabase gekoppeld, /create pagina werkend, Server Component patroon, polls database
---
## Leerdoelen
- Authenticatie vs autorisatie begrijpen
- Supabase Auth functies gebruiken: signUp, signInWithPassword, signOut, getUser
- Server client (SSR) vs Browser client onderscheiden
- Middleware voor session refresh implementeren
- Authenticated Navbar bouwen met getUser
- Row Level Security (RLS) voor authenticated users toepassen
---
## Lesopbouw & Timing
### 09:0009:10 | Welkom + Terugblik (10 min)
**Slides:** 1, 2, 3
Ik start de les. Korte recap van Les 8:
- Supabase project aangemaakt
- NEXT_PUBLIC_SUPABASE_URL en ANON_KEY in .env
- /create pagina with VoteForm component
- Polls tabel in database met votes
- "Na vandaag kunnen jullie je app beveiligen met authenticatie"
**Planning tonen (slide 3):**
- 09:1010:00: Uitleg Auth concepten + Demo
- 10:0010:15: Samen Middleware + Auth Callback bouwen
- 10:1510:30: Pauze
- 10:3011:30: Zelf Doen (signup, login, logout, Navbar)
- 11:3011:45: Vragen
- 11:4512:00: Huiswerk + Afsluiting
---
### 09:1010:00 | Deel 1a: Uitleg Auth Concepten (50 min)
**Slides:** 4, 5, 6
#### 09:10 | Slide 4: Wat is Auth?
**Vertel:**
"Authenticatie is: wie ben jij? Login, password, je identiteit bewijzen.
Autorisatie is: wat mag je doen? Wie mag polls maken? Dit regelen we later met RLS.
Supabase Auth beheert alles: signUp, login, sessies, JWT tokens."
**Demo:** Open https://supabase.com/dashboard
- Klik project → Authentication → Providers → Email
- Laat zien: Disable Email Confirmations is AAN (sneller testen)
- Zeg: "Students zien zelf deze checkbox na Le 9"
#### 09:20 | Slide 5: Auth Functies
**Vertel:**
"Vier kern functies in Supabase Auth:
1. signUp({ email, password }) — Nieuw account
2. signInWithPassword({ email, password }) — Inloggen
3. signOut() — Uitloggen
4. getUser() — Wie is ingelogd?
Hieronder toon ik hoe we deze gebruiken in Next.js."
**Code tonen (slide 5):**
```typescript
// signUp
const { error } = await supabase.auth.signUp({ email, password });
// signIn
const { error } = await supabase.auth.signInWithPassword({ email, password });
// signOut
await supabase.auth.signOut();
// getUser (server of browser)
const { data: { user } } = await supabase.auth.getUser();
```
#### 09:30 | Slide 6: Server vs Browser Client
**Vertel:**
"Supabase Auth werkt in twee omgevingen:
- **Server Client** (Node.js, SSR): via cookies, secure
- **Browser Client** (React, CSR): via localstorage, minder secure
We gebruiken @supabase/ssr package. Dit handelt beide af."
**Toon twee code blokken naast elkaar (slide 6):**
**Server Client** (middleware, Navbar):
```typescript
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
export async function createSupabaseServerClient() {
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 { }
},
},
}
);
}
```
**Browser Client** (signup, login, logout):
```typescript
import { createBrowserClient } from "@supabase/ssr";
export function createSupabaseBrowserClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
}
```
**Zeg:**
"Cookies zijn beveiligd. localStorage in browser kan hack worden. Daarom: server client voor getUser in Navbar, browser client voor login forms."
**📌 Slide 6 referentie voor Middleware:**
Middleware zorgt dat de session word gerefresht op elke request:
```typescript
// middleware.ts
export async function middleware(request: NextRequest) {
let supabaseResponse = NextResponse.next({ request });
const supabase = createServerClient(...);
await supabase.auth.getUser();
return supabaseResponse;
}
```
"Dit zorgt dat je Session JWT token altijd up-to-date is."
---
### 10:0010:15 | Deel 1b: Samen Coderen (15 min)
#### Stap 1: npm install
```bash
npm install @supabase/ssr
```
#### Stap 2: lib/supabase-server.ts aanmaken
Voeg dit in (hieronder exact):
```typescript
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
export async function createSupabaseServerClient() {
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: lib/supabase-browser.ts aanmaken
```typescript
import { createBrowserClient } from "@supabase/ssr";
export function createSupabaseBrowserClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
}
```
#### Stap 4: middleware.ts (root project)
```typescript
import { createServerClient } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";
export async function middleware(request: NextRequest) {
let supabaseResponse = 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));
supabaseResponse = NextResponse.next({ request });
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options)
);
},
},
}
);
await supabase.auth.getUser();
return supabaseResponse;
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)"],
};
```
#### Stap 5: auth/callback route
`app/auth/callback/route.ts`:
```typescript
import { NextResponse } from "next/server";
import { createSupabaseServerClient } from "@/lib/supabase-server";
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url);
const code = searchParams.get("code");
if (code) {
const supabase = await createSupabaseServerClient();
await supabase.auth.exchangeCodeForSession(code);
}
return NextResponse.redirect(origin);
}
```
**Zeg:** "Dit is standaard Supabase/Next.js boilerplate. Niet allemaal letterlijk begrijpen. Focus op: server vs browser client."
---
### 10:1510:30 | Pauze
**Slide 7**
---
### 10:3011:30 | Deel 2: Zelf Doen (60 min)
**Slide 8**
Students bouwen nu zelf:
1. app/signup/page.tsx
2. app/login/page.tsx
3. components/LogoutButton.tsx
4. components/Navbar.tsx (met getUser)
5. Uitloggen in layout.tsx
**Reference code:**
#### app/signup/page.tsx
```typescript
'use client'
import { createSupabaseBrowserClient } from "@/lib/supabase-browser";
import { useRouter } from "next/navigation";
import { useState } from "react";
import Link from "next/link";
export default function SignUp() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [message, setMessage] = useState("");
const [loading, setLoading] = useState(false);
const router = useRouter();
const supabase = createSupabaseBrowserClient();
const handleSignUp = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setMessage("");
const { error } = await supabase.auth.signUp({ email, password });
if (error) { setMessage(error.message); }
else { setMessage("Account aangemaakt!"); router.push("/login"); }
setLoading(false);
};
return (
<div className="w-full max-w-md mx-auto p-6">
<h1 className="text-2xl font-bold mb-6">Registreren</h1>
<form onSubmit={handleSignUp} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">Email</label>
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)}
className="w-full p-2 border rounded" required />
</div>
<div>
<label className="block text-sm font-medium mb-1">Wachtwoord</label>
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)}
className="w-full p-2 border rounded" minLength={6} required />
</div>
<button type="submit" disabled={loading}
className="w-full bg-blue-600 text-white p-2 rounded hover:bg-blue-700 disabled:opacity-50">
{loading ? "Bezig..." : "Registreren"}
</button>
</form>
{message && <p className="mt-4 text-sm text-center">{message}</p>}
<p className="mt-4 text-sm text-center">
Al een account? <Link href="/login" className="text-blue-600 hover:underline">Inloggen</Link>
</p>
</div>
);
}
```
#### app/login/page.tsx
```typescript
'use client'
import { createSupabaseBrowserClient } from "@/lib/supabase-browser";
import { useRouter } from "next/navigation";
import { useState } from "react";
import Link from "next/link";
export default function Login() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [message, setMessage] = useState("");
const [loading, setLoading] = useState(false);
const router = useRouter();
const supabase = createSupabaseBrowserClient();
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setMessage("");
const { error } = await supabase.auth.signInWithPassword({ email, password });
if (error) { setMessage(error.message); }
else { router.push("/"); router.refresh(); }
setLoading(false);
};
return (
<div className="w-full max-w-md mx-auto p-6">
<h1 className="text-2xl font-bold mb-6">Inloggen</h1>
<form onSubmit={handleLogin} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">Email</label>
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)}
className="w-full p-2 border rounded" required />
</div>
<div>
<label className="block text-sm font-medium mb-1">Wachtwoord</label>
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)}
className="w-full p-2 border rounded" required />
</div>
<button type="submit" disabled={loading}
className="w-full bg-blue-600 text-white p-2 rounded hover:bg-blue-700 disabled:opacity-50">
{loading ? "Bezig..." : "Inloggen"}
</button>
</form>
{message && <p className="mt-4 text-sm text-red-600 text-center">{message}</p>}
<p className="mt-4 text-sm text-center">
Nog geen account? <Link href="/signup" className="text-blue-600 hover:underline">Registreren</Link>
</p>
</div>
);
}
```
#### components/LogoutButton.tsx
```typescript
'use client'
import { createSupabaseBrowserClient } from "@/lib/supabase-browser";
import { useRouter } from "next/navigation";
export function LogoutButton() {
const router = useRouter();
const supabase = createSupabaseBrowserClient();
const handleLogout = async () => {
await supabase.auth.signOut();
router.push("/");
router.refresh();
};
return (
<button onClick={handleLogout} className="text-sm text-gray-600 hover:text-gray-900">
Uitloggen
</button>
);
}
```
#### components/Navbar.tsx
```typescript
import Link from "next/link";
import { createSupabaseServerClient } from "@/lib/supabase-server";
import { LogoutButton } from "./LogoutButton";
export async function Navbar() {
const supabase = await createSupabaseServerClient();
const { data: { user } } = await supabase.auth.getUser();
return (
<nav className="w-full border-b p-4 flex justify-between items-center">
<Link href="/" className="text-xl font-bold">QuickPoll</Link>
<div className="flex items-center gap-4">
{user ? (
<>
<span className="text-sm text-gray-600">{user.email}</span>
<LogoutButton />
</>
) : (
<>
<Link href="/login" className="text-sm hover:underline">Inloggen</Link>
<Link href="/signup" className="text-sm bg-blue-600 text-white px-3 py-1 rounded hover:bg-blue-700">Registreren</Link>
</>
)}
</div>
</nav>
);
}
```
#### app/layout.tsx (updated)
```typescript
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { Navbar } from "@/components/Navbar";
const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"] });
const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"] });
export const metadata: Metadata = { title: "QuickPoll", description: "Stem op je favoriete opties" };
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="nl">
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
<Navbar />
{children}
</body>
</html>
);
}
```
**Instructies voor students:**
1. Maak app/signup/page.tsx — form met email/password inputs
2. Maak app/login/page.tsx — inlog form
3. Maak components/LogoutButton.tsx — knop die signOut() aanroept
4. Maak components/Navbar.tsx — toon email als ingelogd, login/signup links anders
5. Update layout.tsx — voeg `<Navbar />` toe
**Ik loop rond en help. Studenten kunnen stuck raken op:**
#### Veelvoorkomende problemen
| Probleem | Oorzaak | Oplossing |
|----------|---------|----------|
| "Module not found: @supabase/ssr" | npm install niet gedaan | `npm install @supabase/ssr` |
| Navbar toont altijd "Inloggen" | getUser() returns null | Check cookies middleware, browser dev tools |
| Login werkt niet | Verkeerde credentials | Check Supabase dashboard → Auth Users |
| "Invalid PKCE flow" | Browser client misconfigured | Zorg dat .env keys correct zijn |
| Logout werkt niet | signOut() niet wacht | `await supabase.auth.signOut()` |
| Layout.tsx error: Navbar is async | Navbar is Server Component | `async` is ok, use await in getUser() |
---
#### 11:00 | Check-in: Navbar
Ik check of iedereen Navbar werkend heeft. Zeg:
"Navbar is een **Server Component** (async). Daarom kunnen we direct getUser() callen zonder hooks. Dit is uniek voor Next.js."
Toon: `const { data: { user } } = await supabase.auth.getUser();`
#### 11:15 | RLS Update
**Vertel:**
"Nu authenticatie werkt, beveiligen we polls. Wie mag die maken?
- Anoniem (niet ingelogd): mag zien en stemmen
- Authenticated (ingelogd): mag polls maken EN zien EN stemmen"
**Stap 1:** Open Supabase dashboard → SQL Editor
**Stap 2:** Voer uit:
```sql
ALTER TABLE polls ENABLE ROW LEVEL SECURITY;
CREATE POLICY "polls_select_all" ON polls
FOR SELECT USING (true);
CREATE POLICY "polls_insert_authenticated" ON polls
FOR INSERT WITH CHECK (auth.uid() IS NOT NULL);
CREATE POLICY "polls_update_owner" ON polls
FOR UPDATE USING (auth.uid() = created_by);
```
(Zeg: "Auth.uid() is de ID van ingelogde user. NULL als anoniem.")
**Stap 3:** Test in /create:
- Niet ingelogd: Knop grijs / gedeactiveerd
- Ingelogd: Knop blauw, poll aanmaken werkt
- Na uitloggen: Weer grijs
---
### 11:3011:45 | Vragen & Debugging
Ik loop rond. Studenten kunnen vragen:
- "Hoe debug ik auth?"
- Supabase dashboard → Auth Users
- Browser dev tools → Application → Cookies (zoek sb-*)
- "Hoe reset ik mijn account?"
- Dashboard → Auth Users → delete user → registreer opnieuw
---
### 11:4512:00 | Huiswerk + Afsluiting (15 min)
**Slides:** 9, 10
**Slide 9: Huiswerk**
1. **Google OAuth (optioneel, moeilijk)**
- Supabase dashboard → Auth → Providers → Google
- Copy Client ID, Secret
- Voeg signInWithOAuth button toe
2. **Profiel pagina (les 10)**
- app/profile/page.tsx
- Toon user.email, user.id
- Update password / email form (kan les 10 zijn)
3. **Maker tonen bij poll (les 10)**
- Voeg `created_by` toe aan polls tabel
- Toon bij elke poll wie het maakte
- Autorisatie: alleen maker mag aanpassen
**Slide 10: Afsluiting**
"Volgende les: Deployment! We zetten je app live op Vercel. Daarna: Google OAuth, profiel, meer RLS."
---
## Extra: Supabase Auth Docs
https://supabase.com/docs/guides/auth/server-side/nextjs