fix: add les 9
This commit is contained in:
552
Les09-Docenttekst.md
Normal file
552
Les09-Docenttekst.md
Normal file
@@ -0,0 +1,552 @@
|
||||
# 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:00–09: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:10–10:00: Uitleg Auth concepten + Demo
|
||||
- 10:00–10:15: Samen Middleware + Auth Callback bouwen
|
||||
- 10:15–10:30: Pauze
|
||||
- 10:30–11:30: Zelf Doen (signup, login, logout, Navbar)
|
||||
- 11:30–11:45: Vragen
|
||||
- 11:45–12:00: Huiswerk + Afsluiting
|
||||
|
||||
---
|
||||
|
||||
### 09:10–10: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:00–10: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:15–10:30 | Pauze
|
||||
**Slide 7**
|
||||
|
||||
---
|
||||
|
||||
### 10:30–11: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:30–11: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:45–12: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
|
||||
Reference in New Issue
Block a user