543 lines
14 KiB
Markdown
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
|