feat: migrate clients module to patients + add docs
This commit is contained in:
206
docs/design/AUTH_FLOW_EXPLAINED.md
Normal file
206
docs/design/AUTH_FLOW_EXPLAINED.md
Normal file
@@ -0,0 +1,206 @@
|
||||
# Hoe werkt de Authenticatie Flow? 🔐
|
||||
|
||||
Een simpele uitleg van wat er gebeurt wanneer gebruikers zich aanmelden.
|
||||
|
||||
---
|
||||
|
||||
## 📧 Email Confirmatie Flow (nieuwe gebruikers)
|
||||
|
||||
### Stap 1: Gebruiker meldt zich aan
|
||||
```
|
||||
Gebruiker vult in op /login:
|
||||
├─ Email: jan@example.com
|
||||
└─ Wachtwoord: Geheim123!
|
||||
```
|
||||
|
||||
### Stap 2: Supabase stuurt email
|
||||
```
|
||||
Supabase maakt account aan → Stuurt bevestigingsmail
|
||||
|
||||
De email bevat een link zoals:
|
||||
https://aispeedrun.nl/auth/callback?token=xyz123&type=signup
|
||||
└─────┬─────┘
|
||||
Dit is de redirect URL!
|
||||
```
|
||||
|
||||
### Stap 3: Gebruiker klikt op link in email
|
||||
```
|
||||
Browser gaat naar: /auth/callback?token=xyz123
|
||||
|
||||
De callback route doet:
|
||||
1. ✅ Controleert de token
|
||||
2. ✅ Activeert het account
|
||||
3. ✅ Logt gebruiker in
|
||||
4. → Stuurt door naar /epd/clients (omdat ze al wachtwoord hebben)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔑 Wachtwoord Reset Flow
|
||||
|
||||
### Stap 1: Gebruiker klikt "Wachtwoord vergeten?"
|
||||
```
|
||||
Gaat naar: /reset-password
|
||||
Vult in: jan@example.com
|
||||
```
|
||||
|
||||
### Stap 2: Supabase stuurt reset email
|
||||
```
|
||||
Email bevat link:
|
||||
https://aispeedrun.nl/auth/callback?token=abc789&type=recovery&next=/update-password
|
||||
└─────┬─────┘ └────┬────┘
|
||||
Callback route Waar naartoe daarna?
|
||||
```
|
||||
|
||||
### Stap 3: Gebruiker klikt link
|
||||
```
|
||||
/auth/callback ontvangt de token
|
||||
├─ Controleert token ✅
|
||||
├─ Logt gebruiker tijdelijk in
|
||||
└─ Redirect naar: /update-password (van de 'next' parameter)
|
||||
```
|
||||
|
||||
### Stap 4: Nieuw wachtwoord instellen
|
||||
```
|
||||
Op /update-password:
|
||||
├─ Gebruiker vult nieuw wachtwoord in
|
||||
├─ Wachtwoord wordt opgeslagen
|
||||
└─ Redirect naar /login → Gebruiker kan inloggen!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✉️ Magic Link Flow (oude methode)
|
||||
|
||||
### Stap 1: Gebruiker vraagt magic link aan
|
||||
```
|
||||
Vult alleen email in (geen wachtwoord)
|
||||
```
|
||||
|
||||
### Stap 2: Email met magic link
|
||||
```
|
||||
Link: https://aispeedrun.nl/auth/callback?token=magic456
|
||||
```
|
||||
|
||||
### Stap 3: Eerste keer inloggen
|
||||
```
|
||||
/auth/callback detecteert: "nieuwe magic link gebruiker"
|
||||
└─ Redirect naar /set-password (optioneel wachtwoord instellen)
|
||||
├─ Wachtwoord instellen → /epd/clients
|
||||
└─ Overslaan → /epd/clients (blijf magic link gebruiken)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Waarom de Redirect URLs belangrijk zijn
|
||||
|
||||
Supabase moet weten welke URLs **veilig** zijn om naar terug te sturen.
|
||||
|
||||
### Zonder redirect URLs in Supabase:
|
||||
```
|
||||
❌ Link in email: https://aispeedrun.nl/auth/callback?token=xyz
|
||||
↓
|
||||
Supabase zegt: "Deze URL ken ik niet, BLOCKED!"
|
||||
↓
|
||||
Gebruiker ziet error 😞
|
||||
```
|
||||
|
||||
### Met redirect URLs in Supabase:
|
||||
```
|
||||
✅ Link in email: https://aispeedrun.nl/auth/callback?token=xyz
|
||||
↓
|
||||
Supabase zegt: "Deze URL staat in mijn lijst, OK!"
|
||||
↓
|
||||
Gebruiker wordt ingelogd en doorgestuurd 🎉
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 De Site URL vs Redirect URLs
|
||||
|
||||
### Site URL (1 URL)
|
||||
```
|
||||
Dit is je "hoofd" URL waar Supabase denkt dat je app draait.
|
||||
|
||||
Supabase gebruikt dit voor:
|
||||
├─ {{ .ConfirmationURL }} in emails (de basis)
|
||||
└─ Default redirects
|
||||
|
||||
Development: http://localhost:3000
|
||||
Production: https://aispeedrun.nl
|
||||
```
|
||||
|
||||
### Redirect URLs (meerdere URLs mogelijk)
|
||||
```
|
||||
Dit is de "whitelist" van URLs waar Supabase naartoe MAG redirecten.
|
||||
|
||||
Je moet ALLE mogelijke auth callbacks toevoegen:
|
||||
├─ /auth/callback → Email confirmaties, magic links
|
||||
├─ /update-password → Na password reset
|
||||
├─ /set-password → Nieuwe users (optioneel wachtwoord)
|
||||
└─ /reset-password → Password reset pagina
|
||||
|
||||
Voor zowel localhost als productie!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Simpel Gezegd
|
||||
|
||||
1. **Site URL** = Waar draait je app?
|
||||
- Tijdens development: `http://localhost:3000`
|
||||
- Live op internet: `https://aispeedrun.nl`
|
||||
|
||||
2. **Redirect URLs** = Welke paginas mag Supabase bezoeken na login/reset?
|
||||
- Voeg ALLE auth-gerelateerde URLs toe
|
||||
- Voor zowel development als productie
|
||||
|
||||
3. **Email links** = Gebouwd met Site URL + token
|
||||
- Als Site URL = localhost → emails gaan naar localhost ❌
|
||||
- Als Site URL = aispeedrun.nl → emails gaan naar je website ✅
|
||||
|
||||
---
|
||||
|
||||
## 📝 Voorbeeld Flow in de Praktijk
|
||||
|
||||
```
|
||||
[Gebruiker]
|
||||
↓ Registreert op /login
|
||||
[Jouw App]
|
||||
↓ POST naar Supabase "maak account"
|
||||
[Supabase]
|
||||
↓ Stuurt email naar gebruiker
|
||||
↓ Email link = [Site URL]/auth/callback?token=xyz
|
||||
[Email Inbox]
|
||||
↓ Gebruiker klikt link
|
||||
[Browser]
|
||||
↓ Gaat naar aispeedrun.nl/auth/callback?token=xyz
|
||||
[Supabase]
|
||||
↓ Checkt: staat "aispeedrun.nl/auth/callback" in Redirect URLs?
|
||||
↓ JA ✅ → Verifieert token
|
||||
[Jouw App - /auth/callback route]
|
||||
↓ Token geldig? → Login gebruiker
|
||||
↓ Nieuwe gebruiker met wachtwoord?
|
||||
↓ JA → Redirect naar /epd/clients
|
||||
[Gebruiker is ingelogd! 🎉]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ❓ Veelgestelde Vragen
|
||||
|
||||
### Waarom krijg ik localhost links in productie emails?
|
||||
→ Je Site URL staat nog op `http://localhost:3000` in Supabase. Wijzig naar `https://aispeedrun.nl`
|
||||
|
||||
### Waarom krijg ik "Invalid Redirect URL" errors?
|
||||
→ De URL staat niet in je Redirect URLs lijst. Voeg hem toe in Supabase Dashboard.
|
||||
|
||||
### Kan ik zowel localhost als productie tegelijk gebruiken?
|
||||
→ JA! Voeg beide toe aan Redirect URLs. Wissel alleen de Site URL afhankelijk van waar je test.
|
||||
|
||||
### Moet ik www. ook toevoegen?
|
||||
→ Als je site bereikbaar is via `www.aispeedrun.nl`, voeg dan ook die URLs toe.
|
||||
|
||||
---
|
||||
|
||||
**Hopelijk is het nu duidelijk! 🚀**
|
||||
183
docs/design/AUTH_HOOK_SETUP.md
Normal file
183
docs/design/AUTH_HOOK_SETUP.md
Normal file
@@ -0,0 +1,183 @@
|
||||
# Auth Hook Setup Guide
|
||||
|
||||
## Overzicht
|
||||
|
||||
Deze hook detecteert duplicate emails VOOR een user wordt aangemaakt,
|
||||
waardoor gebruikers direct feedback krijgen als hun email al geregistreerd is.
|
||||
|
||||
**Voordelen:**
|
||||
- ✅ Server-side validatie (kan niet omzeild worden)
|
||||
- ✅ Duidelijke foutmeldingen voor gebruikers
|
||||
- ✅ Betrouwbaar (werkt ongeacht password)
|
||||
- ✅ Case-insensitive email matching
|
||||
- ✅ Email normalisatie (lowercase + trim)
|
||||
|
||||
## Setup (Eerste Keer)
|
||||
|
||||
### Stap 1: Deploy Migration
|
||||
|
||||
**Optie A: Via Supabase Dashboard (Aanbevolen)**
|
||||
|
||||
1. Ga naar: [Supabase Dashboard → SQL Editor](https://supabase.com/dashboard/project/dqugbrpwtisgyxscpefg/sql)
|
||||
2. Open het migration bestand: `supabase/migrations/20251119094908_auth_hook_duplicate_email.sql`
|
||||
3. Kopieer de volledige inhoud
|
||||
4. Plak in de SQL Editor
|
||||
5. Klik "RUN" om de functie aan te maken
|
||||
|
||||
**Optie B: Via Supabase CLI (Als geconfigureerd)**
|
||||
|
||||
```bash
|
||||
npx supabase db push
|
||||
```
|
||||
|
||||
### Stap 2: Verificatie & Instructies
|
||||
|
||||
Run het setup script om te verifiëren dat de functie bestaat:
|
||||
|
||||
```bash
|
||||
pnpm run setup:auth-hook
|
||||
```
|
||||
|
||||
Dit script:
|
||||
- ✅ Checkt of de functie bestaat
|
||||
- 📋 Geeft instructies voor Dashboard configuratie
|
||||
- 🔗 Biedt directe links naar relevante Dashboard pagina's
|
||||
|
||||
### Stap 3: Configureer Hook Link
|
||||
|
||||
**⚠️ Deze stap moet handmatig via Dashboard** (Supabase ondersteunt dit nog niet via API):
|
||||
|
||||
1. Ga naar: [Supabase Dashboard → Auth → Hooks](https://supabase.com/dashboard/project/dqugbrpwtisgyxscpefg/auth/hooks)
|
||||
2. Klik **"Add a new hook"** of **"Enable Hooks"**
|
||||
3. Vul in:
|
||||
- **Hook Type:** "Send a hook on before a user is created" (`before-user-created`)
|
||||
- **Select hook:** "Postgres Function"
|
||||
- **Schema:** `public`
|
||||
- **Function Name:** `hook_check_duplicate_email`
|
||||
4. Klik **"Create hook"** of **"Save"**
|
||||
|
||||
### Stap 4: Test
|
||||
|
||||
Test de hook door:
|
||||
|
||||
1. Ga naar je signup pagina: `http://localhost:3000/login`
|
||||
2. Probeer te registreren met een **bestaand** emailadres (bijv. `demo@mini-ecd.demo`)
|
||||
3. Je zou een error moeten zien: _"Dit emailadres is al geregistreerd. Probeer in te loggen of gebruik 'Wachtwoord vergeten?'."_
|
||||
4. Probeer te registreren met een **nieuw** emailadres
|
||||
5. Dit zou normaal moeten werken (verificatie email verzonden)
|
||||
|
||||
## Test Cases
|
||||
|
||||
| Test Case | Scenario | Expected Result |
|
||||
|-----------|----------|-----------------|
|
||||
| TC1 | Signup met nieuw email | ✅ Account aangemaakt, email verzonden |
|
||||
| TC2 | Signup met bestaand email | ❌ Error: "Dit emailadres is al geregistreerd..." |
|
||||
| TC3 | Signup met bestaand email (case variant: `Email@Example.com`) | ❌ Error (case-insensitive match) |
|
||||
| TC4 | Signup met lege/NULL email | ❌ Error: "Email adres is verplicht." |
|
||||
| TC5 | Hook disabled → signup met bestaand email | ⚠️ Oude gedrag (geen error, maar ook geen email) |
|
||||
|
||||
## Herhaalbaarheid
|
||||
|
||||
- ✅ **Functie code** staat in migrations (version controlled)
|
||||
- ⚠️ **Hook link** moet per omgeving handmatig worden geconfigureerd
|
||||
- ✅ **Documentatie** staat in Git
|
||||
- ✅ **Setup script** voor validatie en instructies
|
||||
|
||||
## Technische Details
|
||||
|
||||
### Wat Doet de Hook?
|
||||
|
||||
De `hook_check_duplicate_email` functie:
|
||||
|
||||
1. Ontvangt signup event van Supabase Auth
|
||||
2. Haalt email adres uit event payload
|
||||
3. Valideert email (niet NULL/empty)
|
||||
4. Normaliseert email (lowercase + trim)
|
||||
5. Checkt of email al bestaat in `auth.users` table (case-insensitive)
|
||||
6. Als email bestaat → return error object
|
||||
7. Als email nieuw is → return empty object (allow signup)
|
||||
|
||||
### Security
|
||||
|
||||
- **Security Definer:** Functie draait met elevated permissions
|
||||
- **Search Path:** Expliciet ingesteld op `public, auth` voor veilige schema access
|
||||
- **Permissions:** Alleen `supabase_auth_admin` kan de functie uitvoeren
|
||||
- **Email Enumeration Protection:** Werkt samen met bestaande email confirmation
|
||||
|
||||
### Performance
|
||||
|
||||
- ⚡ Direct database check (geen extra HTTP calls)
|
||||
- ⚡ Indexed lookup op `auth.users.email`
|
||||
- ⚡ Minimale overhead (< 10ms typisch)
|
||||
|
||||
## Toekomstige Verbeteringen
|
||||
|
||||
Zodra Supabase Management API Auth Hooks ondersteunt, kunnen we:
|
||||
|
||||
- [ ] Hook link volledig automatiseren
|
||||
- [ ] Setup script uitbreiden met API calls
|
||||
- [ ] CI/CD pipeline voor hook configuratie
|
||||
- [ ] Automated tests voor hook functionaliteit
|
||||
|
||||
## Flexibiliteit
|
||||
|
||||
De functie is geschreven in standaard PostgreSQL, waardoor:
|
||||
|
||||
- ✅ Werkt met elke auth provider die Postgres functies ondersteunt
|
||||
- ✅ Makkelijk te migreren naar andere auth systemen
|
||||
- ✅ Geen vendor lock-in voor de logica zelf
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Function does not exist" error
|
||||
|
||||
**Probleem:** De hook functie is niet aangemaakt in de database.
|
||||
|
||||
**Oplossing:**
|
||||
1. Controleer of migration is uitgevoerd via Dashboard of CLI
|
||||
2. Run `pnpm run setup:auth-hook` voor verificatie
|
||||
3. Check Supabase logs voor SQL errors
|
||||
|
||||
### Hook lijkt niet te werken
|
||||
|
||||
**Probleem:** Signup met bestaand email geeft geen error.
|
||||
|
||||
**Mogelijke oorzaken:**
|
||||
1. Hook link niet geconfigureerd in Dashboard → Ga naar Auth → Hooks
|
||||
2. Hook is disabled → Check hook status in Dashboard
|
||||
3. Email confirmation staat uit → Check Auth → Email Templates
|
||||
|
||||
**Verificatie:**
|
||||
```sql
|
||||
-- Check of functie bestaat
|
||||
SELECT routine_name
|
||||
FROM information_schema.routines
|
||||
WHERE routine_schema = 'public'
|
||||
AND routine_name = 'hook_check_duplicate_email';
|
||||
|
||||
-- Test functie handmatig
|
||||
SELECT hook_check_duplicate_email('{"user": {"email": "demo@mini-ecd.demo"}}'::jsonb);
|
||||
```
|
||||
|
||||
### Wrong error message
|
||||
|
||||
**Probleem:** Error message klopt niet of is in het Engels.
|
||||
|
||||
**Oplossing:**
|
||||
1. Check of je de laatste versie van de migration hebt gebruikt
|
||||
2. Update functie via SQL Editor met correcte error messages
|
||||
3. Rebuild client error handling (`app/login/page.tsx`)
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Supabase Auth Hooks Documentation](https://supabase.com/docs/guides/auth/auth-hooks)
|
||||
- [Bouwplan: Auth Hook Implementation](./bouwplan-auth-hook-duplicate-email-v1.0.md)
|
||||
- [Main README](../README.md)
|
||||
|
||||
## Support
|
||||
|
||||
Voor vragen of problemen:
|
||||
1. Check deze documentatie
|
||||
2. Check Supabase logs in Dashboard
|
||||
3. Run `pnpm run setup:auth-hook` voor diagnostics
|
||||
4. Review `supabase/migrations/20251119094908_auth_hook_duplicate_email.sql`
|
||||
416
docs/design/AUTH_SETUP.md
Normal file
416
docs/design/AUTH_SETUP.md
Normal file
@@ -0,0 +1,416 @@
|
||||
# 🔐 Authentication Setup Guide
|
||||
|
||||
**Project:** AI Speedrun - Mini-ECD Prototype
|
||||
**Epic:** E2 - Database & Auth
|
||||
**Story:** E2.S3 - Demo auth flow
|
||||
**Last Updated:** 2024-11-15
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the authentication implementation for the EPD prototype, including magic link login and demo user accounts.
|
||||
|
||||
---
|
||||
|
||||
## Authentication Methods
|
||||
|
||||
### 1. Magic Link (Primary Method)
|
||||
|
||||
Users can sign in using email-only authentication:
|
||||
|
||||
1. User enters email on `/login`
|
||||
2. Supabase sends magic link to email
|
||||
3. User clicks link → auto-logged in
|
||||
4. **New users:** Account is automatically created on first magic link request
|
||||
|
||||
**Benefits:**
|
||||
- No password to remember
|
||||
- More secure than traditional passwords
|
||||
- Better UX for demo environment
|
||||
- Auto-creates accounts (no separate signup flow needed)
|
||||
|
||||
---
|
||||
|
||||
### 2. Demo Accounts (For Presentations)
|
||||
|
||||
Pre-configured demo accounts for public demos and presentations:
|
||||
|
||||
| Email | Password | Access Level | Purpose |
|
||||
|-------|----------|--------------|---------|
|
||||
| demo@mini-ecd.demo | Demo2024! | interactive | Main demo account - full CRUD |
|
||||
| readonly@mini-ecd.demo | Demo2024! | read_only | View-only for public demos |
|
||||
| presenter@mini-ecd.demo | Demo2024! | presenter | Live presentations |
|
||||
|
||||
**Access Levels:**
|
||||
- `read_only`: Can view all data, cannot create/edit/delete
|
||||
- `interactive`: Full CRUD access to all features
|
||||
- `presenter`: Full access + special presenter features (future)
|
||||
|
||||
---
|
||||
|
||||
## Setup Instructions
|
||||
|
||||
### 1. Environment Variables
|
||||
|
||||
Ensure these are set in your `.env.local`:
|
||||
|
||||
```bash
|
||||
# Supabase
|
||||
NEXT_PUBLIC_SUPABASE_URL=https://dqugbrpwtisgyxscpefg.supabase.co
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
|
||||
|
||||
# Service role key (for admin operations)
|
||||
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
|
||||
```
|
||||
|
||||
### 2. Create Demo Users
|
||||
|
||||
Run the seed script to create demo user accounts:
|
||||
|
||||
```bash
|
||||
# Make sure you have tsx installed
|
||||
pnpm add -D tsx
|
||||
|
||||
# Run the seed script
|
||||
tsx scripts/seed-demo-users.ts
|
||||
```
|
||||
|
||||
**Expected output:**
|
||||
```
|
||||
🌱 Starting demo user seed...
|
||||
|
||||
Creating user: demo@mini-ecd.demo...
|
||||
✅ Created auth user: xxx-xxx-xxx
|
||||
✅ Created demo_users entry
|
||||
✨ demo@mini-ecd.demo ready!
|
||||
|
||||
Creating user: readonly@mini-ecd.demo...
|
||||
✅ Created auth user: xxx-xxx-xxx
|
||||
✅ Created demo_users entry
|
||||
✨ readonly@mini-ecd.demo ready!
|
||||
|
||||
Creating user: presenter@mini-ecd.demo...
|
||||
✅ Created auth user: xxx-xxx-xxx
|
||||
✅ Created demo_users entry
|
||||
✨ presenter@mini-ecd.demo ready!
|
||||
|
||||
✅ Demo user seed complete!
|
||||
```
|
||||
|
||||
### 3. Configure Supabase Auth Settings
|
||||
|
||||
Go to Supabase Dashboard → Authentication → Settings:
|
||||
|
||||
#### Email Templates
|
||||
|
||||
Customize the magic link email template:
|
||||
|
||||
**Subject:** "Login to Mini-ECD"
|
||||
|
||||
**Body:**
|
||||
```html
|
||||
<h2>Je magic link is klaar!</h2>
|
||||
<p>Klik op de knop hieronder om in te loggen bij Mini-ECD:</p>
|
||||
<p><a href="{{ .ConfirmationURL }}">Login naar EPD</a></p>
|
||||
<p>Of kopieer deze link naar je browser:</p>
|
||||
<p>{{ .ConfirmationURL }}</p>
|
||||
<p><small>Deze link is 1 uur geldig.</small></p>
|
||||
```
|
||||
|
||||
#### Redirect URLs
|
||||
|
||||
Add these redirect URLs under "Redirect URLs":
|
||||
|
||||
```
|
||||
http://localhost:3000/auth/callback
|
||||
https://yourdomain.com/auth/callback
|
||||
```
|
||||
|
||||
#### Email Auth Settings
|
||||
|
||||
- ✅ Enable Email provider
|
||||
- ✅ Confirm email: OFF (for demo convenience)
|
||||
- ✅ Secure email change: ON
|
||||
- ⏱️ Rate limits: Default (4 emails per hour)
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
app/
|
||||
login/
|
||||
page.tsx # Login UI (magic link + demo login)
|
||||
auth/
|
||||
callback/
|
||||
route.ts # Handles magic link callback
|
||||
logout/
|
||||
route.ts # Logout endpoint
|
||||
|
||||
lib/
|
||||
auth/
|
||||
client.ts # Client-side auth helpers
|
||||
server.ts # Server-side auth helpers
|
||||
database.types.ts # Generated Supabase types
|
||||
|
||||
middleware.ts # Route protection
|
||||
scripts/
|
||||
seed-demo-users.ts # Demo user creation script
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Client-Side (React Components)
|
||||
|
||||
```typescript
|
||||
import {
|
||||
loginWithMagicLink,
|
||||
loginWithPassword,
|
||||
logout,
|
||||
getUser,
|
||||
isDemoUser
|
||||
} from '@/lib/auth/client'
|
||||
|
||||
// Magic link login
|
||||
async function handleMagicLink(email: string) {
|
||||
const result = await loginWithMagicLink(email)
|
||||
console.log(result.message) // "Check je email voor de magic link!"
|
||||
}
|
||||
|
||||
// Demo account login
|
||||
async function handleDemoLogin() {
|
||||
await loginWithPassword('demo@mini-ecd.demo', 'Demo2024!')
|
||||
router.push('/clients')
|
||||
}
|
||||
|
||||
// Check current user
|
||||
const user = await getUser()
|
||||
const isDemo = await isDemoUser()
|
||||
|
||||
// Logout
|
||||
await logout() // Redirects to /login
|
||||
```
|
||||
|
||||
### Server-Side (API Routes, Server Components)
|
||||
|
||||
```typescript
|
||||
import {
|
||||
requireAuth,
|
||||
getUser,
|
||||
canWrite,
|
||||
getDemoUserInfo
|
||||
} from '@/lib/auth/server'
|
||||
|
||||
// Require authentication in API route
|
||||
export async function GET() {
|
||||
const session = await requireAuth() // Throws if not authenticated
|
||||
// ... handle request
|
||||
}
|
||||
|
||||
// Check write permissions
|
||||
export async function POST() {
|
||||
const hasWriteAccess = await canWrite()
|
||||
|
||||
if (!hasWriteAccess) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Read-only demo account cannot create data' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
// ... create resource
|
||||
}
|
||||
|
||||
// Get demo user info
|
||||
const demoInfo = await getDemoUserInfo()
|
||||
if (demoInfo) {
|
||||
console.log(`Access level: ${demoInfo.access_level}`)
|
||||
console.log(`Usage count: ${demoInfo.usage_count}`)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Route Protection
|
||||
|
||||
Routes are protected via `middleware.ts`:
|
||||
|
||||
### Public Routes (No Auth Required)
|
||||
- `/` - Landing page
|
||||
- `/login` - Login page
|
||||
- `/epd` - EPD demo info
|
||||
- `/contact` - Contact form
|
||||
- `/auth/callback` - Auth callback
|
||||
|
||||
### Protected Routes (Auth Required)
|
||||
- `/clients` - Client list
|
||||
- `/clients/*` - Client details, intake, etc.
|
||||
- Any other route not in public list
|
||||
|
||||
**Behavior:**
|
||||
- ✅ Unauthenticated → Redirect to `/login?redirect=/original-path`
|
||||
- ✅ Authenticated on `/login` → Redirect to `/clients`
|
||||
- ✅ Session auto-refreshed in middleware
|
||||
|
||||
---
|
||||
|
||||
## Security Features
|
||||
|
||||
### ✅ Implemented
|
||||
|
||||
1. **RLS Policies**: All database queries filtered by `auth.uid()`
|
||||
2. **Session Management**: Auto-refresh tokens via middleware
|
||||
3. **Secure Cookies**: HTTP-only, secure flags set
|
||||
4. **CSRF Protection**: Built-in Next.js CSRF protection
|
||||
5. **Rate Limiting**: Supabase default (4 emails/hour)
|
||||
6. **Demo User Tracking**: Usage count and last login tracked
|
||||
|
||||
### 🔒 Production Enhancements
|
||||
|
||||
For production deployment:
|
||||
|
||||
1. **Email Confirmation**: Enable email confirmation
|
||||
2. **Password Requirements**: Enforce strong passwords
|
||||
3. **MFA**: Add multi-factor authentication
|
||||
4. **Session Timeout**: Implement auto-logout after inactivity
|
||||
5. **IP Whitelisting**: Restrict demo accounts to specific IPs
|
||||
6. **Audit Logging**: Enhanced tracking of all auth events
|
||||
|
||||
---
|
||||
|
||||
## Demo User Management
|
||||
|
||||
### Checking Demo Status
|
||||
|
||||
```typescript
|
||||
// Check if user is demo user
|
||||
const isDemo = await isDemoUser()
|
||||
|
||||
// Get access level
|
||||
const accessLevel = await getDemoAccessLevel()
|
||||
// Returns: 'read_only' | 'interactive' | 'presenter' | null
|
||||
```
|
||||
|
||||
### Restricting Actions
|
||||
|
||||
```typescript
|
||||
// In API route
|
||||
const demoInfo = await getDemoUserInfo()
|
||||
|
||||
if (demoInfo?.access_level === 'read_only') {
|
||||
return NextResponse.json(
|
||||
{ error: 'This demo account is read-only' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Resetting Demo Accounts
|
||||
|
||||
To reset a demo account (clear data, reset usage):
|
||||
|
||||
```sql
|
||||
-- Reset usage count
|
||||
UPDATE demo_users
|
||||
SET usage_count = 0, last_login_at = NULL
|
||||
WHERE access_level = 'interactive';
|
||||
|
||||
-- Or via Supabase Dashboard: Authentication → Users → Delete user data
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: Magic link not arriving
|
||||
|
||||
**Causes:**
|
||||
- Email in spam folder
|
||||
- Rate limit exceeded (4 emails/hour)
|
||||
- Email provider blocking Supabase emails
|
||||
|
||||
**Solutions:**
|
||||
1. Check spam folder
|
||||
2. Wait 1 hour and try again
|
||||
3. Use demo account instead
|
||||
4. Configure custom SMTP in Supabase
|
||||
|
||||
### Issue: "Invalid login credentials"
|
||||
|
||||
**Causes:**
|
||||
- Wrong email/password for demo account
|
||||
- Demo user not created yet
|
||||
|
||||
**Solutions:**
|
||||
1. Check credentials match exactly (case-sensitive)
|
||||
2. Run seed script: `tsx scripts/seed-demo-users.ts`
|
||||
3. Verify in Supabase Dashboard → Authentication → Users
|
||||
|
||||
### Issue: Redirect loop on /login
|
||||
|
||||
**Causes:**
|
||||
- Middleware configuration error
|
||||
- Session cookie issues
|
||||
|
||||
**Solutions:**
|
||||
1. Clear browser cookies
|
||||
2. Check middleware.ts public routes config
|
||||
3. Verify `NEXT_PUBLIC_SUPABASE_URL` is correct
|
||||
|
||||
### Issue: "Row violates RLS policy" errors
|
||||
|
||||
**Causes:**
|
||||
- User not properly authenticated
|
||||
- Session expired
|
||||
- RLS policies misconfigured
|
||||
|
||||
**Solutions:**
|
||||
1. Logout and login again
|
||||
2. Check `auth.uid()` returns valid UUID
|
||||
3. Verify RLS policies allow user access
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Magic Link Flow
|
||||
- [ ] Can enter email on /login
|
||||
- [ ] Magic link email received
|
||||
- [ ] Clicking link redirects to /clients
|
||||
- [ ] Session persists after page refresh
|
||||
- [ ] New users auto-created on first login
|
||||
|
||||
### Demo Account Flow
|
||||
- [ ] Can login with demo@mini-ecd.demo
|
||||
- [ ] Can login with readonly@mini-ecd.demo
|
||||
- [ ] Interactive account can create/edit data
|
||||
- [ ] Read-only account blocked from editing
|
||||
- [ ] Demo usage tracked in demo_users table
|
||||
|
||||
### Route Protection
|
||||
- [ ] /clients redirects to /login when not authenticated
|
||||
- [ ] /login redirects to /clients when authenticated
|
||||
- [ ] Public routes accessible without auth
|
||||
- [ ] Session auto-refreshes
|
||||
|
||||
### Logout
|
||||
- [ ] Logout clears session
|
||||
- [ ] Redirects to /login
|
||||
- [ ] Cannot access protected routes after logout
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [Supabase Auth Documentation](https://supabase.com/docs/guides/auth)
|
||||
- [Next.js Middleware](https://nextjs.org/docs/app/building-your-application/routing/middleware)
|
||||
- Build Plan: `docs/specs/bouwplan-ai-speedrun-marketing-first-v1.1.md` Epic 2
|
||||
- Technical Design: `docs/specs/to-mini-ecd-v1_2.md` § 5.7
|
||||
|
||||
---
|
||||
|
||||
**Status:** ✅ Implemented and Ready for Testing
|
||||
**Next Steps:** E2.S4 - Seed data script (clients + dossiers)
|
||||
304
docs/design/RLS_SECURITY.md
Normal file
304
docs/design/RLS_SECURITY.md
Normal file
@@ -0,0 +1,304 @@
|
||||
# 🔒 Row Level Security (RLS) Documentation
|
||||
|
||||
**Project:** AI Speedrun - Mini-ECD Prototype
|
||||
**Epic:** E2 - Database & Auth
|
||||
**Story:** E2.S2 - RLS policies implementeren
|
||||
**Last Updated:** 2024-11-15
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the Row Level Security (RLS) implementation for the EPD core database tables. RLS is PostgreSQL's security feature that restricts which rows users can access in database queries.
|
||||
|
||||
### Security Model
|
||||
|
||||
- **Authentication Required:** All data access requires a valid Supabase authentication session
|
||||
- **Authorization:** Checked via `auth.uid()` function which returns the authenticated user's UUID
|
||||
- **MVP Level:** All authenticated users can access all data (suitable for demo/single-org)
|
||||
- **Production Path:** Ready to extend with `org_id` filtering for multi-tenancy
|
||||
|
||||
---
|
||||
|
||||
## Tables & Policies
|
||||
|
||||
### 1. Clients Table
|
||||
|
||||
**Purpose:** Basic client information
|
||||
**RLS Enabled:** ✅ Yes
|
||||
|
||||
#### Policies:
|
||||
|
||||
| Policy Name | Operation | Rule |
|
||||
|------------|-----------|------|
|
||||
| Authenticated users can view clients | SELECT | `auth.uid() IS NOT NULL` |
|
||||
| Authenticated users can create clients | INSERT | `auth.uid() IS NOT NULL` |
|
||||
| Authenticated users can update clients | UPDATE | `auth.uid() IS NOT NULL` |
|
||||
| Authenticated users can delete clients | DELETE | `auth.uid() IS NOT NULL` |
|
||||
|
||||
**Production Enhancement:**
|
||||
```sql
|
||||
-- Add organization filtering
|
||||
CREATE POLICY "Users can view own org clients"
|
||||
ON clients FOR SELECT
|
||||
USING (
|
||||
auth.uid() IS NOT NULL AND
|
||||
org_id = (SELECT org_id FROM users WHERE id = auth.uid())
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Intake Notes Table
|
||||
|
||||
**Purpose:** TipTap/ProseMirror JSON content storage
|
||||
**RLS Enabled:** ✅ Yes
|
||||
|
||||
#### Policies:
|
||||
|
||||
| Policy Name | Operation | Rule |
|
||||
|------------|-----------|------|
|
||||
| Authenticated users can view intake notes | SELECT | `auth.uid() IS NOT NULL` |
|
||||
| Authenticated users can create intake notes | INSERT | `auth.uid() IS NOT NULL` |
|
||||
| Authenticated users can update intake notes | UPDATE | `auth.uid() IS NOT NULL` |
|
||||
| Authenticated users can delete intake notes | DELETE | `auth.uid() IS NOT NULL` |
|
||||
|
||||
**Security Features:**
|
||||
- Full-text search index with Dutch language support
|
||||
- Cascade delete when parent client is deleted
|
||||
- Automatic `updated_at` trigger
|
||||
|
||||
---
|
||||
|
||||
### 3. Problem Profiles Table
|
||||
|
||||
**Purpose:** DSM-light categorization with severity scoring
|
||||
**RLS Enabled:** ✅ Yes
|
||||
|
||||
#### Policies:
|
||||
|
||||
| Policy Name | Operation | Rule |
|
||||
|------------|-----------|------|
|
||||
| Authenticated users can view problem profiles | SELECT | `auth.uid() IS NOT NULL` |
|
||||
| Authenticated users can create problem profiles | INSERT | `auth.uid() IS NOT NULL` |
|
||||
| Authenticated users can update problem profiles | UPDATE | `auth.uid() IS NOT NULL` |
|
||||
| Authenticated users can delete problem profiles | DELETE | `auth.uid() IS NOT NULL` |
|
||||
|
||||
**Data Constraints:**
|
||||
- Category: Must be one of 6 DSM-light categories
|
||||
- Severity: Must be 'laag', 'middel', or 'hoog'
|
||||
- Cascade delete with parent client
|
||||
- SET NULL on source note deletion
|
||||
|
||||
---
|
||||
|
||||
### 4. Treatment Plans Table
|
||||
|
||||
**Purpose:** Treatment plans with JSONB structure and versioning
|
||||
**RLS Enabled:** ✅ Yes
|
||||
|
||||
#### Policies:
|
||||
|
||||
| Policy Name | Operation | Rule |
|
||||
|------------|-----------|------|
|
||||
| Authenticated users can view treatment plans | SELECT | `auth.uid() IS NOT NULL` |
|
||||
| Authenticated users can create treatment plans | INSERT | `auth.uid() IS NOT NULL` |
|
||||
| Authenticated users can update treatment plans | UPDATE | `auth.uid() IS NOT NULL` |
|
||||
| Authenticated users can delete treatment plans | DELETE | `auth.uid() IS NOT NULL` |
|
||||
|
||||
**Versioning:**
|
||||
- Each client can have multiple versions (v1, v2, etc.)
|
||||
- Status: 'concept' (editable) or 'gepubliceerd' (locked)
|
||||
- UNIQUE constraint on (client_id, version)
|
||||
|
||||
---
|
||||
|
||||
### 5. AI Events Table
|
||||
|
||||
**Purpose:** Telemetry and debugging for AI API calls
|
||||
**RLS Enabled:** ✅ Yes
|
||||
**Special:** Append-only (no UPDATE/DELETE for regular users)
|
||||
|
||||
#### Policies:
|
||||
|
||||
| Policy Name | Operation | Rule |
|
||||
|------------|-----------|------|
|
||||
| Authenticated users can view AI events | SELECT | `auth.uid() IS NOT NULL` |
|
||||
| Authenticated users can create AI events | INSERT | `auth.uid() IS NOT NULL` |
|
||||
| ~~UPDATE~~ | ❌ | Not allowed (audit trail) |
|
||||
| ~~DELETE~~ | ❌ | Not allowed (audit trail) |
|
||||
|
||||
**Immutability:**
|
||||
- Regular users cannot modify or delete AI events
|
||||
- Ensures audit trail integrity
|
||||
- Service role can bypass RLS for admin cleanup
|
||||
|
||||
---
|
||||
|
||||
## Testing RLS
|
||||
|
||||
### Test 1: Verify RLS is Enabled
|
||||
|
||||
```sql
|
||||
SELECT tablename, rowsecurity as rls_enabled
|
||||
FROM pg_tables
|
||||
WHERE schemaname = 'public'
|
||||
ORDER BY tablename;
|
||||
```
|
||||
|
||||
**Expected Result:**
|
||||
All 5 tables should show `rls_enabled: true`
|
||||
|
||||
### Test 2: Check Policy Count
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
tablename,
|
||||
COUNT(*) as policy_count,
|
||||
STRING_AGG(cmd, ', ' ORDER BY cmd) as commands
|
||||
FROM pg_policies
|
||||
WHERE schemaname = 'public'
|
||||
GROUP BY tablename;
|
||||
```
|
||||
|
||||
**Expected Result:**
|
||||
- `ai_events`: 2 policies (INSERT, SELECT)
|
||||
- Other tables: 4 policies each (DELETE, INSERT, SELECT, UPDATE)
|
||||
|
||||
### Test 3: Verify Authentication Check
|
||||
|
||||
```sql
|
||||
SELECT tablename, policyname, cmd, qual
|
||||
FROM pg_policies
|
||||
WHERE schemaname = 'public'
|
||||
AND qual NOT LIKE '%auth.uid()%';
|
||||
```
|
||||
|
||||
**Expected Result:**
|
||||
Empty (all policies use `auth.uid()` checks)
|
||||
|
||||
---
|
||||
|
||||
## TypeScript Integration
|
||||
|
||||
TypeScript types are auto-generated and available at `lib/database.types.ts`:
|
||||
|
||||
```typescript
|
||||
import type { Database } from '@/lib/database.types'
|
||||
|
||||
// Usage with Supabase client
|
||||
const supabase = createClient<Database>(url, key)
|
||||
|
||||
// Type-safe queries
|
||||
const { data: clients } = await supabase
|
||||
.from('clients')
|
||||
.select('*')
|
||||
|
||||
// Insert with type checking
|
||||
const { data: newClient } = await supabase
|
||||
.from('clients')
|
||||
.insert({
|
||||
first_name: 'John',
|
||||
last_name: 'Doe',
|
||||
birth_date: '1990-01-01'
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### ✅ Current Implementation
|
||||
|
||||
1. **Secure by Default:** RLS enabled on all tables
|
||||
2. **Authentication Required:** All policies check `auth.uid() IS NOT NULL`
|
||||
3. **Separation of Concerns:** Separate policies for each operation (SELECT, INSERT, UPDATE, DELETE)
|
||||
4. **Audit Trail:** AI events are append-only
|
||||
5. **Foreign Key Constraints:** Automatic cleanup with CASCADE/SET NULL
|
||||
6. **Type Safety:** Generated TypeScript types prevent runtime errors
|
||||
|
||||
### 🔄 Production Enhancements
|
||||
|
||||
When moving to production with multiple organizations:
|
||||
|
||||
1. **Add Organization Column:**
|
||||
```sql
|
||||
ALTER TABLE clients ADD COLUMN org_id UUID REFERENCES organizations(id);
|
||||
```
|
||||
|
||||
2. **Update Policies with Org Filtering:**
|
||||
```sql
|
||||
CREATE POLICY "Users can view own org data"
|
||||
ON clients FOR SELECT
|
||||
USING (
|
||||
auth.uid() IS NOT NULL AND
|
||||
org_id = (SELECT org_id FROM users WHERE id = auth.uid())
|
||||
);
|
||||
```
|
||||
|
||||
3. **Add Role-Based Access:**
|
||||
```sql
|
||||
CREATE POLICY "Admins can view all"
|
||||
ON clients FOR SELECT
|
||||
USING (
|
||||
auth.uid() IS NOT NULL AND
|
||||
EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE id = auth.uid() AND role IN ('admin', 'superadmin')
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
4. **Implement Row-Level Ownership:**
|
||||
```sql
|
||||
CREATE POLICY "Users can update own records"
|
||||
ON intake_notes FOR UPDATE
|
||||
USING (author = auth.uid());
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: "new row violates row-level security policy"
|
||||
|
||||
**Cause:** Trying to insert/update data that doesn't satisfy RLS WITH CHECK
|
||||
**Solution:** Ensure user is authenticated and data meets policy requirements
|
||||
|
||||
### Issue: No data returned despite existing rows
|
||||
|
||||
**Cause:** User not authenticated or RLS USING clause filters out all rows
|
||||
**Solution:** Verify `auth.uid()` returns a valid UUID
|
||||
|
||||
### Issue: Service role queries still restricted
|
||||
|
||||
**Cause:** Using anon key instead of service role key
|
||||
**Solution:** Use `SUPABASE_SERVICE_ROLE_KEY` for admin operations
|
||||
|
||||
```typescript
|
||||
// Service role bypasses RLS
|
||||
const supabase = createClient(url, serviceRoleKey)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration History
|
||||
|
||||
| Migration | Date | Changes |
|
||||
|-----------|------|---------|
|
||||
| `20241115000002_create_epd_core_tables.sql` | 2024-11-15 | Initial RLS policies (demo-level) |
|
||||
| `20241115000003_enhance_rls_policies.sql` | 2024-11-15 | Granular policies per operation + ai_events immutability |
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [Supabase RLS Documentation](https://supabase.com/docs/guides/auth/row-level-security)
|
||||
- [PostgreSQL RLS Documentation](https://www.postgresql.org/docs/current/ddl-rowsecurity.html)
|
||||
- Technical Design: `docs/specs/to-mini-ecd-v1_2.md` § 2.4
|
||||
- Build Plan: `docs/specs/bouwplan-ai-speedrun-marketing-first-v1.1.md` Epic 2
|
||||
|
||||
---
|
||||
|
||||
**Status:** ✅ Implemented and Tested
|
||||
**Next Steps:** E2.S3 - Demo auth flow
|
||||
494
docs/design/datamodel-documentatie.md
Normal file
494
docs/design/datamodel-documentatie.md
Normal file
@@ -0,0 +1,494 @@
|
||||
# Datamodel Mini-ECD: FHIR-compliant GGZ Dossier
|
||||
|
||||
**Versie:** 1.1
|
||||
**Datum:** 21 november 2024
|
||||
**Status:** In ontwikkeling
|
||||
|
||||
---
|
||||
|
||||
## Overzicht
|
||||
|
||||
Het Mini-ECD gebruikt een datamodel gebaseerd op **FHIR (Fast Healthcare Interoperability Resources)**, de internationale standaard voor uitwisseling van zorggegevens. Dit maakt toekomstige integratie met MedMIJ (patiëntportalen) en Koppeltaal (eHealth apps) mogelijk zonder grote aanpassingen.
|
||||
|
||||
Het datamodel bestaat uit **13 kernonderdelen** die samen het complete GGZ-traject ondersteunen: van aanmelding tot behandelplan, inclusief doelen, toestemmingen en belangrijke waarschuwingen.
|
||||
|
||||
---
|
||||
|
||||
## De 13 bouwstenen van het dossier
|
||||
|
||||
### 1. **Behandelaren** (`practitioners`)
|
||||
**Wat is het?**
|
||||
Alle zorgprofessionals die in het systeem werken: psychologen, psychiaters, gz-psychologen, verpleegkundigen, etc.
|
||||
|
||||
**Belangrijkste gegevens:**
|
||||
- BIG-nummer (indien geregistreerd)
|
||||
- AGB-code
|
||||
- Naam en voorletters
|
||||
- Kwalificaties (bijv. "GZ-psycholoog", "Psychotherapeut")
|
||||
- Contactgegevens
|
||||
|
||||
**Waarom FHIR?**
|
||||
In FHIR heet dit een **Practitioner** resource. Dit maakt het mogelijk om behandelaren later uit te wisselen met andere systemen (bijvoorbeeld voor verwijzingen).
|
||||
|
||||
---
|
||||
|
||||
### 2. **Instellingen** (`organizations`)
|
||||
**Wat is het?**
|
||||
De GGZ-organisaties zelf: jouw instelling, maar ook externe organisaties waarmee je samenwerkt.
|
||||
|
||||
**Belangrijkste gegevens:**
|
||||
- AGB-code instelling
|
||||
- KVK-nummer
|
||||
- Naam en eventuele nevenvestigingen
|
||||
- Contactgegevens en adres
|
||||
|
||||
**Waarom FHIR?**
|
||||
In FHIR heet dit een **Organization** resource. Nodig voor facturatie, verwijzingen en juridische verantwoordelijkheid.
|
||||
|
||||
---
|
||||
|
||||
### 3. **Cliënten** (`patients`)
|
||||
**Wat is het?**
|
||||
De patiënten/cliënten die behandeling krijgen.
|
||||
|
||||
**Belangrijkste gegevens:**
|
||||
- BSN (verplicht)
|
||||
- Naam, geboortedatum, geslacht
|
||||
- Adres en contactgegevens
|
||||
- Verzekeringsgegevens
|
||||
- Huisarts (naam + AGB-code)
|
||||
- Noodcontactpersoon
|
||||
|
||||
**Waarom FHIR?**
|
||||
In FHIR heet dit een **Patient** resource. Dit is de basis voor alle andere gegevens in het dossier. Het correspondeert met de Nederlandse **ZIB Patient** (ZorgInformatieBouwsteen).
|
||||
|
||||
**Privacy:**
|
||||
BSN wordt versleuteld opgeslagen en is alleen toegankelijk voor geautoriseerde behandelaren.
|
||||
|
||||
---
|
||||
|
||||
### 4. **Contactmomenten** (`encounters`)
|
||||
**Wat is het?**
|
||||
Elk contact tussen cliënt en behandelaar: intakegesprek, behandelsessie, telefonisch consult, etc.
|
||||
|
||||
**Belangrijkste gegevens:**
|
||||
- Type contact (intake, diagnostiek, behandeling, crisis)
|
||||
- Status (gepland, bezig, afgerond)
|
||||
- Wanneer (start- en eindtijd)
|
||||
- Wie (behandelaar + cliënt)
|
||||
- Waar (polikliniek, online, kliniek)
|
||||
- Waarom (aanmeldingsreden, klachten)
|
||||
|
||||
**Waarom FHIR?**
|
||||
In FHIR heet dit een **Encounter** resource. Dit is cruciaal omdat alle andere gegevens (diagnoses, observaties, behandelplannen) gekoppeld worden aan een specifiek contactmoment. Hierdoor kun je later zien: "Deze diagnose is gesteld tijdens de intake van 15 maart 2024".
|
||||
|
||||
Dit correspondeert met de Nederlandse **ZIB Contact**.
|
||||
|
||||
---
|
||||
|
||||
### 5. **Diagnoses** (`conditions`)
|
||||
**Wat is het?**
|
||||
De vastgestelde diagnoses volgens DSM-5 of ICD-10. Dit kunnen zowel definitieve diagnoses zijn als voorlopige diagnoses.
|
||||
|
||||
**Belangrijkste gegevens:**
|
||||
- DSM-5 code (bijv. "F32.2")
|
||||
- Omschrijving (bijv. "Depressieve episode, ernstig")
|
||||
- Status (actief, in remissie, opgelost)
|
||||
- Ernst (mild, matig, ernstig)
|
||||
- Zekerheid (voorlopig, bevestigd, uitgesloten)
|
||||
- Wanneer ontstaan / wanneer opgelost
|
||||
- Wie stelde de diagnose vast
|
||||
- Bij welk contactmoment
|
||||
|
||||
**Waarom FHIR?**
|
||||
In FHIR heet dit een **Condition** resource. Dit onderscheidt tussen "encounter diagnosis" (gesteld tijdens een specifiek contact) en "problem list item" (langlopend probleem op de problemlijst).
|
||||
|
||||
Dit correspondeert met de Nederlandse **ZIB Problem**.
|
||||
|
||||
**Voorbeeld:**
|
||||
Een cliënt meldt zich aan met depressieve klachten. Na de intake wordt voorlopig "F32.2 - Depressieve episode, ernstig" vastgesteld. Na behandeling verandert de status naar "in remissie".
|
||||
|
||||
---
|
||||
|
||||
### 6. **Observaties & Metingen** (`observations`)
|
||||
**Wat is het?**
|
||||
Alle metingen, scores, risico-inschattingen en observaties tijdens de behandeling.
|
||||
|
||||
**Belangrijkste gegevens:**
|
||||
- Wat werd geobserveerd (bijv. "Suïcidaliteit", "PHQ-9 score", "Bloeddruk")
|
||||
- Uitkomst (bijv. "Hoog risico", "Score: 18 punten", "120/80")
|
||||
- Interpretatie (normaal, afwijkend hoog, afwijkend laag)
|
||||
- Wanneer gemeten
|
||||
- Door wie
|
||||
- Bij welk contactmoment
|
||||
|
||||
**Categorieën:**
|
||||
- **ROM-metingen**: PHQ-9, GAD-7, OQ-45, etc.
|
||||
- **Risico-inschattingen**: Suïcidaliteit, agressie, verwaarlozing
|
||||
- **Middelengebruik**: Alcohol, drugs, medicatie
|
||||
- **Vitale functies**: Bloeddruk, hartslag (indien relevant)
|
||||
- **Sociale anamnese**: Werk, relatie, financiën
|
||||
|
||||
**Waarom FHIR?**
|
||||
In FHIR heet dit een **Observation** resource. Dit is een zeer flexibele resource die allerlei soorten metingen kan bevatten. Door standaard codes te gebruiken (SNOMED, LOINC) kunnen deze later gedeeld worden met andere systemen.
|
||||
|
||||
Dit correspondeert met de Nederlandse **ZIB Alert** en **ZIB LaboratoryTestResult**.
|
||||
|
||||
**Voorbeeld:**
|
||||
- ROM-vragenlijst PHQ-9 ingevuld: score 18 (matig-ernstige depressie)
|
||||
- Risico-inschatting: "Suïcidale gedachten aanwezig, geen concrete plannen" → interpretatie: matig risico
|
||||
|
||||
---
|
||||
|
||||
### 7. **Medicatie** (`medication_statements`)
|
||||
**Wat is het?**
|
||||
De medicatie die de cliënt gebruikt of heeft gebruikt. Dit kan voorgeschreven zijn door de psychiater, maar ook medicatie van de huisarts.
|
||||
|
||||
**Belangrijkste gegevens:**
|
||||
- Medicijnnaam (bijv. "Sertraline 50mg tablet")
|
||||
- ATC-code (internationale medicijncode)
|
||||
- Status (actief, gestopt, gepland)
|
||||
- Dosering (bijv. "1 tablet 's ochtends")
|
||||
- Toedieningsweg (oraal, intraveneus, etc.)
|
||||
- Startdatum / stopdatum
|
||||
- Reden van gebruik (bijv. "Depressie")
|
||||
|
||||
**Waarom FHIR?**
|
||||
In FHIR heet dit een **MedicationStatement** resource. Dit registreert wat de patiënt daadwerkelijk gebruikt (niet wat voorgeschreven is - dat zou een MedicationRequest zijn).
|
||||
|
||||
Dit correspondeert met de Nederlandse **ZIB MedicationUse** en is onderdeel van het **MedicatieProces 9.0**.
|
||||
|
||||
**Let op:**
|
||||
Voor volledige medicatiegeschiedenis moet later gekoppeld worden met het Landelijk Schakelpunt (LSP) of andere medicatieservices.
|
||||
|
||||
---
|
||||
|
||||
### 8. **Behandelplannen** (`care_plans`)
|
||||
**Wat is het?**
|
||||
Het overzicht van de geplande behandeling: wat gaan we doen, waarom, en met welk doel?
|
||||
|
||||
**Belangrijkste gegevens:**
|
||||
- Titel (bijv. "Behandelplan depressie")
|
||||
- Beschrijving van de aanpak
|
||||
- Status (concept, actief, afgerond, gestopt)
|
||||
- Looptijd (startdatum - einddatum)
|
||||
- Behandeldoelen (bijv. "PHQ-9 score < 10", "Herstel dagelijks functioneren")
|
||||
- Welke diagnoses worden behandeld
|
||||
- Wie is de regiebehandelaar
|
||||
- Welk zorgteam is betrokken
|
||||
|
||||
**Waarom FHIR?**
|
||||
In FHIR heet dit een **CarePlan** resource. Dit is de container voor alle behandelactiviteiten en koppelt diagnoses aan interventies.
|
||||
|
||||
Dit correspondeert met de Nederlandse **ZIB TreatmentDirective**.
|
||||
|
||||
**Koppeltaal-integratie:**
|
||||
Dit is ook de resource die Koppeltaal gebruikt om eHealth-apps te koppelen aan de behandeling. Bijvoorbeeld: "Opdracht: 3x per week mindfulness oefening via app X".
|
||||
|
||||
---
|
||||
|
||||
### 9. **Behandelactiviteiten** (`care_plan_activities`)
|
||||
**Wat is het?**
|
||||
De concrete activiteiten binnen een behandelplan: gesprekken, medicatie, huiswerkopdrachten, ROM-metingen, etc.
|
||||
|
||||
**Belangrijkste gegevens:**
|
||||
- Omschrijving (bijv. "Individuele CGT sessies", "ROM-meting PHQ-9")
|
||||
- Status (nog niet gestart, gepland, bezig, afgerond)
|
||||
- Planning (bijv. "1x per week, 12 sessies")
|
||||
- Uitvoerende behandelaar
|
||||
- Locatie (polikliniek, online, kliniek)
|
||||
- Voortgang (vrije tekst updates)
|
||||
|
||||
**Waarom FHIR?**
|
||||
In FHIR heet dit **CarePlan.activity**. Dit is onderdeel van de CarePlan resource en beschrijft de "wat en wanneer" van de behandeling.
|
||||
|
||||
**Voorbeeld activiteiten:**
|
||||
- Individuele CGT: 1x/week, 12 sessies
|
||||
- Medicatie: Sertraline 50mg dagelijks
|
||||
- ROM-meting: Elke 4 weken PHQ-9 invullen
|
||||
- Huiswerk: Dagboek bijhouden
|
||||
|
||||
---
|
||||
|
||||
### 10. **Toestemmingen & Wilsverklaringen** (`consents`)
|
||||
**Wat is het?**
|
||||
Alle toestemmingen van de cliënt: voor behandeling, voor gegevensuitwisseling (AVG), wilsverklaringen (niet-reanimeren, euthanasie-verklaring, etc.).
|
||||
|
||||
**Belangrijkste gegevens:**
|
||||
- Type toestemming (behandeling, privacy/AVG, wilsverklaring, onderzoek)
|
||||
- Status (actief, ingetrokken, afgewezen)
|
||||
- Categorie (niet-reanimeren, advance directive, noodgevallen-only)
|
||||
- Datum en wie gaf toestemming
|
||||
- Geldigheid (startdatum - einddatum)
|
||||
- Wat mag wel/niet (toegang, delen, correctie)
|
||||
- Met wie mag gedeeld worden (specifieke behandelaren, organisaties)
|
||||
- Documenten (ondertekende verklaring als PDF)
|
||||
|
||||
**Waarom FHIR?**
|
||||
In FHIR heet dit een **Consent** resource. Dit correspondeert met de Nederlandse **ZIB AdvanceDirective**.
|
||||
|
||||
**AVG-compliance:**
|
||||
Dit is cruciaal voor AVG-naleving. Hiermee registreer je:
|
||||
- Toestemming voor behandeling (informed consent)
|
||||
- Toestemming voor delen met huisarts/andere zorgverleners
|
||||
- Intrekking van toestemming
|
||||
- Wilsverklaringen die juridisch bindend zijn
|
||||
|
||||
**Voorbeelden:**
|
||||
- "Toestemming behandeling depressie" (informed consent)
|
||||
- "Geen toestemming delen met huisarts" (privacy)
|
||||
- "Niet-reanimeren verklaring" (wilsverklaring)
|
||||
- "Toestemming opname behandelgegevens in landelijke uitwisseling" (MedMIJ)
|
||||
|
||||
---
|
||||
|
||||
### 11. **Waarschuwingen & Alerts** (`flags`)
|
||||
**Wat is het?**
|
||||
Belangrijke waarschuwingen die behandelaren **direct** moeten zien bij het openen van een dossier. Denk aan veiligheidsrisico's, allergieën, of gedragswaarschuwingen.
|
||||
|
||||
**Belangrijkste gegevens:**
|
||||
- Type waarschuwing (veiligheid, klinisch, gedrag, infectie, allergie)
|
||||
- Alert inhoud (bijv. "Suïciderisico", "Agressie naar hulpverleners")
|
||||
- Prioriteit (hoog, middel, laag)
|
||||
- Status (actief, inactief)
|
||||
- Geldigheid (startdatum - einddatum)
|
||||
- Wie maakte de alert
|
||||
- Gerelateerde diagnoses of observaties
|
||||
|
||||
**Waarom FHIR?**
|
||||
In FHIR heet dit een **Flag** resource. Dit correspondeert met de Nederlandse **ZIB Alert**.
|
||||
|
||||
**Verschil met Observations:**
|
||||
Observations zijn metingen/bevindingen. Flags zijn **actieve waarschuwingen** die aandacht vragen.
|
||||
|
||||
**Categorieën:**
|
||||
- **Safety (veiligheid)**: Suïciderisico, zelfverwaarlozing, valrisico
|
||||
- **Clinical (klinisch)**: Ernstige allergie voor medicatie, infectiegevaar
|
||||
- **Behavioral (gedrag)**: Agressie naar hulpverleners, grensoverschrijdend gedrag
|
||||
- **Administrative**: Geen-toon status (privacy), wanbetaler
|
||||
|
||||
**Voorbeeld flags:**
|
||||
- 🔴 "HOOG SUÏCIDERISICO - Concrete plannen, middelen aanwezig"
|
||||
- 🟠 "Agressie naar vrouwelijke hulpverleners - Alleen mannelijke behandelaar"
|
||||
- 🟡 "Allergie: Penicilline - anafylactische shock"
|
||||
- ⚪ "Geen toestemming contact familie - Privacy verzoek"
|
||||
|
||||
**In de UI:**
|
||||
Flags worden prominent weergegeven (rood banner bovenaan dossier) zodat ze niet gemist kunnen worden.
|
||||
|
||||
---
|
||||
|
||||
### 12. **Documenten** (`document_references`)
|
||||
**Wat is het?**
|
||||
Alle documenten in het dossier: intakeverslagen, behandelplannen, brieven aan huisarts, ROM-rapporten, etc.
|
||||
|
||||
**Belangrijkste gegevens:**
|
||||
- Type document (intakeverslag, behandelplan, brief, rapport)
|
||||
- Status (concept, definitief, vervangen)
|
||||
- Datum
|
||||
- Auteur (behandelaar)
|
||||
- Gekoppeld aan welk contactmoment
|
||||
- Content (Markdown tekst, PDF, of link naar bestand)
|
||||
|
||||
**Waarom FHIR?**
|
||||
In FHIR heet dit een **DocumentReference** resource. Dit zorgt ervoor dat documenten doorzoekbaar zijn en gekoppeld kunnen worden aan specifieke momenten in de behandeling.
|
||||
|
||||
**MedMIJ-integratie:**
|
||||
Via MedMIJ kunnen cliënten later hun eigen documenten ophalen in een persoonlijke gezondheidsomgeving (PGO-app).
|
||||
|
||||
---
|
||||
|
||||
## Hoe hangen deze onderdelen samen?
|
||||
|
||||
```
|
||||
Cliënt (Patient)
|
||||
│
|
||||
├─── heeft Toestemmingen (Consents) ⚠️ AVG-compliant
|
||||
│
|
||||
├─── heeft Waarschuwingen (Flags) 🚨 Altijd zichtbaar
|
||||
│
|
||||
└─── heeft Contactmomenten (Encounters)
|
||||
│
|
||||
├─── leidt tot Diagnoses (Conditions)
|
||||
│ └─── ondersteund door Observaties (Observations)
|
||||
│
|
||||
├─── gebruikt Medicatie (MedicationStatements)
|
||||
│
|
||||
├─── krijgt Behandelplan (CarePlan)
|
||||
│ ├─── met Doelen (Goals) 🎯 Meetbaar
|
||||
│ └─── met Activiteiten (CarePlanActivities)
|
||||
│
|
||||
└─── resulteert in Documenten (DocumentReferences)
|
||||
|
||||
Uitgevoerd door Behandelaar (Practitioner)
|
||||
Binnen Instelling (Organization)
|
||||
```
|
||||
|
||||
**Nieuwe verbindingen:**
|
||||
- **Goals** zijn gekoppeld aan **CarePlan** en **Conditions**
|
||||
- **Goals** worden gemeten via **Observations** (ROM-scores)
|
||||
- **Flags** zijn gekoppeld aan **Conditions** en **Observations** (wat veroorzaakt de alert)
|
||||
- **Consents** bepalen wie **DocumentReferences** mag inzien
|
||||
|
||||
---
|
||||
|
||||
## Waarom FHIR gebruiken?
|
||||
|
||||
### **1. Toekomstbestendig**
|
||||
FHIR is de internationale standaard voor zorggegevens. Alle moderne zorgsystemen ondersteunen dit. Door vanaf dag 1 FHIR-compliant te bouwen, kunnen we later makkelijk integreren met:
|
||||
- MedMIJ (patiëntportalen)
|
||||
- Koppeltaal (eHealth apps)
|
||||
- Landelijk Schakelpunt (LSP)
|
||||
- Andere GGZ-instellingen
|
||||
- Huisartseninformatiesystemen
|
||||
|
||||
### **2. Herbruikbaarheid**
|
||||
Elk onderdeel ("resource") kan apart uitgewisseld worden. Bijvoorbeeld:
|
||||
- Huisarts vraagt diagnoses op via FHIR API
|
||||
- Cliënt haalt eigen medicatielijst op via MedMIJ
|
||||
- eHealth app ontvangt behandelplan via Koppeltaal
|
||||
|
||||
### **3. Geen vendor lock-in**
|
||||
Omdat we een open standaard gebruiken, zijn we niet afhankelijk van één leverancier. Data kan altijd geëxporteerd en geïmporteerd worden in FHIR-formaat.
|
||||
|
||||
### **4. Bewezen technologie**
|
||||
FHIR wordt wereldwijd gebruikt door duizenden ziekenhuizen, klinieken en zorginstellingen. Alle grote EPD-leveranciers ondersteunen het.
|
||||
|
||||
---
|
||||
|
||||
## MedMIJ & Koppeltaal: Wat betekent dit?
|
||||
|
||||
### **MedMIJ - Patiëntportalen**
|
||||
MedMIJ is het Nederlandse afsprakenstelsel waarmee patiënten hun medische gegevens kunnen ophalen in een PGO-app (Persoonlijke Gezondheidsomgeving).
|
||||
|
||||
**Voor GGZ is de "Basisgegevens GGZ 2.0" specificatie relevant:**
|
||||
- 24 zorginformatiebouwstenen (ZIBs)
|
||||
- Inclusief: diagnoses, medicatie, behandelplan, contactmomenten
|
||||
|
||||
**Ons datamodel ondersteunt dit omdat:**
|
||||
- Alle velden volgen de MedMIJ FHIR profielen
|
||||
- DSM-5 codes zijn opgenomen
|
||||
- Juridische status kan vastgelegd worden
|
||||
- Medicatie volgens MedicatieProces 9.0
|
||||
|
||||
**In de toekomst kunnen we:**
|
||||
- Een FHIR API bouwen die MedMIJ-compliant is
|
||||
- Cliënten toegang geven tot hun eigen dossier via een PGO-app
|
||||
- Automatisch gegevens uitwisselen met andere zorgaanbieders
|
||||
|
||||
### **Koppeltaal - eHealth Apps**
|
||||
Koppeltaal is de standaard waarmee GGZ-instellingen eHealth apps kunnen koppelen aan hun EPD.
|
||||
|
||||
**Voorbeeld:**
|
||||
Behandelaar schrijft voor: "Doe dagelijks de mindfulness oefening in app MindDistrict"
|
||||
→ Koppeltaal zorgt dat dit automatisch in het EPD en in de app komt te staan
|
||||
→ Voortgang komt automatisch terug in het EPD
|
||||
|
||||
**Ons datamodel ondersteunt dit omdat:**
|
||||
- CarePlan resource volgt Koppeltaal specificaties
|
||||
- Activities kunnen gekoppeld worden aan externe apps
|
||||
- Status updates worden automatisch verwerkt
|
||||
|
||||
---
|
||||
|
||||
## Privacy & Beveiliging
|
||||
|
||||
### **Encryptie**
|
||||
- BSN wordt versleuteld opgeslagen
|
||||
- Communicatie via HTTPS/TLS
|
||||
|
||||
### **Toegangscontrole (RLS)**
|
||||
- Behandelaren zien alleen hun eigen cliënten
|
||||
- Cliënten kunnen later hun eigen data inzien (via patiëntenportaal)
|
||||
- Auditlog houdt bij wie wat wanneer heeft bekeken
|
||||
|
||||
### **AVG-compliance**
|
||||
- Recht op inzage: cliënt kan eigen data opvragen
|
||||
- Recht op vergetelheid: data kan verwijderd worden
|
||||
- Logging: alle acties worden gelogd
|
||||
- Bewaartermijnen: automatische archivering na X jaar
|
||||
|
||||
---
|
||||
|
||||
## Technische implementatie
|
||||
|
||||
### **Database: PostgreSQL (Supabase)**
|
||||
- Type-safe met ENUMs voor statussen
|
||||
- Automatische timestamps (created_at, updated_at)
|
||||
- Foreign keys voor relaties
|
||||
- Indexes voor performance
|
||||
|
||||
### **Veldnamen volgen FHIR**
|
||||
Bijvoorbeeld:
|
||||
- `name_family` → Patient.name.family
|
||||
- `code_code` → Condition.code.coding.code
|
||||
- `clinical_status` → Condition.clinicalStatus
|
||||
|
||||
Dit maakt het later makkelijk om FHIR JSON te genereren.
|
||||
|
||||
### **Later: FHIR API endpoints**
|
||||
```
|
||||
GET /fhir/Patient/{id}
|
||||
GET /fhir/Encounter?patient={id}
|
||||
GET /fhir/Condition?patient={id}
|
||||
GET /fhir/CarePlan?patient={id}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Wat betekent dit voor gebruikers?
|
||||
|
||||
### **Voor behandelaren:**
|
||||
- Alle data is logisch gestructureerd
|
||||
- Diagnoses zijn gekoppeld aan intake-moment
|
||||
- Behandelplan volgt automatisch uit diagnose
|
||||
- ROM-scores zijn zichtbaar in tijdlijn
|
||||
|
||||
### **Voor cliënten (in toekomst):**
|
||||
- Eigen dossier inzien via app
|
||||
- Behandelplan en afspraken zien
|
||||
- ROM-vragenlijsten invullen via app
|
||||
- Resultaten direct naar behandelaar
|
||||
|
||||
### **Voor beheerders:**
|
||||
- Export naar andere systemen is mogelijk
|
||||
- Backups bevatten FHIR-compliant data
|
||||
- Audits en rapportages zijn eenvoudig
|
||||
- Geen vendor lock-in
|
||||
|
||||
---
|
||||
|
||||
## Roadmap
|
||||
|
||||
### **Fase 1: MVP (nu)**
|
||||
✅ Database schema met alle FHIR resources
|
||||
✅ Intake → Diagnose → Behandelplan workflow
|
||||
✅ Basis toegangscontrole
|
||||
|
||||
### **Fase 2: Basis functionaliteit**
|
||||
🔲 UI voor alle resources
|
||||
🔲 AI-assistentie voor intake
|
||||
🔲 ROM-metingen integratie
|
||||
|
||||
### **Fase 3: Integraties**
|
||||
🔲 FHIR API endpoints
|
||||
🔲 MedMIJ aansluiting (patiëntportaal)
|
||||
🔲 Koppeltaal aansluiting (eHealth apps)
|
||||
🔲 LSP medicatie-uitwisseling
|
||||
|
||||
---
|
||||
|
||||
## Referenties
|
||||
|
||||
- **FHIR Specificatie:** https://hl7.org/fhir/
|
||||
- **MedMIJ GGZ:** https://informatiestandaarden.nictiz.nl/wiki/MedMij:V2020.01/OntwerpGGZ
|
||||
- **Koppeltaal:** https://www.koppeltaal.nl/
|
||||
- **ZIBs (ZorgInformatieBouwstenen):** https://zibs.nl/
|
||||
- **DSM-5 Codes:** American Psychiatric Association
|
||||
- **MedicatieProces 9.0:** https://informatiestandaarden.nictiz.nl/wiki/mp:V9
|
||||
|
||||
---
|
||||
|
||||
**Laatst bijgewerkt:** 21 november 2024
|
||||
**Auteur:** Colin Lit (ikbenlit.nl)
|
||||
**Project:** AI Speedrun - Mini-ECD
|
||||
Reference in New Issue
Block a user