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

623 lines
20 KiB
Markdown
Raw 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
## Live Coding Guide voor Docent
This is your cheat sheet for the full lesson. Follow the timing in Les09-Docenttekst.md.
---
## DEEL 1A: UITLEG AUTH (09:1010:00)
### 09:10 | SLIDE 4: Wat is Auth?
**Demo:** Open https://supabase.com/dashboard
Vertel:
"Authenticatie is: wie ben jij? Login en password, je identiteit bewijzen.
Autorisatie is: wat mag je doen? Wie mag polls maken? Dit regelen we met RLS policies.
Supabase Auth beheert alles: signUp, login, sessies, JWT tokens."
**Stap 1:** Dashboard openen, project selecteren
**Stap 2:** Klik Authentication → Providers → Email
**Stap 3:** Toon: "Disable Email Confirmations" is AAN
Vertel:
"Deze checkbox is cruciaal voor testen. Normaal zouden users een confirmation email krijgen voordat ze inloggen. Dat slaan we over voor deze les. In productie zet je dit uit."
---
### 09:20 | SLIDE 5: Auth Functies
**Vertel:**
"Supabase Auth heeft vier kern functies:"
**Code tonen (copy-paste in terminal of code editor):**
```typescript
// 1. signUp — Nieuw account
const { error } = await supabase.auth.signUp({
email: "user@example.com",
password: "secure123"
});
if (error) console.error(error.message);
// 2. signInWithPassword — Inloggen
const { error } = await supabase.auth.signInWithPassword({
email: "user@example.com",
password: "secure123"
});
if (error) console.error(error.message);
// 3. signOut — Uitloggen
await supabase.auth.signOut();
// 4. getUser — Wie is ingelogd?
const { data: { user } } = await supabase.auth.getUser();
console.log(user.email); // "user@example.com"
console.log(user.id); // "abc-123-def"
```
Vertel:
"Diese vier functies zijn alles wat je nodig hebt. Error handling: altijd checken of `error` null is."
---
### 09:30 | SLIDE 6: Server vs Browser Client
**Vertel:**
"Supabase Auth werkt op twee plaatsen:
1. **Server (Node.js)** — Secure, via cookies
2. **Browser (React)** — Less secure, via localStorage
We gebruiken @supabase/ssr. Dit switcht automatisch."
**Show left code block (Server Client):**
```typescript
// lib/supabase-server.ts
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 { }
},
},
}
);
}
```
Vertel:
"Server client gebruikt cookies. Cookies kunnen beveiligd worden (httpOnly, secure-only). Dit is safer."
**Show right code block (Browser Client):**
```typescript
// lib/supabase-browser.ts
import { createBrowserClient } from "@supabase/ssr";
export function createSupabaseBrowserClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
}
```
Vertel:
"Browser client werkt in React. localStorage is minder secure (scripts kunnen het uitlezen), maar nodig voor login forms."
**Key difference:**
"Server component (Navbar) → Server client (getUser)
Client component (LoginForm) → Browser client (signUp, signIn, signOut)"
---
## DEEL 1B: SAMEN CODEREN (10:0010:15)
Students volgen mee terwijl je dit live codeert (of ze kopieren uit les09-live-coding-guide.md).
### Stap 1: npm install
```bash
npm install @supabase/ssr
```
Wacht tot dit klaar is.
### Stap 2: lib/supabase-server.ts
Maak dit bestand aan:
```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
```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 of project, naast app/)
```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)$).*)"],
};
```
Vertel:
"Middleware runt op elke request. `await supabase.auth.getUser()` refresht de session token. Dit zorgt dat je niet uitgelogd wordt als je token expired."
### Stap 5: 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);
}
```
Vertel:
"Dit is voor OAuth (Google, GitHub). Supabase stuur je hier naartoe na login. De `code` wordt ge-exchanged voor een session. Voor nu: boilerplate, niet essentieel."
### Test middleware
Vertel:
"Test of middleware werkt: open http://localhost:3000. Je zou geen errors moeten zien. In browser dev tools → Application → Cookies → zoek naar `sb-*`. Die cookies beteken dat middleware goed werkt."
---
## PAUZE (10:1510:30)
---
## DEEL 2: ZELF DOEN (10:3011:30)
Students bouwen nu zelf. Jij loopt rond, helpt, en toont code op beamer als studenten stuck zijn.
### Zelf Doen Checklist (wat moet elke student doen):
- [ ] app/signup/page.tsx
- [ ] app/login/page.tsx
- [ ] components/LogoutButton.tsx
- [ ] components/Navbar.tsx
- [ ] app/layout.tsx updated
- [ ] Test signup → login → poll maken → logout
---
### 1. app/signup/page.tsx
Dit is een 'use client' component met form. Students schrijven dit zelf, maar hier is de reference:
```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>
);
}
```
**Students moeten begrijpen:**
- `'use client'` = React component
- `createSupabaseBrowserClient()` = browser auth
- `supabase.auth.signUp({ email, password })` = nieuwe user
- `if (error)` = error handling
- `router.push("/login")` = redirect na success
---
### 2. 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>
);
}
```
**Key difference van signup:**
- `signInWithPassword()` i.p.v. `signUp()`
- `router.refresh()` om Navbar te update
- Error styling: `text-red-600`
---
### 3. 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>
);
}
```
**Dit is een klein client component. Belangrijk:**
- `'use client'` (event handler)
- `signOut()` — geen params
- `router.refresh()` — update Navbar
---
### 4. components/Navbar.tsx
Dit is het interessantste component. Server Component met `async`:
```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>
);
}
```
**Vertel:**
"Navbar is een **Server Component** (geen 'use client'). Dit betekent `async` is ok. We callen `getUser()` direct — geen hooks nodig!
`getUser()` gebruikt server client + cookies. Dit is beveiligd en efficient."
**Logica:**
- Als `user` bestaat: toon email + LogoutButton
- Anders: toon Inloggen + Registreren links
---
### 5. app/layout.tsx (update)
Voeg Navbar toe:
```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>
);
}
```
---
### TROUBLESHOOTING (10:3011:30)
Als studenten stuck zijn, gebruik deze tabel:
| Symptoom | Oorzaak | Oplossing |
|----------|---------|----------|
| "Module not found: @supabase/ssr" | npm install niet gedaan | `npm install @supabase/ssr` |
| Navbar toont altijd "Inloggen" (ook na login) | getUser() returns null | Check: cookies middleware working? Browser dev tools → Cookies (zoek sb-*) |
| Login werkt, maar redirect loopt vast | router.refresh() niet in handleLogin | Voeg `router.refresh()` toe na success |
| "Invalid PKCE flow" | Browser client not configured | Check .env: NEXT_PUBLIC_SUPABASE_URL en ANON_KEY kloppen |
| Logout knop werkt niet | signOut() niet awaited | Zorg: `await supabase.auth.signOut()` |
| Navbar.tsx error: "cannot use async in component" | Navbar is client component | Zorg: geen `'use client'` aan top van Navbar! |
---
### 11:00 | CHECK-IN: NAVBAR DEMO
Toon op beamer je eigen Navbar. Vertel:
"Navbar is een **Server Component**. Dit is uniek voor Next.js. In React kan je geen `async` functions gebruiken als components.
Hier kan het wel, omdat Next.js bij build-time Server Components render. Cookies zijn secure. getUser() is beveiligd. Dit is beter dan client-side auth check."
Toon: `const { data: { user } } = await supabase.auth.getUser();`
"Dat een lijn doet alles: leest cookies → vraagt Supabase → geeft user object."
---
### 11:15 | RLS UPDATE
Voer in Supabase dashboard SQL uit:
**SQL Editor → New Query:**
```sql
-- Enable RLS on polls table
ALTER TABLE polls ENABLE ROW LEVEL SECURITY;
-- Anyone can read polls
CREATE POLICY "polls_select_all" ON polls
FOR SELECT USING (true);
-- Only authenticated users can create polls
CREATE POLICY "polls_insert_authenticated" ON polls
FOR INSERT WITH CHECK (auth.uid() IS NOT NULL);
-- Only the creator can update their own poll
CREATE POLICY "polls_update_owner" ON polls
FOR UPDATE USING (auth.uid() = created_by);
```
Vertel:
"RLS = Row Level Security. Dit enforces wie wat kan doen op database level.
- Iedereen (anoniem) kan polls zien (SELECT)
- Alleen ingelogde users (auth.uid() NOT NULL) mogen polls maken (INSERT)
- Alleen de maker mag hun eigen poll updaten (UPDATE)
auth.uid() is de user ID van Supabase. NULL als je niet ingelogd bent."
**Test:**
1. Open http://localhost:3000/create
2. Niet ingelogd → knop grijs / form gedeactiveerd
3. Inloggen
4. Poll aanmaken → werkt!
5. Uitloggen
6. /create opnieuw → weer grijs (RLS blokkeert INSERT)
---
### 11:3011:45 | VRAGEN & DEBUGGING
Loopround. Antwoord vragen:
**Q: Hoe debug ik auth?**
A: Supabase dashboard → Auth → Users. Daar zie je alle users. Of: Browser dev tools → Application → Cookies (zoek `sb-*` prefix).
**Q: Hoe reset ik mijn test account?**
A: Dashboard → Auth → Users → klik user → delete → registreer opnieuw.
**Q: Waarom zie ik geen email na login?**
A: Middleware werkt niet. Zorg middleware.ts in root project staat. Check: `matcher` is correct.
**Q: Kan ik multiple providers (Google, GitHub) toevoegen?**
A: Ja, later. Dashboard → Auth → Providers. Voor nu: email-password is genoeg.
---
## HUISWERK & AFSLUITING (11:4512:00)
**Huiswerk (slides 9):**
1. **Profiel pagina (Les 10)**
```typescript
// app/profile/page.tsx
import { createSupabaseServerClient } from "@/lib/supabase-server";
export default async function ProfilePage() {
const supabase = await createSupabaseServerClient();
const { data: { user } } = await supabase.auth.getUser();
return <div>{user?.email}</div>;
}
```
2. **Maker tonen bij poll (Les 10)**
- Voeg `created_by uuid` kolom toe polls tabel
- Update INSERT in /create om `created_by: user.id` toe te voegen
- Toon "Gemaakt door: [email]" op homepage
3. **Google OAuth (Bonus)**
- Supabase dashboard → Auth → Providers → Google
- Copy Client ID en Secret van Google Cloud
- Voeg button toe: `signInWithOAuth({ provider: 'google' })`
**Afsluiting (slide 10):**
"Volgende les: **Deployment**. We zetten je app live op Vercel. Dan kunnen je vrienden echt je polls gebruiken!
Daarna: Google OAuth, profiel updaten, meer security features.
Vandaag hebben we de kern van auth gebouwd. Goed gedaan!"
---
## DOCS
Supabase Auth docs: https://supabase.com/docs/guides/auth/server-side/nextjs
Next.js Server Components: https://nextjs.org/docs/app/building-your-application/rendering/server-components