feat: add duplicate email detection via auth hook with fallback

- Add before-user-created hook function for server-side duplicate email detection
- Implement fallback detection using identities array check (Supabase limitation)
- Update signUpWithPassword to detect duplicate emails via hook errors or empty identities
- Add error handling in login page to show duplicate email errors and auto-switch to login mode
- Add auth hook setup documentation and test scripts
- Add password reset and update password flows
- Add email templates for signup confirmation and password reset
This commit is contained in:
colinislit
2025-11-19 11:03:46 +01:00
parent 114e7d200d
commit 2505b27437
21 changed files with 3671 additions and 201 deletions

183
docs/AUTH_HOOK_SETUP.md Normal file
View 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`

52
docs/emails/README.md Normal file
View File

@@ -0,0 +1,52 @@
# Email Templates voor Supabase
Deze directory bevat HTML email templates die gebruikt kunnen worden in Supabase Authentication emails.
## Beschikbare Templates
### 1. `confirm-signup.html`
**Gebruik:** Email bevestiging voor nieuwe gebruikersregistratie
**Supabase variabelen:**
- `{{ .ConfirmationURL }}` - De bevestigingslink die de gebruiker moet klikken
**Waar te gebruiken in Supabase:**
- Authentication → Email Templates → Confirm signup
---
### 2. `reset-password.html`
**Gebruik:** Wachtwoord reset email
**Supabase variabelen:**
- `{{ .ConfirmationURL }}` - De reset link die de gebruiker moet klikken
**Waar te gebruiken in Supabase:**
- Authentication → Email Templates → Reset Password
---
## Hoe te gebruiken in Supabase
1. Log in op je Supabase Dashboard
2. Ga naar **Authentication****Email Templates**
3. Selecteer het juiste template type (Confirm signup of Reset Password)
4. Kopieer de volledige HTML inhoud van het corresponderende `.html` bestand
5. Plak deze in het Supabase email template veld
6. Klik op **Save**
## Design Kenmerken
- **Brand kleur:** Teal (#0D9488) - consistent met AI Speedrun branding
- **Responsive:** Werkt op desktop en mobiel
- **Email client compatibiliteit:** Gebruikt table-based layout voor maximale compatibiliteit
- **Toegankelijkheid:** Goede contrast ratios en duidelijke call-to-action buttons
- **Nederlandse taal:** Alle teksten zijn in het Nederlands
## Aanpassingen
Als je de templates wilt aanpassen:
- Kleuren kunnen worden aangepast in de inline styles (zoek naar `#0D9488` voor de primary kleur)
- Teksten kunnen direct worden aangepast in de HTML
- Let op: Supabase gebruikt Go template syntax (`{{ .VariableName }}`), dus behoud deze variabelen

View File

