fix: add les 9
This commit is contained in:
622
Les09-Live-Coding-Guide.md
Normal file
622
Les09-Live-Coding-Guide.md
Normal 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:10–10: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:00–10: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:15–10:30)
|
||||
|
||||
---
|
||||
|
||||
## DEEL 2: ZELF DOEN (10:30–11: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:30–11: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:30–11: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:45–12: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
|
||||
Reference in New Issue
Block a user