fix: add les 9

This commit is contained in:
2026-03-31 16:18:33 +02:00
parent ca11a67016
commit b9ffee586f
5 changed files with 1707 additions and 0 deletions

552
Les09-Docenttekst.md Normal file
View 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: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

263
Les09-Lesopdracht.pdf Normal file
View File

@@ -0,0 +1,263 @@
%PDF-1.4
%<25><><EFBFBD><EFBFBD> ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 4 0 R /F4 6 0 R /F5 14 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
5 0 obj
<<
/Contents 20 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
7 0 obj
<<
/Contents 21 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
8 0 obj
<<
/Contents 22 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
9 0 obj
<<
/Contents 23 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
10 0 obj
<<
/Contents 24 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
11 0 obj
<<
/Contents 25 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
12 0 obj
<<
/Contents 26 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
13 0 obj
<<
/Contents 27 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
14 0 obj
<<
/BaseFont /Symbol /Name /F5 /Subtype /Type1 /Type /Font
>>
endobj
15 0 obj
<<
/Contents 28 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
16 0 obj
<<
/Contents 29 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
17 0 obj
<<
/PageMode /UseNone /Pages 19 0 R /Type /Catalog
>>
endobj
18 0 obj
<<
/Author (\(anonymous\)) /CreationDate (D:20260331161655+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260331161655+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False
>>
endobj
19 0 obj
<<
/Count 10 /Kids [ 5 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R 15 0 R 16 0 R ] /Type /Pages
>>
endobj
20 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 367
>>
stream
Gatn`h+kg@(^Api?H%8IB%6fuo1TfT-D3Zi"Vb!G7OJ!uDr5Hj&d[EPD:7mGDu2\33EVZ]>N[Npf&GtdK+$<jE((N.#^;T!K^Y(%;5oL0Lc(raAh';qPG8/@oRW0%@771RW(`RP"_]C?JSIJ;;r0Tj4S2k^Kn)"^6AX28GT:VJ1*g[j_8t5!04<[G0/;8>%,AsEO\++'M,`]<[8]68FL--H=*r_=R\_[+I.!I$ZfmDGnM<Y:W",D_cYB9?"!qC3p*!RT_;QVApQ6J@;!3brFWI0XO2\OkDjgsl4Nfqd\#Xn0-hfK><J6^do[eNrb70`"`@%oO:&;XI=LjK@n:e&Nj0NMlNj5YI9*clg!=IlmkuaGJ~>endstream
endobj
21 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1444
>>
stream
GauHKD/\E'&H88.0g2KN[)@4f%TSc@3HB<q:!s#9.p7`FZ]HKQbO$p$<L!O?Ymh1?i8U9%)@9I<XIj-sH.db!M'%n/7l,[Z4uCRk&.A>E+HGDG^C-V3_<1p+[^7'mG!*eI5A1q;A)"L(*ugMYq2Pi;*LM#n!i'>)(Wa]!^6<BLoOeG<!=:N;+9e^WmrZ-+g+_GGAeR.1kZ^"]8]9b[-$AEIU&SUpj%oU>U>jo3U,j`gh$/mH(mO2U$EeP)8&k4jC>^T%GZ985fg[m+a3S^p*!g[G1E\1O,)-glY(Q$Te5A,hBHC$q)2,E`Nu)4FBVJQpa:X5R)cl`Aa:am2kn/#LPfN8,bc+JjZtM^C0rIC=(bD*9A'S$UI_5$#(3d9$_Nuff^d9a^@Q7q]hKWXEn6q<Q05LX/2arm,bsjWkLNVWg"h[n%70Tbu!i/R!a]7<jEW;HQ(lK9&^m)Z+YiH(md58Nk(NHNqFdLs4cS\6O@53)>_WOV1<O\=Kn-F^rj$V`D[Vq>B$0HN/0W:FJ7AR4E3('qeP,WU2QB&XIijlk8XH,,T@Y+%q^7XR^F-A>Op`s"diJ26c92qTt\QeR,$$6C/@nLa'@*fNI@5H,N%!j2;*JPuDQ[h$n.e#k]W0a2_Cjd%k/iODQ=ICcBkE@B7Ik5'R&'$NDC''8Q<<CRUWut_ZR2a^@#Vh0="$U]&EA;80dudKSPcjC^b9660Ec[(?(fW)r^Gn-b;^!$K/WpBd9o0!l>(L+/QF3'Pj'.4'<J"r`%H\s-\P@_OrM=%7+_ObaVnSsimL1:/DLM.:&8VM)&rG%U%noShR0WKjk_`g_<U7q@Ufh6h]YYT@jbH4N3Zc07cVO?(@dA7C+m)iO[*6DF/L:7Y>]Y>*WP&pbRT>">J'%MNj0qWr[qm:."+.5Y[c\G2D^"MeU-og(SQ5bp:!\7Q+cW^,7N[C&T':Yl+6"%?<V;b64i">fAUUYG?\#jb^5](gD>_Yu\qdtU[q8`73\*AhE9c=2S`L*28R-b[JSPu)Pp[k)ELMTXJeKS6WL'M;a2+I&fXQ6<nX9PtNL9n-)>EGdbbe8nXLE20M/Zd:^lhVuO"Ur%(F*Nc<An<IJE)m38#)HRW+=.o\f=W$m.IMR9VN<m^6/1hM0:WBiWKI#Mgd3$)Y\2e^k&Qf<t%TTAs@-s<.)V*]ns]2^XJ,n1JM(c[b?5D'25-[6.Hs3q67DLoMB,81N]$&cWVu.;iZN5]*a_Ik!Gp[RK%EP;eR2,#8g)gK6L+cnZF:YXhB!4'cReZ75o-&*AON7mB\tB9=BVUJ!1/Xfi3TVobr2eZV;VqICLpQ!O'.kjHsWcYbHDZ7e5qiO_p3hs/72)Hjjddm*]Z%7>0?+Y[hI<GQ.$f>RjN?T2'<NK?N;=j'Ln\E7OoinbR+Ck83;?^N]\1Z1n7NS_VQ8S+bn&/1'645K=L.!<~>endstream
endobj
22 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 581
>>
stream
GasalIr!dm&H1LYi`[0YUJkVpp/8oMP2a;[aKes"%1GPmPh)=@V398DP^N4rm3HNeBB^:,+ME-rlcFc#IU9^b"i-3epO`^B%t"C!,7O>Q,4=5Ih?HOhTe'^tKHVWBUD3Zs(DAA:8f,?Jq-tVUE-aBH?TZ?=FnK^I+n,PC0^.+3r$m4):[D9=6(THAdXcbld]RZuWD^Jd)-`p`H+8>+l\c8BT>4eTd[1KFEA_"?dT2T&*@eDlmg2BnO!tC$*f9Lno5X5m'SX2M4-?^c;bNUZd_Hd=^3t3gomsZQ'+7@d2Su82OpcX.?d'Ed@FH03nG4R$88h^mpGj;8hVFi`^9e4rn48UHZ:[[T!'f*:DB7_lrbE8LTm]&qJR$XGSnP8&CSCR&\OOfNddh52263R@$R9$'[JFa9-J%PFfU>4ddp3U4Q@$C(VY7)n.W)H\WKtED@?\Z1T#oF?=;G'TE;k<UA_.cI^2%B;;Yd4;=KKZP;T"5:hDjll2/d/H38=1L(d)dKk.9%lQ,XA<hN;gMNR&s#iTT=P/=oT@_a,9eVI)i+LN5B=2AgobcTXeTc^q*TE;fmqBI_^~>endstream
endobj
23 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1164
>>
stream
GatU2qetI`&H4hB`>n.F:?`Z]![LXsP)*)8VRc><UfMVl2J+\Xpm'Ym8Het[2iA-R_9cG*/M,PFq<$FWU-fkAIJ%bNJ4gigCE7L!lj;kkZ,"))JUR04V!!o<780alFeF(rOqZ"q;%J^b;;YdG@#(\_FL>ldFAnkFULSDJ'-LnjD4VY2://-HHdM].+--Nt9K@_ITV'@U>gX[=T^H:tm*B(S<uh4XViF:AUr9%A3p'b6OJ)r^0%O?7bYZg3<d>SiLa-:``a=;'Vj%KZi.Ju:'@+O\$'8'iJdLD2S;\Om"<rTP3/)>lr@#Kl&q\d-fGNUIOuEs@R&&?A'^tm>Zb&DN3nj&9)V5d#c[sLP2`E$\r&6G'oOMh7q`AmbbX3j+)rFLfCH[Vo22tpSg4EYQa*sDo>f%IccDY8@`mM\*J^5MH9:53.KZ:2>:$;Zo9G<":cI_t$h-9Rs5;A;=O^Q3P?PA`DXhqH]M/Z6(.glO'$AlHtOJmD`>aV=L.od,gY;jchjC`%3VZlZ$)8HtpLt>n`I,L_cO!kW9F2#pC=?<SU$AaUL"UqF"HH'Z"1t5-BlsfAp(<sqfi%KG!GCOo0iG(3^^[#8#LP+"QVjhnN?a&%qrHLF#IIMH?mX/XpUE;_s9UtK1<XUFO*s>>;q%*EH;]]dJ.T\!ObW.4uD6K;I`H!gt*jXV+Cp1,u_;lV5GD_\?-kQnCJS$`0R0j@-r4GB*/NV.>SC?1%i"!eGc%uD=rts``A[0<0EY,L9k2%%qOIargcpQF;'@N?:AS#KnP<Mp[-l,-h1oSp7a!tHX:.7-<[I&kf6Hcs;OOYSi-bbA0X/6[+4a5'D)>qt1<![qu2p/^jUsaH'fag<C*a;>%bXWA!Y6B4?3!?)(`6;F2Ogk+UW"8J8Sj-@?Z\QJ5IHRt6XO[*TnB(76d[TP`F0@ri[aj1q1sA^A,Ohg6K-lGP4Kc6j0\/3Y[^V5-6SE\sH%tNpkMQatQdt&t^GC*)S>`&E>2VVYla6KP.ji>U"1pBNp5P%__rfUq.!MT3!MR:Qj1a\H8E'[pp'fTg2c(d]62fGTj0@l&AL(7d::$Wh)V[m`"B=h0ZeD"]1PrDPg[DE'P1s'5m)>ODaCA:bq`<lWlu1@A9,1r>=2^@6b1YG1bXMDse&hs>8hS!.(QeA!d/~>endstream
endobj
24 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1414
>>
stream
Gb!SjD/Z1=&H88.0h(Au;\^uE[Zd5PlfI6(8njP;Xt$jILBY\b"r;)Z@],OlHY4-^S:GZ@l0_1\al[/@Sp=pkFr]_ojlg&9!B?R#rQtYD#Lb[mY3!Om$8/4]T+7BnADafkma)ukkigY;\G\VUd:]L=m#Bd$=T,"4s0r<bEF1:/i;qVRG2,b)EmY!P8@piZ%QYDmQWMNqJK(,S5jfkeVZFZ9f,MPu(sh.b7=GNkIpi,*K(BpeD3.58MnHY/;S.H@WWc^ifr6_#93O/aQ=Y4\KtPLnN.kreI._c<NpG0p^:tFC:IW-r`8I'jG0ne!:Ye*<(_ATb^kIrYSd+\ma:\>068%.rSi]f4V*`$;I"=uND36Q6j`i-#AWGbT9>^tr,#,lJac7UlcF5R+<sfi6Rb#R19j1(%[4]LbZoPaZ67/IlmnpZ]"JXHj8A8n4?:Fd0f4n"*O&B-o='@Ap3@ePFo7Hk^/)4#(Ia;VsfcdG4ZTu]0GqS&/9NW#/#W^%#Fn[ggC/hO"@E]5=[:s2'c^1U:[,UcPiK^t\juJ]Z<rJl-J#-bIZ68/#f"<L$T5)YSS7B1:.$HH49U+P49sVo"ahCRq:UH`s=>1f7=>EZQ=T6*aQuu5B?coSFO_Xa`?5YbVam$.EW.e8!:^LBkl"i[dl]Hm(7uF#Qpf;"ME>u*BOOo"LUoJYGmug,#1g$i@$S1,#&]p:A;cNg6g5JLB'sFpAlL"rcrRT[<!U7Iu)*VX%R?mBZpX=_;-=].`#jb<8Vo%-Le]Nbs5uMlrL(QAuc0k/Q=t,qQ63YLuE!iIND&bt<e6Ifa?Whso@qj5H3/3*gbb^^1OK;,b/qqLh)Q+$]D.2Di)N_>4c/'q)H-2uJV(pZmPk#9<;(dRfqdY!3lnP@U-)CIIn<-=;f"3fLfl`KkbX_9@FSDH.@b^o_9>o&lGh9)7Vu@_#+S)=e+i,9N&f$+1SEff'[^Ft)Ce$T<,fkb"bj'4)i1@mk:=f8U6XcQ><#nZ`<);U&WYu_fYTsl*8k\Ko%\&1AHo86tR6C&<\uB19Jai^BK1b*$&IAl]IXsKU7HV;fYCA6U+dXX/rGlqZ9jI9o^">hpe`smib=TXUOG@j2=V!GuKlaGZOq,>ai`pC4"+hq6?Au^2]ff>$`o"`&Kr.@kS&9-Q9[7Yjd6Xp%g;X2PoH(tC,rd*bb3MVkRkU!9k+MUg/l\m'#OfJg_geH.Z7H)Q^t<?.`d[8a]8V27c`gEuB`GNtG%c-l#0,=K!JrYh\fUOZ:F?lO7AlErAo)+Xm@[d$C2ZK^@R0"ib2_DCN:h"oTL!QqFoOp0XV4]H6tJm8#_g:_c^[s$eQ8$(I4SsS%kU5ZH^`I#lTr!sbj@JmJ(/W7%cU6-:^_Z16!R@["`sH(j6Ib7d2_Rts-I6*AdJtH"6fRN[/~>endstream
endobj
25 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1156
>>
stream
Gb!#Z;/b2I&:XAW3!`XF%$P3^E]0`%$`$FH!a1X3#5U%BJWe:k=,Bm*.X#G!^[F]rYnP5npFou3CeN9++7Hp**YlJH5(WZl$\58]p`BPC`irQ2&:,KNDW#gqpc8]SQKOrc*aXL90V1tN8U,\Z\A8]b$<L+?9P[=>R::f22-9$m(gCZ-Jk8Ll"g&BR06W2W=^]=/;CW,7>^V*fOp*fH2qlUBhKaGPMn`Zi,FIp22qBb#'ruBbX'3H19*$t,j^L5TT#%kC*s.F(MFtc.=>6?H";U3;F7gp)TW[cj1QBSOj"/O5p7Y)Z<GSQH$bY<QHmcA<Wa[D\k\n`=G9V+M&h[(?]P4O@,?HG"YTKrWX\gt,GP9a(dUu_5-&YlZA/d$EhY[8iQ'1G?ebbl3d*L&l(c!G$*3J]u_Q$*<_fCR"E/-!qh?U'9lNb=I#.+,`[$ZDNWR)L[g*F#jW>PpQQ:%e>:>C2c]u5?)*^&9\.O>hrN5'/J%k;h*esTV0<.#CP\^&o>08G3pC8WWff_$i3=!i+.DG'Dd7s<J<f)oGif9Lq#8uO(`?<L>I"lc"Bg,[ko$I*ZW_6HVGjg",`;5(.X+LaFKdtBqNS5[4q=Fi8@mB5_:LOsBu[9FnQCg>C]6Kqf9o+'J0AAL!OVVUU!2`[t8cu(+MieS$<UW@#8U6td[E.Z4('p.FhNXDq%gS:e'*A:9T/4TB]%8V5u`c_-W[E)L(q(ZfPbP2It!32d`^VGNSKo$pkk-gmUI>dt,2]$&-5sLZ'CQ$>d#F<t$gY07[7JWOl950!XVV*kEa.`l91R4^./7s1Y/V9=aS&*Mn6UWe*/V<7[,a^Pj#%)/K0EM1giVZcBq_*'ibi2=",rtkk`GN*LPJ6G&.>\SirVrs><,52u>n^^ZpO`PSY?7$!1>D6CNRp>]H5i\u7%q1.UJ[58FiH;m+SOYZ-6H&i[Y@#1@Ctg$Ui"0FUH9@,ZL?"Nh1Kq0>iFR&ofUQO95rT#i1+t<l8PE/kRbIB&cL%aqmHlNhS?K\NkWkXpf)_9T<`ML+f9;ePr!=bG#Q/GpNuZehNmG;Gb8LR.A?eXa/$4o#?1LHPYoo%3PIM=lI+qhe#09)kqT/8oYuR0r!lY3Bna\d<^V"%\?c")V[/@0Os^S"V==3[%/p9pBBc;~>endstream
endobj
26 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1392
>>
stream
Gatm:D/\E'&H88.0sXba;Oq\&&kG!T<N9h7.@900mSd%aCO7e]R9b#\9gFO7hUsae3uF.Sc:B(joCC,n4O:)6*W10E4.ZIXNPYSD"4)bm"ME(f@=/M+a!^nWGT4gKFFp_9G+j*t39O]]TH>dJIb4%BkMQIZS`%p$k;(BpM"Cg[HJ:7tN+\Np#;<#RkQIe<T,!knVPJR`;dQK);CTm2,o:2JIY@6P)T6:dZiE,TH!;CUg^6+$@RcTVaXn+cUgqW<0Y@njSVmhH#T!]$Dh,JN-&">)&B_WJbbl>1'+.bO$VD,H0+/C)8Zr8HP#V?gXC2PO_.R0Sm,hI3b)MB$<CYPprFhS`%SNX-lc\KY\W"b=Pc0PISWe&h$fa2q=fb\u^/GQ?%qi-#joBd<cD1M=AJjj@C"!SEQHtZ?p.1a0]VgE/3NXE4"<s0`#(pYMH3$](jG@EZPR].me<*nVr[:u+kbkGu.7uUK343#WL^eNXT\Ab/c+<^P:fRjW+SPpH,."8a)&=6B*Nn\XXg(*HPW*,BLR,djZfMV>D^riVmWLWFj`;pJ]6BU4N]#P6U7X*,b'BFR[ZA8,5W&H)-*8-9BHp!uF"Vd4k!f/nE;mNL*=7*\jq/2DUTX(YSe8W'\NqTZA+#(Pf,=HhB2aijgW7u_nTubJY;'/uS$lFA,&Y)bL6ArC\RN[Hd+-D^UiMHf'n$SDE/:>@8;JJg8pn#6XLH$7>YM-n,29Ka#\d,FlI:'a-Y8l:`'m"H*C4+h0ZiP\*_WK<ajQ3JHD6WiVW^_-0uCrtD5YLk*:";#A;K$u,N'c$$"qD@Gb/$4cR!W*QKS(ZeSYU9-<UGQ5upJW.7(lWnE=Lr[d^3'X_8t@*+<V,$c64M'l.>83cmisHp)GVdi2aTRaXFf0K?@u:"fOf[FS+dV(Z?Ua,-'_$<f3$*Fpk37_c9%-]ah__*!%LPXTA^.(,h_Udhf;];I*$!>Go'J7<5>HDhsDA:s+!]Vj*9Ni&4DJr7%R3ki5lp,7&u:\WZLqNgU[P6#SdNo(.i#=jDjiGT1>FZ,>UE-&>'0%<5hH`/JlKkU$rpA0='<]L'X1a+tDbJOu[/IRF;pk^Jp`NX@XC/ala-m%sVlK@V3SFP`a9ad#OEh;E8h]&IYRe)?rdYXf:MdC+HD<C1ZdMJ[R'(T"s\L1(FIN)f.V^n,JOc_&iIB[PB#bj&[-/)Mj;;i`kQ0cD\<fZoIB)Mo!HIpXjRkfc,RZE/piVMm!lZ98P1u"NcIs2Yqg_#6A,+W3oitskO1)=#dV^/G#UJ(bCi/tm0bdkeC17$rMPud:8GOSess,Wad]lS#48a(hlH0o3ma18c>*O#2hSn<8")?K.fgej5RQ7iKTfB-YG*:nTr'+4&&r<?ZPJ<PH`.e;!i~>endstream
endobj
27 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 317
>>
stream
Garo=;+ne\'SYH9.h4pTo:^$P=<)3B#l:?,@NP9JgG\@IBo6UE8EkU_X`Gn[2fKpqF7+6>3$NkId'F(hQO"Znp_7`V/cl(LKEoWU)*^)j&]>k*$'Nsm\/')NF6mpYe[Dn$3N)E_=^ocd[fu@:J"k`-."\N_6f;QA9RFPl^8u>semLY45e]NjIB.e@GHTB@A;\@IkgA4C@5+O#reX<Idk%IrMfs0h(923+/"%a:P:\66Wl%,NY"ejf-0%VG,rI10FQWZ,A4cZ_/AhE=D71s;DO'J3Y8R%\T#Tr%s2_tOH?+AF+/eq\G[Xue01B_i~>endstream
endobj
28 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 757
>>
stream
Gatn#h/8]I&;BTO'M"98ftANj+1LNhj),c7A37#u'a_u+Y_d'JU^P[ET>%p1*'(!f\nXN5L=r!dc#O"/6ZMDV2r+IH'W:+TF9D.>\trQ&!=8(@lfjH\9hR8oa)^*a,L*Yj;rie>AiPN\g!EE2BT<L7,?#ZA[lmSehij`Q+^mY1P_:CfqI-d#^qR,V&PH?"2\1`A@1^D1!U*V3\!#]JS;0(oI?(!.^Jm'O<RKGqW>_=B&E1>Qnhau:=F!j8P[V2,O<u9Z9)MC%<$dtj6^BRNF](")@4Ru22587oH&8K5<tV^5?=-b?c_-q,cgBn9Z!f_TOV].?'ZopCin=tRgTK\LQL+?RgbGG\Ds!RH[Pg_+Y\]p#jP^"C(0uFF+q*6KfJoNL"<F*<7.+#OK1sfi3*RRL?V#^'9GX<tYMS``jjfuVA5!<lZbW2<iC)e1C0%RG=X?LVK%[L]\BF/)*9XY)e-j?N@M/Fb:Vm5DDco1^U3L"qN/Q\tG["aCVhsX4'cDK>OcX&:dgJi%aQ0\SP.=bcB5p-W^lL1?;+qclE0Pfu0.at@e[B#d(#?k,UWhQ%pIt,[;U-Q^lM=pb;`Wg`HT""2(g?@rNA*2\(*#Q4qU:+dh+'^j@4=Z3(fZFtCYo$CUUFh&8cR%3`k(KQB,T`gQ+.KkfGX-J/oK]uB9-Q:$b#*n$c9D%,X>\mZD+qT(g>n%)L$%$Z5qC@\Pl=qR>um)]`.sXBV[IdgTtcZ*-;[T4IsTCiBco[JQ%$d~>endstream
endobj
29 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 735
>>
stream
GatUq?#SFN'Sc)P($Dae<12D7E8YW!g9+>_Yr1i"`\\QR#VJlB`<ipOISKs#TlpeT92Uppm.^&?CR]oc)f=H\']:HmrXB%R+IW?(L^2<j>XuKb4hAp\4lUd=(B=sN`:mNgp9S2E!ldAFL`8DCJ,pOto`IY2ZX_E(R4,HU$u"9aKXJVkLJ"!M(5iQVSfi>\.t4bj2-'&W/)\sXY:"RBS'W16DR%u?jpfld(:fUWGmWO&HSe!Yn\q^7,9,tl3)+X)OL9\Ke4q70)F6u'<9T`<"h@07-=G%7#Ee3J]`,+=DuV%2c#X*u8rpoH[*G="=:e/[<1!!]N7T0#!_%t902ZnCE#c_^rq:XA+kn=`7`bp`'FQIbe/;jPO\_'SV>edT"Q#9(NuA\a9RlU7<@)-WBVeW8JW7<n;G=;&Kbk_#nL]eR!8N!K\GdCtbb3i;%<p0RRl0L3_SRN`)P<PU>*Fb2$*i(#&:QI%q0<0*LUg0bp1jm;He,FJp#!-5+,8O';n)Fbc73>5[*D<YXb3Y&Y4fE46Z>Sa`ftPSeN-P5TAN`3]\V*K<A#W8+N2.@-X%3=1_fm-J$2*.K6:^CA8"Fo\<f?gQ86Is*8\)o7YbaSm$)7EKJ=uo.oN%VZE9IU8TpGKQfWT6SLM8;^uBCOq;<@WZ7"`\iDalX$=$2._,@<:ZPZ!%l/414i9h^-;Qoe')X^lNQ[]n@''rUV[D<]$V-d-Ep5a+Tr;gEmk@a~>endstream
endobj
xref
0 30
0000000000 65535 f
0000000061 00000 n
0000000133 00000 n
0000000240 00000 n
0000000352 00000 n
0000000467 00000 n
0000000672 00000 n
0000000777 00000 n
0000000982 00000 n
0000001187 00000 n
0000001392 00000 n
0000001598 00000 n
0000001804 00000 n
0000002010 00000 n
0000002216 00000 n
0000002294 00000 n
0000002500 00000 n
0000002706 00000 n
0000002776 00000 n
0000003057 00000 n
0000003178 00000 n
0000003636 00000 n
0000005172 00000 n
0000005844 00000 n
0000007100 00000 n
0000008606 00000 n
0000009854 00000 n
0000011338 00000 n
0000011746 00000 n
0000012594 00000 n
trailer
<<
/ID
[<c2b0ca93638236c118c39bf1e4748e63><c2b0ca93638236c118c39bf1e4748e63>]
% ReportLab generated PDF document -- digest (opensource)
/Info 18 0 R
/Root 17 0 R
/Size 30
>>
startxref
13420
%%EOF

622
Les09-Live-Coding-Guide.md Normal file
View File

@@ -0,0 +1,622 @@
# 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

270
Les09-Slide-Overzicht.md Normal file
View File

@@ -0,0 +1,270 @@
# Les 9 — Supabase Auth
## Slide Overzicht
---
## Slide 1: Title
### Les 9 — Supabase Auth
**Visual:** Large centered title with QuickPoll icon
- Background: CREAM
- "Les 9" in BLUE
- "Supabase Auth" in BLACK
- Subtitle: "signUp, signIn, signOut, Navbar, RLS"
---
## Slide 2: Terugblik (Recap)
### Waar staan we?
**Content:**
- Supabase project aangemaakt en gekoppeld
- /create pagina gebouwd
- Server Component + VoteForm patroon
- Polls werkend in database
- Real-time votes
- "Nu: beveiligde login"
**Visual:**
- Left: screenshot van huidige app
- Right: checkmarks of badges
---
## Slide 3: Planning
### Vandaag — 120 minuten
| Tijd | Onderwerp | Duur |
|------|-----------|------|
| 09:0009:10 | Welkom + Terugblik | 10 min |
| 09:1010:00 | Uitleg Auth | 50 min |
| 10:0010:15 | Samen Middleware bouwen | 15 min |
| 10:1510:30 | **Pauze** | 15 min |
| 10:3011:30 | Zelf Doen (signup, login, Navbar) | 60 min |
| 11:3011:45 | Vragen & Debugging | 15 min |
| 11:4512:00 | Huiswerk + Afsluiting | 15 min |
**Visual:** Timeline with YELLOW background, icons per blok
---
## Slide 4: Wat is Auth?
### Authenticatie vs Autorisatie
**Authenticatie (WHO):**
- Wie ben jij?
- Email + password
- Supabase verifies en geeft JWT token
- User object: email, id, created_at
**Autorisatie (WHAT):**
- Wat mag je doen?
- Wie mag polls maken?
- Later: RLS policies
**Features van Supabase Auth:**
- Email/password signup & signin
- Session management (cookies)
- JWT tokens
- Password reset
- Multi-factor auth (later)
- OAuth (Google, GitHub, etc.)
**Visual:**
- Left: "Authentication" icon (person + key)
- Right: "Authorization" icon (person + checkmark)
- Supabase logo
---
## Slide 5: Auth Functies
### Vier Core Operations
**signUp**
```typescript
const { error } = await supabase.auth.signUp({
email: "user@example.com",
password: "secure123"
});
```
→ Account aanmaken
**signInWithPassword**
```typescript
const { error } = await supabase.auth.signInWithPassword({
email: "user@example.com",
password: "secure123"
});
```
→ Inloggen
**signOut**
```typescript
await supabase.auth.signOut();
```
→ Uitloggen
**getUser**
```typescript
const { data: { user } } = await supabase.auth.getUser();
// user.email, user.id, user.email_confirmed_at
```
→ Huidige user
**Visual:** Code blocks in BLUE boxes, icons above each
---
## Slide 6: Server vs Browser Client
### Two Clients, One Auth
**Server Client** (@supabase/ssr)
```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) { /* ... */ },
},
}
);
}
```
**Use in:**
- Middleware (refresh token)
- Server Components (Navbar, getUser)
- API routes
**Browser Client** (@supabase/ssr)
```typescript
import { createBrowserClient } from "@supabase/ssr";
export function createSupabaseBrowserClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
}
```
**Use in:**
- Client Components ('use client')
- Login forms
- Logout buttons
**Visual:** Two side-by-side code blocks
- Left: Server (BLUE bg), lock icon
- Right: Browser (PINK bg), web icon
**Key difference:**
- Server: Cookies (secure, secure-only, httpOnly)
- Browser: localStorage (accessible, but less safe)
---
## Slide 7: Pauze
### Pauze!
**Visual:** Relaxed illustration, "15 minuten", clock
---
## Slide 8: Zelf Doen — Auth Bouwen
### Nu jij — 60 minuten
**To-Do:**
- [ ] app/signup/page.tsx (form)
- [ ] app/login/page.tsx (form)
- [ ] components/LogoutButton.tsx
- [ ] components/Navbar.tsx (Server Component + getUser)
- [ ] app/layout.tsx (add `<Navbar />`)
- [ ] Update RLS policies (authenticated only!)
**Reference code beschikbaar** (docent toont op beamer)
**Process:**
1. Start simpel: form met email + password inputs
2. Voeg supabase.auth.signUp / signInWithPassword toe
3. Test in browser
4. Navbar: toon email of login link
5. RLS: polls INSERT nur voor authenticated users
**Expected result:**
- Registreren → inloggen → poll maken → uitloggen
- Na logout: kan geen poll meer maken (RLS!)
**Visual:** Big BLUE background, "Bouw Auth" header, checklist
---
## Slide 9: Huiswerk
### Volgende Stap
**Verplicht (Les 10):**
1. Profiel pagina (app/profile/page.tsx)
- Toon user.email, user.id
- Later: password update form
2. Maker tonen bij poll
- Voeg `created_by` kolom toe polls tabel
- Toon "Gemaakt door: [email]" bij elke poll
- RLS: alleen maker mag aanpassen (UPDATE)
**Bonus (optioneel):**
1. Google OAuth signup
- Supabase dashboard → Auth → Providers → Google
- Voeg "Sign in with Google" knop toe
2. Password reset
- Email link naar reset form
- supabase.auth.resetPasswordForEmail()
**Visual:** Checklist, bonus items in PINK
---
## Slide 10: Afsluiting
### Volgende Les — Deployment
**Wat hebben we gedaan vandaag:**
- Auth concepten: authenticatie vs autorisatie
- Supabase Auth functies: signUp, signIn, signOut, getUser
- Server vs browser client
- Middleware voor session refresh
- Navbar met authenticated user
- RLS policies
**Volgende keer:**
- Vercel deployment
- Google OAuth
- Profiel pagina
- Meer security!
**Vragen? Feedback?**
**Visual:** Vercel logo, rocket icon, "Deployment!" in YELLOW
---
## Slide Summary
| # | Title | Duration | Key Content |
|---|-------|----------|-------------|
| 1 | Title | Opening | Les 9 — Supabase Auth |
| 2 | Recap | 09:10 | Where we are |
| 3 | Plan | 09:05 | 120-min schedule |
| 4 | Auth Concepts | 09:10 | Auth vs AuthN, Supabase features |
| 5 | Functions | 09:20 | signUp, signIn, signOut, getUser |
| 6 | Clients | 09:30 | Server vs Browser (@supabase/ssr) |
| 7 | Break | 10:15 | 15 min pauze |
| 8 | Build | 10:30 | Students implement auth |
| 9 | Homework | 11:45 | Profile, maker, Google OAuth |
| 10 | Closing | 11:55 | Next: Deployment |

BIN
Les09-Slides.pptx Normal file

Binary file not shown.