@@ -0,0 +1,97 @@
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Bevestig je account - AI Speedrun</title>
<!--[if mso]>
<style type="text/css">
body, table, td {font-family: Arial, sans-serif !important;}
</style>
<![endif]-->
</head>
<body style="margin: 0; padding: 0; background-color: #F8FAFC; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #F8FAFC;">
<tr>
<td align="center" style="padding: 40px 20px;">
<!-- Main Container -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="600" style="max-width: 600px; background-color: #FFFFFF; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
<!-- Header -->
<tr>
<td style="padding: 40px 40px 30px; text-align: center; border-bottom: 1px solid #E2E8F0;">
<h1 style="margin: 0; font-size: 24px; font-weight: 600; color: #0F172A; line-height: 1.3;">
Welkom bij AI Speedrun
</h1>
</td>
</tr>
<!-- Content -->
<tr>
<td style="padding: 40px 40px 30px;">
<p style="margin: 0 0 20px; font-size: 16px; line-height: 1.6; color: #475569;">
Leuk dat je meekijkt met deze speedrun!
</p>
<p style="margin: 0 0 30px; font-size: 16px; line-height: 1.6; color: #475569;">
Klik op de onderstaande knop om je emailadres te bevestigen en je account te activeren:
</p>
<!-- CTA Button -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td align="center" style="padding: 0 0 30px;">
<a href="{{ .ConfirmationURL }}" style="display: inline-block; padding: 14px 32px; background-color: #0D9488; color: #FFFFFF; text-decoration: none; border-radius: 6px; font-weight: 500; font-size: 16px; line-height: 1.5;">
Bevestig je account
</a>
</td>
</tr>
</table>
<!-- Alternative Link -->
<p style="margin: 0 0 20px; font-size: 14px; line-height: 1.6; color: #64748B;">
Werkt de knop niet? Kopieer en plak deze link in je browser:
</p>
<p style="margin: 0 0 30px; font-size: 12px; line-height: 1.6; color: #0D9488; word-break: break-all;">
{{ .ConfirmationURL }}
</p>
<!-- Security Note -->
<div style="padding: 16px; background-color: #F0FDFA; border-left: 3px solid #0D9488; border-radius: 4px;">
<p style="margin: 0; font-size: 14px; line-height: 1.6; color: #0F766E;">
<strong>Beveiliging:</strong> Deze link is 24 uur geldig. Als je deze email niet hebt aangevraagd, kun je deze negeren.
</p>
</div>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="padding: 30px 40px 40px; text-align: center; border-top: 1px solid #E2E8F0; background-color: #F8FAFC;">
<p style="margin: 0; font-size: 12px; line-height: 1.6; color: #94A3B8;">
AI Speedrun - Software on Demand<br>
Build in Public door <a href="https://ikbenlit.nl" style="color: #0D9488; text-decoration: none;">AI Speedrun</a>
</p>
</td>
</tr>
</table>
<!-- Bottom Spacing -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="600" style="max-width: 600px;">
<tr>
<td style="padding: 20px 0; text-align: center;">
<p style="margin: 0; font-size: 11px; line-height: 1.5; color: #94A3B8;">
Je ontvangt deze email omdat je je hebt geregistreerd voor AI Speedrun.
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,105 @@
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Wachtwoord opnieuw instellen - AI Speedrun</title>
<!--[if mso]>
<style type="text/css">
body, table, td {font-family: Arial, sans-serif !important;}
</style>
<![endif]-->
</head>
<body style="margin: 0; padding: 0; background-color: #F8FAFC; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #F8FAFC;">
<tr>
<td align="center" style="padding: 40px 20px;">
<!-- Main Container -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="600" style="max-width: 600px; background-color: #FFFFFF; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
<!-- Header -->
<tr>
<td style="padding: 40px 40px 30px; text-align: center; border-bottom: 1px solid #E2E8F0;">
<h1 style="margin: 0; font-size: 24px; font-weight: 600; color: #0F172A; line-height: 1.3;">
Wachtwoord opnieuw instellen
</h1>
</td>
</tr>
<!-- Content -->
<tr>
<td style="padding: 40px 40px 30px;">
<p style="margin: 0 0 20px; font-size: 16px; line-height: 1.6; color: #475569;">
Je hebt een verzoek gedaan om je wachtwoord opnieuw in te stellen voor je AI Speedrun account.
</p>
<p style="margin: 0 0 30px; font-size: 16px; line-height: 1.6; color: #475569;">
Klik op de onderstaande knop om een nieuw wachtwoord in te stellen:
</p>
<!-- CTA Button -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td align="center" style="padding: 0 0 30px;">
<a href="{{ .ConfirmationURL }}" style="display: inline-block; padding: 14px 32px; background-color: #0D9488; color: #FFFFFF; text-decoration: none; border-radius: 6px; font-weight: 500; font-size: 16px; line-height: 1.5;">
Wachtwoord opnieuw instellen
</a>
</td>
</tr>
</table>
<!-- Alternative Link -->
<p style="margin: 0 0 20px; font-size: 14px; line-height: 1.6; color: #64748B;">
Werkt de knop niet? Kopieer en plak deze link in je browser:
</p>
<p style="margin: 0 0 30px; font-size: 12px; line-height: 1.6; color: #0D9488; word-break: break-all;">
{{ .ConfirmationURL }}
</p>
<!-- Security Warning -->
<div style="padding: 16px; background-color: #FEF2F2; border-left: 3px solid #DC2626; border-radius: 4px;">
<p style="margin: 0; font-size: 14px; line-height: 1.6; color: #991B1B;">
<strong>Belangrijk:</strong> Als je deze email niet hebt aangevraagd, kun je deze veilig negeren. Je wachtwoord blijft ongewijzigd.
</p>
</div>
<!-- Expiry Note -->
<p style="margin: 20px 0 0; font-size: 14px; line-height: 1.6; color: #64748B;">
Deze link is 1 uur geldig. Na deze tijd moet je een nieuw verzoek indienen.
</p>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="padding: 30px 40px 40px; text-align: center; border-top: 1px solid #E2E8F0; background-color: #F8FAFC;">
<p style="margin: 0 0 12px; font-size: 14px; line-height: 1.6; color: #64748B;">
Vragen over je account? Neem contact met ons op via <a href="mailto:support@aispeedrun.nl" style="color: #0D9488; text-decoration: none;">support@aispeedrun.nl</a>
</p>
<p style="margin: 0; font-size: 12px; line-height: 1.6; color: #94A3B8;">
AI Speedrun - Software on Demand<br>
Build in Public door <a href="https://ikbenlit.nl" style="color: #0D9488; text-decoration: none;">AI Speedrun</a>
</p>
</td>
</tr>
</table>
<!-- Bottom Spacing -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="600" style="max-width: 600px;">
<tr>
<td style="padding: 20px 0; text-align: center;">
<p style="margin: 0; font-size: 11px; line-height: 1.5; color: #94A3B8;">
Je ontvangt deze email omdat er een wachtwoord reset is aangevraagd voor je account.
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,613 @@
# 🚀 Bouwplan — Auth Flow Incrementele Uitbreiding
**Projectnaam:** Mini EPD - Auth Features Toevoegen
**Versie:** v1.0 (Incrementeel)
**Datum:** 18-01-2025
**Auteur:** Colin (met Claude Code)
**Scope:** Alleen TOEVOEGEN wat ontbreekt (geen refactor van bestaande code)
---
## 1. Wat Hebben We Al? ✅
**Bestaande Features (BLIJVEN ZOALS ZE ZIJN):**
- ✅ Login page met split-screen design (bento grid + form)
- ✅ Magic Link login (signup + login in één)
- ✅ Password login (demo accounts)
- ✅ Quick demo button (auto-fill + submit)
- ✅ Toggle tussen magic/demo
- ✅ Success/error messaging
- ✅ Auth callback handler (`/auth/callback`)
- ✅ Logout handler (`/auth/logout`)
- ✅ Middleware (route protection)
- ✅ Supabase integration
**Wat Ontbreekt:**
- ❌ Password reset flow (forgot password)
- ❌ Set password optie (na magic link signup)
- ❌ "Wachtwoord vergeten?" link op login page
---
## 2. Wat Gaan We TOEVOEGEN?
### **Minimale Toevoegingen (Production-Ready Auth):**
| Feature | Wat | Waarom | Tijd |
|---------|-----|--------|------|
| Password Reset | Forgot password → email → new password | Must-have voor production | 2 uur |
| Set Password | Optioneel password instellen na magic link | Power users willen sneller inloggen | 1 uur |
| Links Toevoegen | "Wachtwoord vergeten?" link in login form | UX improvement | 10 min |
**Totaal: ~3 uur werk**
---
## 3. Epics & Stories (Minimaal)
| Epic ID | Titel | Wat Doen We? | Stories | Tijd |
|---------|-------|--------------|---------|------|
| E1 | Password Reset Flow | 2 nieuwe pagina's + 1 email template | 3 | 2 uur |
| E2 | Set Password (Optional) | 1 nieuwe pagina + link in onboarding | 2 | 1 uur |
| E3 | UX Polish | Link toevoegen + testing | 1 | 15 min |
**Totaal: 3 uur werk, 6 stories**
---
## 4. Epic 1 — Password Reset Flow
**Wat:** Users kunnen vergeten wachtwoord resetten via email.
| Story ID | Wat Bouwen? | Bestanden | Story Points |
|----------|-------------|-----------|--------------|
| E1.S1 | Reset request page | `app/reset-password/page.tsx` (NIEUW) | 2 |
| E1.S2 | Update password page | `app/update-password/page.tsx` (NIEUW) | 3 |
| E1.S3 | Email template configureren | Supabase dashboard (config) | 1 |
### E1.S1 - Reset Request Page
**Nieuw bestand:** `app/reset-password/page.tsx`
```typescript
'use client'
import { useState } from 'react'
import { supabase } from '@/lib/supabase/client'
import Link from 'next/link'
export default function ResetPasswordPage() {
const [email, setEmail] = useState('')
const [loading, setLoading] = useState(false)
const [sent, setSent] = useState(false)
const [error, setError] = useState('')
async function handleResetRequest(e: React.FormEvent) {
e.preventDefault()
setLoading(true)
setError('')
try {
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${window.location.origin}/update-password`
})
if (error) throw error
setSent(true)
} catch (err: any) {
setError(err.message || 'Er ging iets mis')
} finally {
setLoading(false)
}
}
if (sent) {
return (
<div className="min-h-screen flex items-center justify-center bg-slate-50 px-4">
<div className="max-w-md w-full bg-white p-8 rounded-lg shadow-sm text-center space-y-4">
<div className="text-5xl mb-4"></div>
<h1 className="text-2xl font-bold text-slate-900">Email Verstuurd!</h1>
<p className="text-slate-600">
Check je inbox voor de reset link. De link is 1 uur geldig.
</p>
<p className="text-sm text-slate-500">
Niet ontvangen? Check je spam folder.
</p>
<Link
href="/login"
className="inline-block mt-4 text-teal-600 hover:text-teal-700 font-medium"
>
Terug naar login
</Link>
</div>
</div>
)
}
return (
<div className="min-h-screen flex items-center justify-center bg-slate-50 px-4">
<div className="max-w-md w-full bg-white p-8 rounded-lg shadow-sm">
<div className="mb-6">
<h1 className="text-2xl font-bold text-slate-900 mb-2">
Wachtwoord Vergeten?
</h1>
<p className="text-slate-600">
Geen probleem! Vul je email in en we sturen je een reset link.
</p>
</div>
{error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-800 text-sm">
{error}
</div>
)}
<form onSubmit={handleResetRequest} className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Email
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="jouw@email.nl"
required
className="w-full px-4 py-2.5 border border-slate-300 rounded-lg focus:ring-2 focus:ring-teal-500 focus:border-teal-500"
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full bg-teal-600 hover:bg-teal-700 text-white font-medium py-2.5 px-4 rounded-lg transition-colors disabled:opacity-50"
>
{loading ? 'Verzenden...' : 'Stuur Reset Link'}
</button>
</form>
<div className="mt-6 text-center">
<Link
href="/login"
className="text-sm text-slate-600 hover:text-slate-900"
>
Terug naar login
</Link>
</div>
</div>
</div>
)
}
```
### E1.S2 - Update Password Page
**Nieuw bestand:** `app/update-password/page.tsx`
```typescript
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { supabase } from '@/lib/supabase/client'
export default function UpdatePasswordPage() {
const router = useRouter()
const [password, setPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [success, setSuccess] = useState(false)
async function handleUpdatePassword(e: React.FormEvent) {
e.preventDefault()
setError('')
// Validation
if (password !== confirmPassword) {
setError('Wachtwoorden komen niet overeen')
return
}
if (password.length < 8) {
setError('Wachtwoord moet minimaal 8 tekens zijn')
return
}
setLoading(true)
try {
const { error } = await supabase.auth.updateUser({
password
})
if (error) throw error
setSuccess(true)
setTimeout(() => router.push('/login'), 2000)
} catch (err: any) {
setError(err.message || 'Er ging iets mis')
} finally {
setLoading(false)
}
}
if (success) {
return (
<div className="min-h-screen flex items-center justify-center bg-slate-50 px-4">
<div className="max-w-md w-full bg-white p-8 rounded-lg shadow-sm text-center space-y-4">
<div className="text-5xl mb-4"></div>
<h1 className="text-2xl font-bold text-slate-900">Wachtwoord Gewijzigd!</h1>
<p className="text-slate-600">
Je wordt doorgestuurd naar de login pagina...
</p>
</div>
</div>
)
}
return (
<div className="min-h-screen flex items-center justify-center bg-slate-50 px-4">
<div className="max-w-md w-full bg-white p-8 rounded-lg shadow-sm">
<div className="mb-6">
<h1 className="text-2xl font-bold text-slate-900 mb-2">
Nieuw Wachtwoord Instellen
</h1>
<p className="text-slate-600">
Kies een sterk wachtwoord (minimaal 8 tekens).
</p>
</div>
{error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-800 text-sm">
{error}
</div>
)}
<form onSubmit={handleUpdatePassword} className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Nieuw Wachtwoord
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
minLength={8}
className="w-full px-4 py-2.5 border border-slate-300 rounded-lg focus:ring-2 focus:ring-teal-500 focus:border-teal-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Bevestig Wachtwoord
</label>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="••••••••"
required
minLength={8}
className="w-full px-4 py-2.5 border border-slate-300 rounded-lg focus:ring-2 focus:ring-teal-500 focus:border-teal-500"
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full bg-teal-600 hover:bg-teal-700 text-white font-medium py-2.5 px-4 rounded-lg transition-colors disabled:opacity-50"
>
{loading ? 'Wachtwoord Wijzigen...' : 'Wachtwoord Wijzigen'}
</button>
</form>
</div>
</div>
)
}
```
### E1.S3 - Email Template Configureren
**Waar:** Supabase Dashboard → Authentication → Email Templates → Reset Password
**Template:**
```html
<h2>Wachtwoord Resetten</h2>
<p>Je hebt een wachtwoord reset aangevraagd voor je Mini EPD account.</p>
<a href="{{ .ConfirmationURL }}"
style="display: inline-block; background: #0d9488; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; margin: 16px 0;">
Reset Wachtwoord
</a>
<p style="color: #64748b; font-size: 14px;">
Deze link is 1 uur geldig. Heb je deze reset niet aangevraagd? Negeer deze email.
</p>
```
---
## 5. Epic 2 — Set Password (Optional)
**Wat:** Na magic link signup kunnen users een password instellen.
| Story ID | Wat Bouwen? | Bestanden | Story Points |
|----------|-------------|-----------|--------------|
| E2.S1 | Set password page | `app/set-password/page.tsx` (NIEUW) | 2 |
| E2.S2 | Link toevoegen | Update `app/auth/callback/route.ts` | 1 |
### E2.S1 - Set Password Page
**Nieuw bestand:** `app/set-password/page.tsx`
```typescript
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { supabase } from '@/lib/supabase/client'
export default function SetPasswordPage() {
const router = useRouter()
const [password, setPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
async function handleSetPassword(e: React.FormEvent) {
e.preventDefault()
setError('')
if (password !== confirmPassword) {
setError('Wachtwoorden komen niet overeen')
return
}
if (password.length < 8) {
setError('Wachtwoord minimaal 8 tekens')
return
}
setLoading(true)
try {
const { error } = await supabase.auth.updateUser({ password })
if (error) throw error
router.push('/epd/clients')
} catch (err: any) {
setError(err.message)
} finally {
setLoading(false)
}
}
function handleSkip() {
router.push('/epd/clients')
}
return (
<div className="min-h-screen flex items-center justify-center bg-slate-50 px-4">
<div className="max-w-md w-full bg-white p-8 rounded-lg shadow-sm">
<h1 className="text-2xl font-bold text-slate-900 mb-2">
Stel een Wachtwoord In (Optioneel)
</h1>
<p className="text-slate-600 mb-6">
Met een wachtwoord kun je sneller inloggen zonder magic link.
</p>
{error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-800 text-sm">
{error}
</div>
)}
<form onSubmit={handleSetPassword} className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Wachtwoord
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Min. 8 tekens"
minLength={8}
className="w-full px-4 py-2.5 border border-slate-300 rounded-lg focus:ring-2 focus:ring-teal-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Bevestig Wachtwoord
</label>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="••••••••"
minLength={8}
className="w-full px-4 py-2.5 border border-slate-300 rounded-lg focus:ring-2 focus:ring-teal-500"
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full bg-teal-600 hover:bg-teal-700 text-white font-medium py-2.5 px-4 rounded-lg disabled:opacity-50"
>
{loading ? 'Instellen...' : 'Wachtwoord Instellen'}
</button>
<button
type="button"
onClick={handleSkip}
className="w-full text-slate-600 hover:text-slate-900 text-sm font-medium py-2"
>
Sla over (blijf magic link gebruiken)
</button>
</form>
</div>
</div>
)
}
```
### E2.S2 - Update Callback Handler
**Bestand:** `app/auth/callback/route.ts`
**Wat wijzigen:**
```typescript
// TOEVOEGEN aan einde van success flow:
if (data.user) {
// Check if new user (first login)
const isNewUser = new Date(data.user.created_at).getTime() ===
new Date(data.user.last_sign_in_at || '').getTime()
if (isNewUser) {
// Optie: redirect naar set-password voor nieuwe users
return NextResponse.redirect(`${requestUrl.origin}/set-password`)
}
// Bestaande redirect naar dashboard
return NextResponse.redirect(`${requestUrl.origin}/epd/clients`)
}
```
---
## 6. Epic 3 — UX Polish
**Wat:** Kleine verbeteringen aan bestaande login page.
| Story ID | Wat Doen? | Bestand | Story Points |
|----------|-----------|---------|--------------|
| E3.S1 | "Wachtwoord vergeten?" link toevoegen | `app/login/page.tsx` | 1 |
### E3.S1 - Forgot Password Link
**Bestand:** `app/login/page.tsx` (line ~346)
**TOEVOEGEN na password input field:**
```typescript
{/* Password Input */}
<div>
<label htmlFor="password" className="block text-sm font-medium text-slate-700 mb-1">
Password
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
className="w-full px-4 py-2.5 border border-slate-300 rounded-lg focus:ring-2 focus:ring-slate-400 focus:border-slate-400"
/>
{/* NIEUW: Forgot password link */}
<div className="mt-1 text-right">
<a
href="/reset-password"
className="text-xs text-slate-600 hover:text-teal-600"
>
Wachtwoord vergeten?
</a>
</div>
</div>
```
---
## 7. Checklist - Wat Moet Er Gebeuren?
### **Nieuwe Bestanden Aanmaken:**
- [ ] `app/reset-password/page.tsx` (E1.S1)
- [ ] `app/update-password/page.tsx` (E1.S2)
- [ ] `app/set-password/page.tsx` (E2.S1)
### **Bestaande Bestanden Wijzigen:**
- [ ] `app/login/page.tsx` - Voeg "Wachtwoord vergeten?" link toe (E3.S1)
- [ ] `app/auth/callback/route.ts` - Optionele redirect naar set-password (E2.S2)
### **Supabase Configuratie:**
- [ ] Email template configureren (E1.S3)
- [ ] Test reset email wordt verstuurd
### **Testing:**
- [ ] Test forgot password flow (request → email → reset)
- [ ] Test set password na magic link signup
- [ ] Test alle links werken
- [ ] Test error handling (wrong email, weak password, etc.)
---
## 8. Implementatie Volgorde
**Stap 1:** Password Reset (E1) - 2 uur
1. Maak `reset-password/page.tsx`
2. Maak `update-password/page.tsx`
3. Configureer email template in Supabase
4. Test hele flow
**Stap 2:** Set Password (E2) - 1 uur
1. Maak `set-password/page.tsx`
2. Update `auth/callback/route.ts`
3. Test flow
**Stap 3:** UX Polish (E3) - 15 min
1. Voeg "Wachtwoord vergeten?" link toe
2. Final testing
**Totaal: ~3 uur werk**
---
## 9. Testing Checklist
| Test Case | Scenario | Expected |
|-----------|----------|----------|
| TC1 | Click "Wachtwoord vergeten?" | Redirect naar /reset-password |
| TC2 | Submit valid email for reset | Email sent, success message |
| TC3 | Click reset link in email | Redirect naar /update-password |
| TC4 | Set new password (valid) | Success, redirect to login |
| TC5 | Set new password (mismatch) | Error: "Wachtwoorden komen niet overeen" |
| TC6 | Set new password (too short) | Error: "Minimaal 8 tekens" |
| TC7 | Magic link signup (new user) | Redirect naar /set-password |
| TC8 | Skip set password | Redirect naar /epd/clients |
| TC9 | Set password after signup | Success, redirect to dashboard |
---
## 10. Wat NIET Doen
**Bestaande login page NIET refactoren**
**GEEN nieuwe UI components maken** (gebruik bestaande styling)
**GEEN email verification** (nice-to-have, niet kritiek)
**GEEN OAuth/SSO** (overkill voor prototype)
**GEEN complexe state management** (useState is prima)
---
## 11. Definition of Done
**E1-E3 compleet wanneer:**
- 3 nieuwe pagina's werken (reset-password, update-password, set-password)
- "Wachtwoord vergeten?" link zichtbaar op login page
- Email template geconfigureerd in Supabase
- Alle flows getest en werkend
- Geen breaking changes aan bestaande auth
- Git commit: `feat: Add password reset and set password flows`
---
**Status:** ⏳ Ready for Implementation
**Geschatte Tijd:** 3 uur
**Next Step:** Maak eerst `app/reset-password/page.tsx`

View File

@@ -0,0 +1,502 @@
# 🔐 Bouwplan — Auth Hook voor Duplicate Email Detection
**Projectnaam:** Mini EPD - Auth Hook Implementatie
**Versie:** v1.0 (Code-First Approach)
**Datum:** 18-01-2025
**Auteur:** Colin (met Claude Code)
**Scope:** Server-side duplicate email detection via Auth Hook - Volledig in code waar mogelijk
---
## 1. Filosofie: Code-First Approach
**Principe:**
- ✅ Alles wat mogelijk is in code/migrations → komt in code
- ✅ Geen handmatige Dashboard configuratie waar mogelijk
- ✅ Flexibel voor toekomstige auth provider switches
- ✅ Herhaalbaar en version controlled
**Realiteit:**
- Functie: Volledig in SQL migrations ✅
- Hook link: Vereist Dashboard configuratie (Supabase limietatie) ⚠️
- Oplossing: Setup script + duidelijke documentatie
---
## 2. Probleem Statement
**Huidige Situatie:**
- Bij signup met bestaand emailadres (bijv. `colin.lit@gmail.com`) krijgt gebruiker succesmelding
- Geen email wordt verzonden (omdat account al bestaat)
- Gebruiker denkt dat account is aangemaakt maar krijgt geen verificatie email
- Verwarrende UX
**Root Cause:**
- Supabase geeft geen error bij duplicate email als "Email confirmation" AAN staat
- Dit is een security feature (email enumeration prevention)
- Client-side detection is onbetrouwbaar (werkt alleen als password correct is)
**Oplossing:**
- Implementeer `before-user-created` Auth Hook
- Server-side check of email al bestaat in database
- Return custom error message als email al geregistreerd is
---
## 3. Wat Gaan We Bouwen?
| Component | Wat | Waar | Status |
|-----------|-----|------|--------|
| Postgres Function | Database functie | `supabase/migrations/` | ✅ Volledig in code |
| Setup Script | Automatiseer hook link | `scripts/setup-auth-hook.ts` | ✅ Code-based |
| README | Documentatie | `docs/AUTH_HOOK_SETUP.md` | ✅ Documentatie |
| Error Handling | Client-side updates | `app/login/page.tsx` | ✅ Code |
**Totaal: ~2.5 uur werk**
---
## 4. Technische Keuze: Postgres Function vs HTTP Edge Function
### **Optie A: Postgres Function (AANBEVOLEN) ✅**
**Voordelen:**
- ✅ Geen extra dependencies nodig
- ✅ Direct database access (sneller)
- ✅ Makkelijk te onderhouden (SQL in migrations)
- ✅ Geen extra hosting/configuration
- ✅ Past bij bestaande setup (je hebt al migrations)
**Nadelen:**
- ⚠️ Moet SQL schrijven (maar is simpel)
### **Optie B: HTTP Edge Function**
**Voordelen:**
- ✅ TypeScript (bekende taal)
- ✅ Meer flexibiliteit voor complexe logica
**Nadelen:**
- ❌ Extra setup nodig (Supabase Functions)
- ❌ Extra deployment step
- ❌ Meer complexiteit
**Beslissing: Postgres Function (Optie A)**
---
## 5. Implementatie Plan
### **Epic 1: Volledig Code-Based Setup**
| Story ID | Wat Bouwen? | Bestanden | Story Points |
|----------|-------------|-----------|--------------|
| E1.S1 | Create hook function | `supabase/migrations/YYYYMMDDHHMMSS_auth_hook_duplicate_email.sql` | 2 |
| E1.S2 | Setup script voor hook link | `scripts/setup-auth-hook.ts` | 2 |
| E1.S3 | Documentatie | `docs/AUTH_HOOK_SETUP.md` | 1 |
| E1.S4 | Update client error handling | `app/login/page.tsx` | 1 |
---
## 6. Epic 1 — Implementatie Details
### **E1.S1 - Create Hook Function (Volledig in Migrations)**
**Bestand:** `supabase/migrations/20250118120000_auth_hook_duplicate_email.sql`
**Wat doet de 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)
**SQL Code:**
```sql
-- ============================================================================
-- Auth Hook: Duplicate Email Detection
-- ============================================================================
-- Deze functie wordt aangeroepen VOOR een nieuwe user wordt aangemaakt.
-- Checkt of het emailadres al bestaat en blokkeert signup indien nodig.
--
-- Hook Type: before-user-created
-- Flexibel: Werkt met elke auth provider die Postgres functies ondersteunt
-- ============================================================================
create or replace function public.hook_check_duplicate_email(event jsonb)
returns jsonb
language plpgsql
security definer
set search_path = public, auth
as $$
declare
user_email text;
email_exists boolean;
begin
-- Extract email from event payload
user_email := event->'user'->>'email';
-- Validate email is not null or empty
if user_email is null or trim(user_email) = '' then
return jsonb_build_object(
'error', jsonb_build_object(
'message', 'Email adres is verplicht.',
'http_code', 400
)
);
end if;
-- Normalize email (lowercase, trim) for consistent checking
user_email := lower(trim(user_email));
-- Check if email already exists in auth.users (case-insensitive)
select exists(
select 1
from auth.users
where lower(email) = user_email
) into email_exists;
-- If email exists, reject signup with error
if email_exists then
return jsonb_build_object(
'error', jsonb_build_object(
'message', 'Dit emailadres is al geregistreerd. Probeer in te loggen of gebruik "Wachtwoord vergeten?".',
'http_code', 400
)
);
end if;
-- Email doesn't exist, allow signup
return '{}'::jsonb;
end;
$$;
-- Grant execute permission to Supabase Auth service
grant execute
on function public.hook_check_duplicate_email
to supabase_auth_admin;
-- Revoke from other roles (security)
revoke execute
on function public.hook_check_duplicate_email
from authenticated, anon, public;
-- Add comment for documentation
comment on function public.hook_check_duplicate_email is
'Auth hook voor duplicate email detection. Wordt aangeroepen via Supabase Auth Hooks (before-user-created).';
```
**Waarom `set search_path`?**
- Zorgt dat `auth.users` correct wordt gevonden
- Voorkomt "table not found" errors
- Best practice voor security definer functies
**MVP Verbeteringen:**
- ✅ NULL/empty email check (voorkomt crashes)
- ✅ Email normalisatie (lowercase + trim voor consistentie)
- ✅ Case-insensitive duplicate check (Email@Example.com = email@example.com)
---
### **E1.S2 - Setup Script (Automatiseer Hook Link)**
**Bestand:** `scripts/setup-auth-hook.ts`
**Doel:** Automatiseer hook link configuratie waar mogelijk
```typescript
#!/usr/bin/env tsx
/**
* Setup Script: Configure Auth Hook Link
*
* Dit script configureert de link tussen Supabase Auth Hook en onze Postgres functie.
*
* Helaas ondersteunt Supabase Management API nog geen Auth Hooks configuratie,
* dus dit script geeft instructies voor handmatige configuratie.
*
* In de toekomst kan dit worden geautomatiseerd zodra Supabase API dit ondersteunt.
*/
import { supabaseAdmin } from '@/lib/supabase/server'
async function setupAuthHook() {
console.log('🔐 Auth Hook Setup Script\n')
// Check if function exists by trying to call it with a test payload
// This is more reliable than querying pg_proc directly
const { error } = await supabaseAdmin.rpc('hook_check_duplicate_email', {
event: JSON.stringify({
user: {
email: 'test@example.com'
}
})
})
// If function doesn't exist, we'll get a "function does not exist" error
// If it exists but returns an error, that's fine - we just want to check existence
if (error && error.message?.includes('does not exist')) {
console.error('❌ Function niet gevonden:', error.message)
console.log('\n📝 Stap 1: Run eerst de migration:')
console.log(' supabase db push')
console.log(' Of via Supabase Dashboard → SQL Editor')
return
}
console.log('✅ Function exists: hook_check_duplicate_email')
console.log('\n📝 Stap 2: Configureer hook link in Supabase Dashboard:')
console.log(' 1. Ga naar: https://supabase.com/dashboard/project/YOUR_PROJECT/auth/hooks')
console.log(' 2. Klik "Add hook"')
console.log(' 3. Selecteer:')
console.log(' - Hook Type: before-user-created')
console.log(' - Hook Name: check-duplicate-email')
console.log(' - Hook Function: hook_check_duplicate_email')
console.log(' - Hook URL: (leeg laten)')
console.log(' 4. Klik "Save"')
console.log('\n💡 Tip: Deze stap moet handmatig omdat Supabase Management API')
console.log(' Auth Hooks configuratie nog niet ondersteunt.')
console.log('\n✅ Setup compleet! Test met: pnpm run test:auth-hook')
}
setupAuthHook().catch(console.error)
```
**Toevoegen aan `package.json`:**
```json
{
"scripts": {
"setup:auth-hook": "tsx scripts/setup-auth-hook.ts"
}
}
```
---
### **E1.S3 - Documentatie**
**Bestand:** `docs/AUTH_HOOK_SETUP.md`
```markdown
# 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.
## Setup (Eerste Keer)
### Stap 1: Deploy Migration
\`\`\`bash
# Via Supabase CLI (aanbevolen)
supabase db push
# Of via Dashboard
# Ga naar SQL Editor → Run migration file
\`\`\`
### Stap 2: Configureer Hook Link
**Helaas moet dit handmatig via Dashboard** (Supabase ondersteunt dit nog niet via API):
1. Ga naar: [Supabase Dashboard → Auth → Hooks](https://supabase.com/dashboard/project/_/auth/hooks)
2. Klik "Add hook"
3. Vul in:
- **Hook Type:** \`before-user-created\`
- **Hook Name:** \`check-duplicate-email\`
- **Hook Function:** \`hook_check_duplicate_email\`
- **Hook URL:** (leeg laten)
4. Klik "Save"
### Stap 3: Test
\`\`\`bash
pnpm run setup:auth-hook
\`\`\`
## Herhaalbaarheid
- ✅ Functie code staat in migrations (version controlled)
- ⚠️ Hook link moet per omgeving handmatig worden geconfigureerd
- 📝 Documentatie staat in Git
## 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
## 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
```
---
### **E1.S4 - Update Client Error Handling**
**Bestand:** `app/login/page.tsx`
**Wat wijzigen:**
- Update error handling om hook error messages te tonen
- Hook errors komen binnen via `error.message`
- Verbeter UX door automatisch naar login mode te switchen bij duplicate email
**Verbeterde code:**
```typescript
catch (error: any) {
const errorMessage = error.message || 'Er ging iets mis. Probeer opnieuw.'
// Check if it's a duplicate email error from hook
if (errorMessage.includes('al geregistreerd')) {
setMessage({
type: 'error',
text: errorMessage
})
// Switch to login mode after 2 seconds
setTimeout(() => {
setMode('login')
setPassword('')
setConfirmPassword('')
}, 2000)
} else {
setMessage({
type: 'error',
text: errorMessage
})
}
}
```
---
## 7. Implementatie Volgorde
**Stap 1: Create Migration** (30 min)
1. Maak `supabase/migrations/20250118120000_auth_hook_duplicate_email.sql`
2. Deploy via `supabase db push` of Dashboard
**Stap 2: Create Setup Script** (30 min)
1. Maak `scripts/setup-auth-hook.ts`
2. Test script: `pnpm run setup:auth-hook`
**Stap 3: Create Documentation** (15 min)
1. Maak `docs/AUTH_HOOK_SETUP.md`
2. Update main README met link
**Stap 4: Configure Hook Link** (5 min)
1. Run setup script voor instructies
2. Volg Dashboard stappen
**Stap 5: Update Client** (15 min)
1. Verbeter error handling
2. Test duplicate email scenario
**Stap 6: Testing** (30 min)
1. Test signup met nieuw email → moet werken
2. Test signup met bestaand email → moet error geven
3. Test signup met bestaand email + verkeerd password → moet error geven
4. Test signup met bestaand email + correct password → moet error geven (want account bestaat al)
**Totaal: ~2.5 uur werk**
---
## 8. Testing Checklist
| 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: "Dit emailadres is al geregistreerd..." |
| 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) |
---
## 9. Voordelen van Code-First Aanpak
**Version Control**: Functie code staat in Git
**Herhaalbaar**: Migrations kunnen opnieuw worden gedraaid
**Documentatie**: Alles staat in code en docs
**Flexibel**: Makkelijk te migreren naar andere auth providers
**Team-vriendelijk**: Iedereen ziet wat er gebeurt
**CI/CD Ready**: Migrations kunnen geautomatiseerd worden
**Server-side**: Veilig, kan niet worden omzeild door client
**Betrouwbaar**: Werkt altijd, ongeacht password
**Duidelijke UX**: Gebruiker krijgt direct feedback
**Security**: Behoudt email enumeration protection voor andere scenario's
**Performance**: Direct database check, geen extra HTTP calls
---
## 10. Limitaties & Workarounds
**Limitaties:**
- ⚠️ Hook link configuratie kan niet volledig in code (Supabase limietatie)
- ⚠️ Setup script geeft alleen instructies (geen API beschikbaar)
**Workarounds:**
- ✅ Duidelijke documentatie voor handmatige stap
- ✅ Setup script valideert dat functie bestaat
- ✅ Toekomst-proof: zodra API beschikbaar is, kunnen we automatiseren
---
## 11. Alternatieve Aanpakken (Niet Aanbevolen)
### **Optie X: Email Confirmation UIT zetten**
- ❌ Minder secure (email enumeration mogelijk)
- ✅ Wel makkelijker duplicate detection
- **Niet aanbevolen voor production**
### **Optie Y: Client-side login check (huidige aanpak)**
- ⚠️ Werkt alleen als password correct is
- ⚠️ Extra API call
- ⚠️ Kan niet onderscheiden tussen "nieuw account" en "verkeerd password"
- **Acceptabel voor prototype, niet voor production**
---
## 12. Definition of Done
**Epic 1 compleet wanneer:**
- Postgres functie bestaat en werkt (in migrations)
- Setup script werkt en geeft duidelijke instructies
- Documentatie compleet en up-to-date
- Hook geconfigureerd in Supabase Dashboard
- Test signup met bestaand email geeft error
- Test signup met nieuw email werkt normaal
- Error messages zijn gebruiksvriendelijk
- Geen breaking changes aan bestaande auth flow
- Git commit: `feat: Add auth hook for duplicate email detection`
---
## 13. MVP vs Production
**Wat zit er in MVP (huidige plan):**
- ✅ Duplicate email detection
- ✅ NULL/empty email validation
- ✅ Case-insensitive matching
- ✅ Email normalisatie (lowercase + trim)
- ✅ Duidelijke error messages
- ✅ Setup script voor validatie
**Wat komt later (Production - Optioneel):**
- [ ] Rate limiting op hook (voorkom abuse)
- [ ] Logging tabel voor duplicate attempts
- [ ] Analytics: hoeveel duplicate attempts per dag?
- [ ] Monitoring/alerting voor hook failures
- [ ] Custom error messages per scenario
---
**Status:** ⏳ Ready for Implementation
**Geschatte Tijd:** 2.5 uur
**Next Step:** Maak migration file `supabase/migrations/20250118120000_auth_hook_duplicate_email.sql`