Files
novi-lessons/Les18-Supabase-Auth-RLS/Les18-Lesstof.md
2026-06-03 16:58:25 +02:00

543 lines
14 KiB
Markdown

# 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