fix: update lessons
This commit is contained in:
542
Les18-Supabase-Auth-RLS/Les18-Lesstof.md
Normal file
542
Les18-Supabase-Auth-RLS/Les18-Lesstof.md
Normal file
@@ -0,0 +1,542 @@
|
||||
# Les 18 — Lesstof
|
||||
## Supabase Auth + RLS — multi-user apps in productie
|
||||
|
||||
**Vak:** AI-Assisted Development
|
||||
**Vorige les:** Les 17 — Externe APIs in diepte
|
||||
**Volgende les:** Geen — eindopdracht thuis
|
||||
|
||||
---
|
||||
|
||||
## Inhoud
|
||||
|
||||
1. [Auth vs Authz](#1-auth-vs-authz)
|
||||
2. [Supabase Auth — drie methoden](#2-supabase-auth--drie-methoden)
|
||||
3. [Magic link flow](#3-magic-link-flow)
|
||||
4. [Social login (OAuth)](#4-social-login-oauth)
|
||||
5. [Session in Next.js App Router](#5-session-in-nextjs-app-router)
|
||||
6. [Middleware — sessie refresh](#6-middleware--sessie-refresh)
|
||||
7. [RLS — wat en waarom](#7-rls--wat-en-waarom)
|
||||
8. [RLS policies in praktijk](#8-rls-policies-in-praktijk)
|
||||
9. [Protected routes + server checks](#9-protected-routes--server-checks)
|
||||
10. [Eindopdracht checklist](#10-eindopdracht-checklist)
|
||||
|
||||
---
|
||||
|
||||
## 1. Auth vs Authz
|
||||
|
||||
Twee verschillende vragen — vaak verward.
|
||||
|
||||
- **Authentication (Auth)** — "wie ben jij?" — login flow
|
||||
- **Authorization (Authz)** — "wat mag jij?" — permissions
|
||||
|
||||
Voorbeeld: jij bent ingelogd als `tim@example.com` (auth). Dat betekent niet dat je een admin task kan deleten (authz). Authz checkt: heeft deze user de juiste rol/permissie voor deze actie?
|
||||
|
||||
In Supabase:
|
||||
- **Auth** wordt geregeld door Supabase Auth — magic link, social, password
|
||||
- **Authz** wordt geregeld door RLS — Row Level Security policies in Postgres
|
||||
|
||||
Geïntegreerd: Supabase Auth zet `auth.uid()` beschikbaar in Postgres, RLS policies gebruiken dat voor filtering.
|
||||
|
||||
---
|
||||
|
||||
## 2. Supabase Auth — drie methoden
|
||||
|
||||
### 2.1 Email + password
|
||||
|
||||
Klassiek. User registreert met email + wachtwoord.
|
||||
|
||||
```typescript
|
||||
const supabase = createClient(...);
|
||||
|
||||
// Signup
|
||||
await supabase.auth.signUp({
|
||||
email: "user@example.com",
|
||||
password: "geheim123",
|
||||
});
|
||||
|
||||
// Login
|
||||
await supabase.auth.signInWithPassword({
|
||||
email: "user@example.com",
|
||||
password: "geheim123",
|
||||
});
|
||||
|
||||
// Logout
|
||||
await supabase.auth.signOut();
|
||||
```
|
||||
|
||||
Voordelen: bekend, werkt offline (na login). Nadelen: wachtwoord-management (reset, lekken, weak passwords).
|
||||
|
||||
### 2.2 Magic link (passwordless)
|
||||
|
||||
User vult email in, krijgt link, klikt → ingelogd.
|
||||
|
||||
```typescript
|
||||
await supabase.auth.signInWithOtp({
|
||||
email: "user@example.com",
|
||||
options: {
|
||||
emailRedirectTo: `${location.origin}/auth/callback`,
|
||||
},
|
||||
});
|
||||
// Email is verstuurd. User klikt link → redirect naar callback → sessie actief.
|
||||
```
|
||||
|
||||
Voordelen: geen wachtwoord, secure (link bevat one-time token), modern. Nadelen: vereist email-access (geen offline login).
|
||||
|
||||
**Aanrader voor demo + meeste apps.**
|
||||
|
||||
### 2.3 Social login (OAuth)
|
||||
|
||||
"Login met Google", "Login met GitHub".
|
||||
|
||||
```typescript
|
||||
await supabase.auth.signInWithOAuth({
|
||||
provider: "github", // of "google", "discord", "apple"...
|
||||
options: {
|
||||
redirectTo: `${location.origin}/auth/callback`,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Voordelen: geen account-creation, vertrouwde providers. Nadelen: vereist OAuth-setup bij provider.
|
||||
|
||||
Supabase ondersteunt 20+ providers: Google, GitHub, Discord, Twitter, Apple, Microsoft, Spotify, etc.
|
||||
|
||||
---
|
||||
|
||||
## 3. Magic link flow
|
||||
|
||||
```
|
||||
1. User opent /login
|
||||
2. Vult email in, klikt Submit
|
||||
3. Server: supabase.auth.signInWithOtp({ email })
|
||||
4. Supabase verstuurt email met magic link:
|
||||
https://your-project.supabase.co/auth/v1/verify?token=...&redirect_to=...
|
||||
5. User klikt link in email
|
||||
6. Supabase verifies token, redirect naar je app /auth/callback?code=...
|
||||
7. Callback route: exchange code voor sessie:
|
||||
await supabase.auth.exchangeCodeForSession(code)
|
||||
8. Cookie gezet, user is ingelogd
|
||||
```
|
||||
|
||||
### Callback route in Next.js
|
||||
|
||||
`app/auth/callback/route.ts`:
|
||||
```typescript
|
||||
import { createClient } from "@/utils/supabase/server";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams, origin } = new URL(request.url);
|
||||
const code = searchParams.get("code");
|
||||
|
||||
if (code) {
|
||||
const supabase = await createClient();
|
||||
await supabase.auth.exchangeCodeForSession(code);
|
||||
}
|
||||
|
||||
return NextResponse.redirect(`${origin}/`);
|
||||
}
|
||||
```
|
||||
|
||||
### Custom email template
|
||||
|
||||
Supabase Dashboard → Auth → Email Templates → "Magic Link". Pas onderwerp + body aan. Variabelen: `{{ .Token }}`, `{{ .ConfirmationURL }}`.
|
||||
|
||||
Belangrijk: voor productie eigen SMTP setup (Resend, Sendgrid). Supabase's default SMTP is rate-limited en sender-reputation onbekend.
|
||||
|
||||
---
|
||||
|
||||
## 4. Social login (OAuth)
|
||||
|
||||
### Setup GitHub
|
||||
|
||||
1. GitHub → Settings → Developer settings → OAuth Apps → New OAuth App
|
||||
2. **Application name:** "Mijn App"
|
||||
3. **Homepage URL:** `https://yourapp.vercel.app`
|
||||
4. **Authorization callback URL:** `https://your-project.supabase.co/auth/v1/callback`
|
||||
5. Save → kopieer Client ID + generate Client Secret
|
||||
|
||||
### Configure in Supabase
|
||||
|
||||
1. Supabase Dashboard → Auth → Providers → GitHub
|
||||
2. Enable
|
||||
3. Plak Client ID + Client Secret
|
||||
4. Save
|
||||
|
||||
### Frontend
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
import { createClient } from "@/utils/supabase/client";
|
||||
|
||||
async function signInWithGitHub() {
|
||||
const supabase = createClient();
|
||||
await supabase.auth.signInWithOAuth({
|
||||
provider: "github",
|
||||
options: { redirectTo: `${location.origin}/auth/callback` },
|
||||
});
|
||||
}
|
||||
|
||||
// Render button
|
||||
<button onClick={signInWithGitHub}>Login met GitHub</button>
|
||||
```
|
||||
|
||||
Klik → redirect naar GitHub → user authorize → terug naar je callback → ingelogd.
|
||||
|
||||
### Combineer providers
|
||||
|
||||
Same app kan magic link **én** GitHub login hebben. User kiest wat hij prefereert. Supabase merged users met dezelfde email automatisch.
|
||||
|
||||
---
|
||||
|
||||
## 5. Session in Next.js App Router
|
||||
|
||||
Supabase Auth in Next.js is iets complexer dan andere libraries. Drie redenen:
|
||||
|
||||
- Cookies moeten gesynced tussen browser + server-side rendering
|
||||
- Sessies verlopen — middleware refreshed automatisch
|
||||
- Server components hebben aparte client nodig dan client components
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
pnpm add @supabase/supabase-js @supabase/ssr
|
||||
```
|
||||
|
||||
### Vier files
|
||||
|
||||
**`utils/supabase/client.ts`** — voor Client Components:
|
||||
```typescript
|
||||
import { createBrowserClient } from "@supabase/ssr";
|
||||
|
||||
export function createClient() {
|
||||
return createBrowserClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**`utils/supabase/server.ts`** — voor Server Components / actions:
|
||||
```typescript
|
||||
import { createServerClient } from "@supabase/ssr";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
export async function createClient() {
|
||||
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 {}
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**`utils/supabase/middleware.ts`** + **`middleware.ts` (root)** — zie volgende sectie.
|
||||
|
||||
Supabase heeft een **template** voor deze exacte setup. Niet zelf uitvinden, kopieer van docs.
|
||||
|
||||
---
|
||||
|
||||
## 6. Middleware — sessie refresh
|
||||
|
||||
Sessies verlopen na een uur (default). Zonder refresh moet user steeds opnieuw inloggen.
|
||||
|
||||
`utils/supabase/middleware.ts`:
|
||||
```typescript
|
||||
import { createServerClient } from "@supabase/ssr";
|
||||
import { NextResponse, type NextRequest } from "next/server";
|
||||
|
||||
export async function updateSession(request: NextRequest) {
|
||||
let response = 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));
|
||||
response = NextResponse.next({ request });
|
||||
cookiesToSet.forEach(({ name, value, options }) =>
|
||||
response.cookies.set(name, value, options)
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await supabase.auth.getUser(); // refreshed sessie
|
||||
|
||||
return response;
|
||||
}
|
||||
```
|
||||
|
||||
`middleware.ts` (root):
|
||||
```typescript
|
||||
import { type NextRequest } from "next/server";
|
||||
import { updateSession } from "@/utils/supabase/middleware";
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
return await updateSession(request);
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
|
||||
};
|
||||
```
|
||||
|
||||
Elke request → middleware draait → sessie auto-refreshed → user blijft ingelogd.
|
||||
|
||||
---
|
||||
|
||||
## 7. RLS — wat en waarom
|
||||
|
||||
### Zonder RLS
|
||||
|
||||
```typescript
|
||||
// Client met anon key
|
||||
const { data } = await supabase.from("tasks").select("*");
|
||||
// → Returns ALL tasks from ALL users — disaster
|
||||
```
|
||||
|
||||
Met alleen anon key in browser kan iedereen alles. Niet acceptabel voor multi-user apps.
|
||||
|
||||
### Met RLS
|
||||
|
||||
```sql
|
||||
alter table tasks enable row level security;
|
||||
create policy "select own" on tasks for select using (auth.uid() = user_id);
|
||||
```
|
||||
|
||||
Now:
|
||||
```typescript
|
||||
const { data } = await supabase.from("tasks").select("*");
|
||||
// → Returns alleen tasks waar user_id = auth.uid()
|
||||
```
|
||||
|
||||
Postgres past de filter ALTIJD toe, ongeacht wie de query stelt. Sterke garantie.
|
||||
|
||||
### `auth.uid()`
|
||||
|
||||
Supabase helper functie. Returns:
|
||||
- UUID van ingelogde user (uit JWT)
|
||||
- `NULL` als anonymous
|
||||
|
||||
Te gebruiken in:
|
||||
- `WHERE` clauses
|
||||
- `default` waarden bij INSERT
|
||||
- Custom functions
|
||||
|
||||
### Hoe Postgres weet wie ingelogd is
|
||||
|
||||
Supabase JS client stuurt JWT mee in `Authorization: Bearer <jwt>` header. Postgres parsed JWT, extract user-id, beschikbaar via `auth.uid()`.
|
||||
|
||||
Magic onder de motorkap, voor jou: werkt gewoon.
|
||||
|
||||
---
|
||||
|
||||
## 8. RLS policies in praktijk
|
||||
|
||||
### Basis: per-user data
|
||||
|
||||
```sql
|
||||
create table tasks (
|
||||
id bigserial primary key,
|
||||
user_id uuid not null references auth.users(id) default auth.uid(),
|
||||
text text not null,
|
||||
done boolean default false
|
||||
);
|
||||
|
||||
alter table tasks enable row level security;
|
||||
|
||||
-- Read
|
||||
create policy "select own tasks" on tasks
|
||||
for select using (auth.uid() = user_id);
|
||||
|
||||
-- Create
|
||||
create policy "insert own tasks" on tasks
|
||||
for insert with check (auth.uid() = user_id);
|
||||
|
||||
-- Update
|
||||
create policy "update own tasks" on tasks
|
||||
for update using (auth.uid() = user_id);
|
||||
|
||||
-- Delete
|
||||
create policy "delete own tasks" on tasks
|
||||
for delete using (auth.uid() = user_id);
|
||||
```
|
||||
|
||||
`using` = check tijdens read (welke rows mag user zien?)
|
||||
`with check` = check tijdens write (welke rows mag user maken?)
|
||||
|
||||
### Variant: shared data per team
|
||||
|
||||
Voor team-features waar users elkaars data zien:
|
||||
|
||||
```sql
|
||||
create table team_members (
|
||||
team_id uuid,
|
||||
user_id uuid,
|
||||
primary key (team_id, user_id)
|
||||
);
|
||||
|
||||
create policy "see team tasks" on tasks
|
||||
for select using (
|
||||
team_id in (
|
||||
select team_id from team_members where user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
### Variant: admin role
|
||||
|
||||
Voor admins die alles mogen zien:
|
||||
|
||||
```sql
|
||||
create policy "admins see all" on tasks
|
||||
for select using (
|
||||
auth.jwt() ->> 'role' = 'admin'
|
||||
OR auth.uid() = user_id
|
||||
);
|
||||
```
|
||||
|
||||
Set custom JWT claims via Supabase Dashboard of code.
|
||||
|
||||
### Best practices
|
||||
|
||||
- **Begin met `enable row level security`** — anders heeft policies geen effect
|
||||
- **Default-deny:** zonder policies = niemand mag iets
|
||||
- **Per-operation policies:** select/insert/update/delete apart
|
||||
- **Test in incognito** — eerlijke check zonder admin-context
|
||||
- **Hou queries simpel** — complexe policies = trage queries
|
||||
|
||||
---
|
||||
|
||||
## 9. Protected routes + server checks
|
||||
|
||||
### Protected page (Server Component)
|
||||
|
||||
```tsx
|
||||
import { createClient } from "@/utils/supabase/server";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function Dashboard() {
|
||||
const supabase = await createClient();
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
|
||||
if (!user) redirect("/login");
|
||||
|
||||
return <div>Welkom {user.email}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### Protected layout
|
||||
|
||||
`app/(authenticated)/layout.tsx`:
|
||||
```tsx
|
||||
export default async function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||
const supabase = await createClient();
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) redirect("/login");
|
||||
return <>{children}</>;
|
||||
}
|
||||
```
|
||||
|
||||
Alle pages onder `(authenticated)` zijn nu beschermd.
|
||||
|
||||
### Protected API route
|
||||
|
||||
```typescript
|
||||
import { createClient } from "@/utils/supabase/server";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const supabase = await createClient();
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) return new Response("Unauthorized", { status: 401 });
|
||||
|
||||
// user is ingelogd — handle request
|
||||
}
|
||||
```
|
||||
|
||||
### Logout
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
import { createClient } from "@/utils/supabase/client";
|
||||
|
||||
async function logout() {
|
||||
const supabase = createClient();
|
||||
await supabase.auth.signOut();
|
||||
window.location.href = "/login";
|
||||
}
|
||||
|
||||
<button onClick={logout}>Uitloggen</button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Eindopdracht checklist
|
||||
|
||||
Wat je voor je eindopdracht moet hebben:
|
||||
|
||||
### Technische stack
|
||||
|
||||
- ✓ Next.js 16 + TypeScript + Tailwind
|
||||
- ✓ Supabase Postgres
|
||||
- ✓ Vercel AI SDK met Tool Calling
|
||||
- ✓ Minstens 1 externe API
|
||||
- ✓ Multi-user — Supabase Auth + RLS
|
||||
- ✓ Deployed op Vercel
|
||||
- ✓ Code op GitHub met PR's + CI
|
||||
|
||||
### Feature must-haves
|
||||
|
||||
- Login flow (magic link of social)
|
||||
- Per-user data (RLS policies actief)
|
||||
- AI feature met tools (chat, agent, RAG, etc.)
|
||||
- Externe API integratie (geen alleen-OpenAI)
|
||||
- Productie URL die werkt
|
||||
|
||||
### Documentatie
|
||||
|
||||
- README met setup-instructies
|
||||
- ENV vars template (`.env.example`)
|
||||
- Beschrijving van je AI feature(s)
|
||||
- Architectuur-diagram (mag schets zijn)
|
||||
|
||||
### Tijd-indicatie
|
||||
|
||||
40-60 uur thuiswerk. Verspreid over 4-6 weken. Niet laatste weekend.
|
||||
|
||||
### Inleveren
|
||||
|
||||
- GitHub repo URL
|
||||
- Vercel productie URL
|
||||
- Demo-account credentials (test user)
|
||||
- 2-5 min screen recording van app in actie (optioneel maar aangeraden)
|
||||
|
||||
---
|
||||
|
||||
## Bronnen
|
||||
|
||||
- **Supabase Auth:** https://supabase.com/docs/guides/auth
|
||||
- **Next.js setup:** https://supabase.com/docs/guides/auth/server-side/nextjs
|
||||
- **Magic links:** https://supabase.com/docs/guides/auth/auth-magic-link
|
||||
- **Social login:** https://supabase.com/docs/guides/auth/social-login
|
||||
- **RLS guide:** https://supabase.com/docs/guides/database/postgres/row-level-security
|
||||
- **RLS policies:** https://supabase.com/docs/guides/database/postgres/row-level-security#policies
|
||||
- **Custom claims:** https://supabase.com/docs/guides/database/postgres/custom-claims-and-role-based-access-control-rbac
|
||||
- **Auth UI library:** https://supabase.com/docs/guides/auth/auth-helpers/auth-ui
|
||||
Reference in New Issue
Block a user