-
-
- π― Demo Account Login
-
-
-
-
- {/* Demo Credentials Info */}
-
-
- π Demo Credentials:
-
-
-
Email: demo@mini-ecd.demo
-
Password: Demo2024!
-
-
-
-
+
+ {/* Divider */}
+
+
+ {/* Quick Demo Button */}
+
{/* Footer */}
diff --git a/app/reset-password/page.tsx b/app/reset-password/page.tsx
new file mode 100644
index 0000000..0cdb059
--- /dev/null
+++ b/app/reset-password/page.tsx
@@ -0,0 +1,105 @@
+'use client'
+
+import { useState } from 'react'
+import { resetPasswordForEmail } from '@/lib/auth/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 {
+ await resetPasswordForEmail(email)
+ setSent(true)
+ } catch (err: any) {
+ setError(err.message || 'Er ging iets mis')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ if (sent) {
+ return (
+
+
+
βοΈ
+
Email Verstuurd!
+
+ Check je inbox voor de reset link. De link is 1 uur geldig.
+
+
+ Niet ontvangen? Check je spam folder.
+
+
+ β Terug naar login
+
+
+
+ )
+ }
+
+ return (
+
+
+
+
+ Wachtwoord Vergeten?
+
+
+ Geen probleem! Vul je email in en we sturen je een reset link.
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+
+ β Terug naar login
+
+
+
+
+ )
+}
+
diff --git a/app/set-password/page.tsx b/app/set-password/page.tsx
new file mode 100644
index 0000000..3b9cd06
--- /dev/null
+++ b/app/set-password/page.tsx
@@ -0,0 +1,108 @@
+'use client'
+
+import { useState } from 'react'
+import { useRouter } from 'next/navigation'
+import { updateUserPassword } from '@/lib/auth/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 {
+ await updateUserPassword(password)
+ router.push('/epd/clients')
+ } catch (err: any) {
+ setError(err.message)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ function handleSkip() {
+ router.push('/epd/clients')
+ }
+
+ return (
+
+
+
+ Stel een Wachtwoord In (Optioneel)
+
+
+ Met een wachtwoord kun je sneller inloggen zonder magic link.
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+ )
+}
diff --git a/app/update-password/page.tsx b/app/update-password/page.tsx
new file mode 100644
index 0000000..e4a4163
--- /dev/null
+++ b/app/update-password/page.tsx
@@ -0,0 +1,118 @@
+'use client'
+
+import { useState } from 'react'
+import { useRouter } from 'next/navigation'
+import { updateUserPassword } from '@/lib/auth/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 {
+ await updateUserPassword(password)
+ setSuccess(true)
+ setTimeout(() => router.push('/login'), 2000)
+ } catch (err: any) {
+ setError(err.message || 'Er ging iets mis')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ if (success) {
+ return (
+
+
+
β
+
Wachtwoord Gewijzigd!
+
+ Je wordt doorgestuurd naar de login pagina...
+
+
+
+ )
+ }
+
+ return (
+
+
+
+
+ Nieuw Wachtwoord Instellen
+
+
+ Kies een sterk wachtwoord (minimaal 8 tekens).
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+ )
+}
+
diff --git a/components/ui/bento-grid.tsx b/components/ui/bento-grid.tsx
index 342a957..ed41032 100644
--- a/components/ui/bento-grid.tsx
+++ b/components/ui/bento-grid.tsx
@@ -35,7 +35,7 @@ const BentoCard = ({
name: string;
className: string;
background: ReactNode;
- Icon: any;
+ Icon?: any;
description: string;
href: string;
cta: string;
@@ -53,7 +53,7 @@ const BentoCard = ({
>
{background}
-
+ {Icon &&
}
{name}
diff --git a/docs/AUTH_HOOK_SETUP.md b/docs/AUTH_HOOK_SETUP.md
new file mode 100644
index 0000000..477388c
--- /dev/null
+++ b/docs/AUTH_HOOK_SETUP.md
@@ -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`
diff --git a/docs/emails/README.md b/docs/emails/README.md
new file mode 100644
index 0000000..b33bcc0
--- /dev/null
+++ b/docs/emails/README.md
@@ -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
+
diff --git a/docs/emails/confirm-signup.html b/docs/emails/confirm-signup.html
new file mode 100644
index 0000000..d7a55d7
--- /dev/null
+++ b/docs/emails/confirm-signup.html
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
Bevestig je account - AI Speedrun
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Welkom bij AI Speedrun
+
+ |
+
+
+
+
+ |
+
+ Leuk dat je meekijkt met deze speedrun!
+
+
+
+ Klik op de onderstaande knop om je emailadres te bevestigen en je account te activeren:
+
+
+
+
+
+
+
+ Werkt de knop niet? Kopieer en plak deze link in je browser:
+
+
+ {{ .ConfirmationURL }}
+
+
+
+
+
+ Beveiliging: Deze link is 24 uur geldig. Als je deze email niet hebt aangevraagd, kun je deze negeren.
+
+
+ |
+
+
+
+
+ |
+
+ AI Speedrun - Software on Demand
+ Build in Public door AI Speedrun
+
+ |
+
+
+
+
+
+
+
+ |
+
+ Je ontvangt deze email omdat je je hebt geregistreerd voor AI Speedrun.
+
+ |
+
+
+
+ |
+
+
+
+
+
diff --git a/docs/emails/reset-password.html b/docs/emails/reset-password.html
new file mode 100644
index 0000000..8e1eeaf
--- /dev/null
+++ b/docs/emails/reset-password.html
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
Wachtwoord opnieuw instellen - AI Speedrun
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Wachtwoord opnieuw instellen
+
+ |
+
+
+
+
+ |
+
+ Je hebt een verzoek gedaan om je wachtwoord opnieuw in te stellen voor je AI Speedrun account.
+
+
+
+ Klik op de onderstaande knop om een nieuw wachtwoord in te stellen:
+
+
+
+
+
+
+
+ Werkt de knop niet? Kopieer en plak deze link in je browser:
+
+
+ {{ .ConfirmationURL }}
+
+
+
+
+
+ Belangrijk: Als je deze email niet hebt aangevraagd, kun je deze veilig negeren. Je wachtwoord blijft ongewijzigd.
+
+
+
+
+
+ Deze link is 1 uur geldig. Na deze tijd moet je een nieuw verzoek indienen.
+
+ |
+
+
+
+
+ |
+
+ Vragen over je account? Neem contact met ons op via support@aispeedrun.nl
+
+
+ AI Speedrun - Software on Demand
+ Build in Public door AI Speedrun
+
+ |
+
+
+
+
+
+
+
+ |
+
+ Je ontvangt deze email omdat er een wachtwoord reset is aangevraagd voor je account.
+
+ |
+
+
+
+ |
+
+
+
+
+
diff --git a/docs/specs/archive/bouwplan-auth-flow-complete-v1.0.md b/docs/specs/archive/bouwplan-auth-flow-complete-v1.0.md
new file mode 100644
index 0000000..3be9394
--- /dev/null
+++ b/docs/specs/archive/bouwplan-auth-flow-complete-v1.0.md
@@ -0,0 +1,1055 @@
+# π Bouwplan β Volledige Auth Flow + Demo Account
+
+**Projectnaam:** Mini EPD Demo Platform - Complete Authenticatie Systeem
+**Versie:** v1.0
+**Datum:** 18-01-2025
+**Auteur:** Colin (met Claude Code)
+**Scope:** Production-ready auth flow met signup, login, password reset + demo account convenience
+
+---
+
+## 1. Doel en Context
+
+π― **Doel:** Een complete, production-ready authenticatie flow bouwen die professionele development skills showcaset, met een demo account voor quick access.
+
+π **Toelichting:**
+
+**Portfolio Waarde:**
+- Demonstreert full-stack auth expertise
+- Security best practices (password hashing, email verification, rate limiting)
+- Professional UX (duidelijke error states, loading indicators, success feedback)
+- Production-ready mindset (edge cases, error handling, rollback scenarios)
+
+**Feature Scope:**
+1. β
**Signup Flow** - Magic Link (passwordless onboarding)
+2. β
**Login Flow** - Email + Password (returning users)
+3. β
**Password Reset** - Forgot password β Email β New password
+4. β
**Email Verification** - Confirm email na signup
+5. β
**Demo Account** - One-click quick access
+6. β
**Session Management** - Auto-refresh, secure cookies, logout
+
+**User Journeys:**
+- **Recruiter/Prospect:** Quick Demo β Impressed β Account aanmaken
+- **New User:** Email β Magic Link β Account created β Onboarding
+- **Returning User:** Email + Password β Dashboard
+- **Forgot Password:** Reset link β New password β Login
+
+---
+
+## 2. Uitgangspunten
+
+### 2.1 Technische Stack
+
+**Bestaand (blijft):**
+- Next.js 16.0.1 + React 19 + TypeScript
+- Supabase Auth (backend)
+- Tailwind CSS (styling)
+- Vercel (hosting)
+
+**Nieuw (toe te voegen):**
+- Zod voor form validation
+- React Hook Form (optioneel - betere UX)
+- Email templates (Supabase)
+
+### 2.2 Projectkaders
+
+- **Tijd:** 8-10 uur voor volledige implementatie + testing
+- **Team:** 1 developer (zelfstandig uit te voeren)
+- **Breaking changes:** GEEN - Bestaande auth blijft werken tijdens migratie
+- **Deployment:** Phased rollout (feature flags mogelijk)
+- **Security:** Production standards (OWASP top 10)
+
+### 2.3 Programmeer Uitgangspunten
+
+**Code Quality:**
+- β
**DRY:** Herbruikbare auth componenten en hooks
+- β
**KISS:** Gebruik Supabase built-in features waar mogelijk
+- β
**SOC:** Auth logic gescheiden van UI components
+- β
**YAGNI:** Geen OAuth/SSO (kan later), focus op email auth
+
+**Security Principles:**
+- Passwords NOOIT in plain text (Supabase handled dit)
+- Rate limiting op sensitive endpoints
+- HTTPS only (Vercel default)
+- Secure session cookies (HTTP-only, SameSite)
+- Input validation (email format, password strength)
+- CSRF protection (Next.js built-in)
+
+**UX Principles:**
+- Loading states op alle async operaties
+- Clear error messages (user-friendly, geen technical jargon)
+- Success feedback (toasts/messages)
+- Keyboard accessible (tab order, focus states)
+- Mobile responsive (touch-friendly buttons)
+
+---
+
+## 3. Epics & Stories Overzicht
+
+| Epic ID | Titel | Doel | Status | Stories | Geschatte Tijd |
+|---------|-------|------|--------|---------|----------------|
+| E1 | Login/Signup Page Refactor | Unified auth page met tabs/modes | β³ To Do | 4 | 2 uur |
+| E2 | Signup Flow (Magic Link) | Passwordless account creation | β³ To Do | 3 | 2 uur |
+| E3 | Login Flow (Password) | Returning user login | β³ To Do | 2 | 1 uur |
+| E4 | Password Reset Flow | Forgot password β reset email β update | β³ To Do | 4 | 2.5 uur |
+| E5 | Email Verification | Confirm email after signup | β³ To Do | 2 | 1 uur |
+| E6 | Demo Account | Quick access button | β³ To Do | 2 | 1 uur |
+| E7 | Testing & Polish | All flows tested, edge cases handled | β³ To Do | 3 | 1.5 uur |
+
+**Totale schatting:** 9-11 uur werk
+
+---
+
+## 4. Epics & Stories (Uitwerking)
+
+### Epic 1 β Login/Signup Page Refactor
+
+**Epic Doel:** Unified auth page die signup, login en demo ondersteunt met duidelijke modes.
+
+| Story ID | Beschrijving | Acceptatiecriteria | Status | Story Points |
+|----------|--------------|---------------------|--------|--------------|
+| E1.S1 | Design nieuwe page structuur | Wireframe + component breakdown | β³ | 1 |
+| E1.S2 | Implementeer mode switcher | Toggle tussen "Nieuw account" / "Inloggen" | β³ | 2 |
+| E1.S3 | Basis form components | Email input, password input, submit button | β³ | 2 |
+| E1.S4 | Error/success messaging | Toast/banner component voor feedback | β³ | 2 |
+
+**Technical Notes:**
+
+**E1.S1 - Page Structuur:**
+```
+ββββββββββββββββββββββββββββββββββββββββββββ
+β Mini EPD - Professioneel Elektronisch β
+β PatiΓ«nten Dossier β
+ββββββββββββββββββββββββββββββββββββββββββββ€
+β β
+β [π Quick Demo - Bekijk Prototype] β β Prominent, secundair
+β β
+β ββββββ of ββββββ β
+β β
+β ( ) Nieuw account aanmaken β β Radio/Tab
+β (β’) Inloggen β
+β β
+β π§ Email β
+β [_________________________] β
+β β
+β π Wachtwoord β β Alleen bij "Inloggen"
+β [_________________________] β
+β β
+β [Inloggen] of [Account Aanmaken] β β Dynamic button text
+β β
+β Wachtwoord vergeten? [Reset] β β Link naar /reset-password
+β β
+ββββββββββββββββββββββββββββββββββββββββββββ
+```
+
+**E1.S2 - Mode State Management:**
+```typescript
+type AuthMode = 'signup' | 'login'
+
+const [mode, setMode] = useState
('login') // Default = login
+
+// Dynamic UI based on mode
+const showPasswordField = mode === 'login'
+const buttonText = mode === 'login' ? 'Inloggen' : 'Account Aanmaken'
+const submitHandler = mode === 'login' ? handleLogin : handleSignup
+```
+
+**E1.S3 - Form Components:**
+```typescript
+// Input component met validation states
+
+
+
+```
+
+**E1.S4 - Messaging System:**
+```typescript
+// Toast/banner component
+type Message = {
+ type: 'success' | 'error' | 'info'
+ text: string
+ duration?: number
+}
+
+// Usage examples:
+showMessage({
+ type: 'success',
+ text: 'Check je email voor de magic link!'
+})
+
+showMessage({
+ type: 'error',
+ text: 'Email of wachtwoord incorrect'
+})
+```
+
+---
+
+### Epic 2 β Signup Flow (Magic Link)
+
+**Epic Doel:** Passwordless signup via magic link in email (Supabase OTP).
+
+| Story ID | Beschrijving | Acceptatiecriteria | Status | Story Points |
+|----------|--------------|---------------------|--------|--------------|
+| E2.S1 | Implementeer signup handler | Email submit β Supabase OTP β Success message | β³ | 3 |
+| E2.S2 | Email callback handler | Magic link β account created β redirect dashboard | β³ | 2 |
+| E2.S3 | Custom email template | Branded email met clear CTA | β³ | 2 |
+
+**Technical Notes:**
+
+**E2.S1 - Signup Handler:**
+```typescript
+// app/login/page.tsx
+
+async function handleSignup(email: string) {
+ setLoading(true)
+
+ try {
+ const { data, error } = await supabase.auth.signInWithOtp({
+ email,
+ options: {
+ emailRedirectTo: `${window.location.origin}/auth/callback`,
+ shouldCreateUser: true,
+ data: {
+ // Optional: Store extra user metadata
+ source: 'web_signup',
+ timestamp: new Date().toISOString(),
+ }
+ }
+ })
+
+ if (error) throw error
+
+ setMessage({
+ type: 'success',
+ text: `Check je email (${email}) voor de magic link!`
+ })
+
+ // Optional: Track analytics
+ trackEvent('signup_initiated', { email })
+
+ } catch (error) {
+ setMessage({
+ type: 'error',
+ text: error.message || 'Signup mislukt. Probeer opnieuw.'
+ })
+ } finally {
+ setLoading(false)
+ }
+}
+```
+
+**E2.S2 - Callback Handler:**
+```typescript
+// app/auth/callback/route.ts (EXISTING - update if needed)
+
+export async function GET(request: Request) {
+ const requestUrl = new URL(request.url)
+ const code = requestUrl.searchParams.get('code')
+
+ if (code) {
+ const supabase = createServerClient()
+
+ const { data, error } = await supabase.auth.exchangeCodeForSession(code)
+
+ if (!error && data.user) {
+ // Check if this is first login (new user)
+ const isNewUser = data.user.created_at === data.user.last_sign_in_at
+
+ if (isNewUser) {
+ // Redirect to onboarding
+ return NextResponse.redirect(`${requestUrl.origin}/onboarding`)
+ } else {
+ // Redirect to dashboard
+ return NextResponse.redirect(`${requestUrl.origin}/epd/clients`)
+ }
+ }
+ }
+
+ // Error fallback
+ return NextResponse.redirect(`${requestUrl.origin}/login?error=auth_failed`)
+}
+```
+
+**E2.S3 - Email Template (Supabase Dashboard):**
+```html
+
+
+Welkom bij Mini EPD!
+
+Je hebt een account aangemaakt. Klik op de knop hieronder om je email te bevestigen en in te loggen:
+
+
+ Bevestig Email & Login
+
+
+
+ Deze link is 1 uur geldig. Heb je deze email niet aangevraagd? Negeer deze email.
+
+
+
+ Mini EPD - Professioneel Elektronisch PatiΓ«nten Dossier
+
+```
+
+---
+
+### Epic 3 β Login Flow (Password)
+
+**Epic Doel:** Returning users kunnen inloggen met email + wachtwoord.
+
+| Story ID | Beschrijving | Acceptatiecriteria | Status | Story Points |
+|----------|--------------|---------------------|--------|--------------|
+| E3.S1 | Implementeer login handler | Email + password β Supabase auth β redirect | β³ | 2 |
+| E3.S2 | Set password flow | Na magic link signup β optie om password te setten | β³ | 3 |
+
+**Technical Notes:**
+
+**E3.S1 - Login Handler:**
+```typescript
+// app/login/page.tsx
+
+async function handleLogin(email: string, password: string) {
+ setLoading(true)
+
+ // Validation
+ if (!email || !password) {
+ setMessage({
+ type: 'error',
+ text: 'Vul email en wachtwoord in'
+ })
+ setLoading(false)
+ return
+ }
+
+ try {
+ const { data, error } = await supabase.auth.signInWithPassword({
+ email,
+ password
+ })
+
+ if (error) throw error
+
+ setMessage({
+ type: 'success',
+ text: 'Ingelogd! Redirect naar dashboard...'
+ })
+
+ // Track analytics
+ trackEvent('login_success', { method: 'password' })
+
+ // Redirect after 500ms
+ setTimeout(() => {
+ router.push('/epd/clients')
+ }, 500)
+
+ } catch (error) {
+ setMessage({
+ type: 'error',
+ text: 'Email of wachtwoord incorrect'
+ })
+
+ // Track failed login (rate limiting check)
+ trackEvent('login_failed', { email })
+
+ } finally {
+ setLoading(false)
+ }
+}
+```
+
+**E3.S2 - Set Password Flow:**
+```typescript
+// app/set-password/page.tsx (NEW)
+
+'use client'
+
+export default function SetPasswordPage() {
+ const [password, setPassword] = useState('')
+ const [confirmPassword, setConfirmPassword] = useState('')
+ const [loading, setLoading] = useState(false)
+ const router = useRouter()
+
+ async function handleSetPassword() {
+ // Validation
+ if (password !== confirmPassword) {
+ setMessage({ type: 'error', text: 'Wachtwoorden komen niet overeen' })
+ return
+ }
+
+ if (password.length < 8) {
+ setMessage({ type: 'error', text: 'Wachtwoord moet minimaal 8 tekens zijn' })
+ return
+ }
+
+ setLoading(true)
+
+ try {
+ const { error } = await supabase.auth.updateUser({
+ password
+ })
+
+ if (error) throw error
+
+ setMessage({
+ type: 'success',
+ text: 'Wachtwoord ingesteld! Je kunt nu inloggen met email en wachtwoord.'
+ })
+
+ setTimeout(() => router.push('/epd/clients'), 2000)
+
+ } catch (error) {
+ setMessage({ type: 'error', text: error.message })
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+
+
Stel een wachtwoord in
+
Gebruik dit wachtwoord om later in te loggen
+
+
+
+
+
+
+
+
+
+
+ )
+}
+```
+
+---
+
+### Epic 4 β Password Reset Flow
+
+**Epic Doel:** Users kunnen vergeten wachtwoord resetten via email link.
+
+| Story ID | Beschrijving | Acceptatiecriteria | Status | Story Points |
+|----------|--------------|---------------------|--------|--------------|
+| E4.S1 | Reset request page | Email input β Send reset link | β³ | 2 |
+| E4.S2 | Reset email template | Branded email met reset link | β³ | 1 |
+| E4.S3 | Reset password page | New password form + validation | β³ | 3 |
+| E4.S4 | Update password handler | Supabase update + redirect | β³ | 2 |
+
+**Technical Notes:**
+
+**E4.S1 - Reset Request Page:**
+```typescript
+// app/reset-password/page.tsx
+
+'use client'
+
+export default function ResetPasswordPage() {
+ const [email, setEmail] = useState('')
+ const [loading, setLoading] = useState(false)
+ const [sent, setSent] = useState(false)
+
+ async function handleResetRequest() {
+ if (!email) {
+ setMessage({ type: 'error', text: 'Vul je email in' })
+ return
+ }
+
+ setLoading(true)
+
+ try {
+ const { error } = await supabase.auth.resetPasswordForEmail(email, {
+ redirectTo: `${window.location.origin}/update-password`
+ })
+
+ if (error) throw error
+
+ setSent(true)
+ setMessage({
+ type: 'success',
+ text: `Reset link verstuurd naar ${email}. Check je inbox!`
+ })
+
+ } catch (error) {
+ setMessage({ type: 'error', text: error.message })
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ if (sent) {
+ return (
+
+
Email verstuurd! βοΈ
+
Check je inbox voor de reset link.
+
+ Niet ontvangen? Check je spam folder of
+
+
+
+ )
+ }
+
+ return (
+
+
+
Wachtwoord Vergeten
+
Vul je email in en we sturen je een reset link.
+
+
+
+
+
+
+ β Terug naar login
+
+
+
+ )
+}
+```
+
+**E4.S2 - Reset Email Template:**
+```html
+
+
+Wachtwoord Resetten
+
+Je hebt een wachtwoord reset aangevraagd voor je Mini EPD account.
+
+
+ Reset Wachtwoord
+
+
+
+ Deze link is 1 uur geldig. Heb je deze reset niet aangevraagd? Negeer deze email - je wachtwoord blijft ongewijzigd.
+
+```
+
+**E4.S3 - Update Password Page:**
+```typescript
+// app/update-password/page.tsx
+
+'use client'
+
+export default function UpdatePasswordPage() {
+ const [password, setPassword] = useState('')
+ const [confirmPassword, setConfirmPassword] = useState('')
+ const [loading, setLoading] = useState(false)
+ const router = useRouter()
+
+ async function handleUpdatePassword() {
+ // Validation
+ if (password !== confirmPassword) {
+ setMessage({ type: 'error', text: 'Wachtwoorden komen niet overeen' })
+ return
+ }
+
+ if (password.length < 8) {
+ setMessage({ type: 'error', text: 'Wachtwoord minimaal 8 tekens' })
+ return
+ }
+
+ setLoading(true)
+
+ try {
+ const { error } = await supabase.auth.updateUser({
+ password
+ })
+
+ if (error) throw error
+
+ setMessage({
+ type: 'success',
+ text: 'Wachtwoord gewijzigd! Je kunt nu inloggen.'
+ })
+
+ setTimeout(() => router.push('/login'), 2000)
+
+ } catch (error) {
+ setMessage({ type: 'error', text: error.message })
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+ )
+}
+```
+
+---
+
+### Epic 5 β Email Verification
+
+**Epic Doel:** Confirm email na signup (security + anti-spam).
+
+| Story ID | Beschrijving | Acceptatiecriteria | Status | Story Points |
+|----------|--------------|---------------------|--------|--------------|
+| E5.S1 | Enable email confirmation | Supabase settings + email template | β³ | 1 |
+| E5.S2 | Unverified state handling | Block app access until confirmed | β³ | 2 |
+
+**Technical Notes:**
+
+**E5.S1 - Enable Confirmation:**
+```bash
+# Supabase Dashboard:
+# Authentication β Settings β Email Auth
+# β
Enable email confirmations
+# β
Secure email change
+```
+
+**E5.S2 - Verification Check:**
+```typescript
+// middleware.ts (UPDATE)
+
+export async function middleware(request: NextRequest) {
+ const supabase = createServerClient()
+ const { data: { user } } = await supabase.auth.getUser()
+
+ // Check if email is verified
+ if (user && !user.email_confirmed_at) {
+ // Redirect to verification notice page
+ return NextResponse.redirect(new URL('/verify-email', request.url))
+ }
+
+ // Rest of middleware logic...
+}
+```
+
+```typescript
+// app/verify-email/page.tsx (NEW)
+
+export default function VerifyEmailPage() {
+ const [resending, setResending] = useState(false)
+
+ async function resendVerification() {
+ setResending(true)
+ // Trigger resend via Supabase
+ await supabase.auth.resend({
+ type: 'signup',
+ email: user.email
+ })
+ setResending(false)
+ }
+
+ return (
+
+
+
Email Verificatie Vereist
+
Check je inbox voor de verificatie link.
+
+
+
+
β Terug naar login
+
+
+ )
+}
+```
+
+---
+
+### Epic 6 β Demo Account
+
+**Epic Doel:** Quick demo access button voor recruiters/prospects.
+
+| Story ID | Beschrijving | Acceptatiecriteria | Status | Story Points |
+|----------|--------------|---------------------|--------|--------------|
+| E6.S1 | QuickDemoButton component | One-click demo login | β³ | 2 |
+| E6.S2 | Demo mode indicator | Banner in EPD app | β³ | 1 |
+
+**Technical Notes:**
+
+**E6.S1 - Quick Demo Button:**
+```typescript
+// app/login/components/quick-demo-button.tsx
+
+export function QuickDemoButton() {
+ const [loading, setLoading] = useState(false)
+ const router = useRouter()
+
+ async function handleQuickDemo() {
+ setLoading(true)
+
+ try {
+ const { error } = await supabase.auth.signInWithPassword({
+ email: 'demo@mini-ecd.demo',
+ password: 'Demo2024!'
+ })
+
+ if (error) throw error
+
+ trackEvent('demo_access')
+ router.push('/epd/clients?demo=true')
+
+ } catch (error) {
+ setMessage({ type: 'error', text: 'Demo login mislukt' })
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+ )
+}
+```
+
+**E6.S2 - Demo Banner:**
+```typescript
+// app/epd/components/demo-banner.tsx
+
+export function DemoBanner() {
+ const searchParams = useSearchParams()
+ const isDemo = searchParams.get('demo') === 'true'
+
+ if (!isDemo) return null
+
+ return (
+
+
+
+ π Je bekijkt het prototype met demo data
+
+
+ Maak gratis account β
+
+
+
+ )
+}
+```
+
+---
+
+### Epic 7 β Testing & Polish
+
+**Epic Doel:** Alle flows grondig testen + edge cases + UX polish.
+
+| Story ID | Beschrijving | Acceptatiecriteria | Status | Story Points |
+|----------|--------------|---------------------|--------|--------------|
+| E7.S1 | Test alle happy flows | Signup, login, reset, demo werken | β³ | 2 |
+| E7.S2 | Test error scenarios | Network errors, invalid input, rate limits | β³ | 2 |
+| E7.S3 | UX polish | Loading states, animations, responsive | β³ | 2 |
+
+**Test Cases:**
+
+| Test ID | Flow | Scenario | Expected Result |
+|---------|------|----------|-----------------|
+| TC1 | Signup | Valid email β Magic link | Success message, email sent |
+| TC2 | Signup | Invalid email format | Validation error |
+| TC3 | Signup | Email already exists | "Account bestaat al - probeer in te loggen" |
+| TC4 | Login | Valid credentials | Redirect to dashboard |
+| TC5 | Login | Invalid password | "Email of wachtwoord incorrect" |
+| TC6 | Login | Unverified email | Redirect to verify-email page |
+| TC7 | Password Reset | Valid email | Reset link sent |
+| TC8 | Password Reset | Email not found | Still show success (security) |
+| TC9 | Password Reset | Weak new password | Validation error "Min 8 characters" |
+| TC10 | Demo | Quick demo button | Instant login, redirect with ?demo=true |
+| TC11 | Session | Auto-refresh token | Session stays valid 24h+ |
+| TC12 | Logout | Click logout | Session cleared, redirect to login |
+
+---
+
+## 5. Kwaliteit & Testplan
+
+### Security Checklist
+
+- [ ] Passwords hashed (Supabase handles)
+- [ ] Rate limiting on sensitive endpoints
+- [ ] HTTPS only (Vercel default)
+- [ ] HTTP-only secure cookies
+- [ ] Input validation (email, password strength)
+- [ ] CSRF protection (Next.js built-in)
+- [ ] No sensitive data in client-side code
+- [ ] Error messages don't leak info ("Email or password incorrect" not "Email not found")
+
+### Performance Checklist
+
+- [ ] Auth state loaded < 500ms
+- [ ] Form submissions < 2s response
+- [ ] Optimistic UI updates
+- [ ] Debounced email validation
+- [ ] Code splitting (lazy load auth pages)
+
+### UX Checklist
+
+- [ ] Clear loading states (spinners)
+- [ ] Success feedback (toasts/messages)
+- [ ] Error messages user-friendly
+- [ ] Keyboard accessible (tab order)
+- [ ] Mobile responsive (touch targets)
+- [ ] Focus management (auto-focus first input)
+- [ ] Password visibility toggle
+- [ ] Password strength indicator
+
+---
+
+## 6. Routes & File Structure
+
+### New Routes
+
+```
+app/
+βββ login/page.tsx # Main auth page (signup/login/demo)
+βββ reset-password/page.tsx # Request reset link
+βββ update-password/page.tsx # Set new password (from email)
+βββ set-password/page.tsx # Set password after magic link signup
+βββ verify-email/page.tsx # Email verification notice
+βββ onboarding/page.tsx # First-time user onboarding (optional)
+βββ auth/
+β βββ callback/route.ts # Magic link callback (EXISTING)
+β βββ logout/route.ts # Logout handler (EXISTING)
+```
+
+### Components
+
+```
+components/auth/
+βββ auth-form.tsx # Main form component
+βββ quick-demo-button.tsx # Demo access button
+βββ password-strength.tsx # Password strength indicator
+βββ auth-message.tsx # Success/error message component
+βββ mode-switcher.tsx # Toggle between signup/login
+```
+
+---
+
+## 7. Supabase Configuration
+
+### Email Templates (To Configure)
+
+1. **Confirm Signup** (Magic Link)
+2. **Reset Password**
+3. **Email Change Confirmation**
+
+### Settings (To Enable)
+
+```bash
+# Supabase Dashboard β Authentication β Settings
+
+β
Enable email confirmations
+β
Enable email change confirmations
+β
Secure email change (require password)
+
+# Email Auth Settings
+Rate limit: 4 emails per hour (default)
+Confirmation expiry: 1 hour
+```
+
+### RLS Policies (Verify)
+
+```sql
+-- Users can only see their own data
+CREATE POLICY "Users can view own data" ON profiles
+ FOR SELECT USING (auth.uid() = user_id);
+
+-- Users can update own profile
+CREATE POLICY "Users can update own data" ON profiles
+ FOR UPDATE USING (auth.uid() = user_id);
+```
+
+---
+
+## 8. Risico's & Mitigatie
+
+| Risico | Kans | Impact | Mitigatie |
+|--------|------|--------|-----------|
+| Email delivery issues | Middel | Hoog | - Demo account fallback
- "Resend email" buttons
- Clear "check spam" messaging |
+| Rate limiting blocks users | Laag | Middel | - Increase Supabase limits
- Clear error message with wait time |
+| Password reset abuse | Middel | Laag | - Rate limiting (built-in)
- Track suspicious activity |
+| Email enumeration | Middel | Laag | - Same message for existing/non-existing emails
- No "email not found" errors |
+| Session hijacking | Laag | Hoog | - HTTP-only cookies
- Short token expiry
- Secure flag on cookies |
+| Broken magic links | Laag | Middel | - 24h expiry (reasonable)
- Clear error page
- "Request new link" option |
+
+---
+
+## 9. Definition of Done
+
+**All Epics (E1-E7) zijn compleet wanneer:**
+
+β
**Functional Requirements:**
+- Signup flow werkt (magic link β account created)
+- Login flow werkt (email + password β dashboard)
+- Password reset flow werkt (request β email β new password)
+- Email verification enforced (unverified users blocked)
+- Demo account accessible (one-click)
+- Session management werkt (auto-refresh, logout)
+
+β
**Security Requirements:**
+- All OWASP top 10 addressed
+- No sensitive data in client code
+- Rate limiting active
+- Secure cookies configured
+
+β
**UX Requirements:**
+- All flows tested on mobile + desktop
+- Loading states smooth
+- Error messages clear
+- Keyboard accessible
+- No console errors
+
+β
**Code Quality:**
+- TypeScript strict mode (no `any`)
+- Reusable components extracted
+- Error handling comprehensive
+- Code commented where complex
+
+β
**Documentation:**
+- README updated with auth flow docs
+- Environment variables documented
+- Supabase setup guide created
+
+β
**Deployment:**
+- Tested on staging environment
+- Production deployment successful
+- Smoke tests passed
+- Rollback plan ready
+
+---
+
+## 10. Implementatie Volgorde
+
+**Aanbevolen phased rollout:**
+
+### **Phase 1: Foundation (3-4 uur)**
+1. E1.S1-S4: Refactor login page
+2. E2.S1-S2: Signup flow (magic link)
+3. E3.S1: Login flow (password)
+
+**Checkpoint:** Basic signup + login werkt
+
+### **Phase 2: Password Management (2-3 uur)**
+4. E4.S1-S4: Password reset flow
+5. E3.S2: Set password after signup
+
+**Checkpoint:** Complete password lifecycle
+
+### **Phase 3: Security & Demo (2 uur)**
+6. E5.S1-S2: Email verification
+7. E6.S1-S2: Demo account + banner
+
+**Checkpoint:** Production-ready security
+
+### **Phase 4: Polish (1-2 uur)**
+8. E7.S1-S3: Testing + UX polish
+
+**Checkpoint:** All flows tested, ready to showcase
+
+**Total: 8-11 uur**
+
+---
+
+## 11. Referenties
+
+**Mission Control Documents:**
+- **PRD:** `docs/specs/prd-mini-ecd-v1.2.md`
+- **TO:** `docs/specs/to-mini-ecd-v1_2.md`
+
+**External Resources:**
+- Supabase Auth Docs: https://supabase.com/docs/guides/auth
+- Next.js SSR Auth: https://supabase.com/docs/guides/auth/server-side
+- OWASP Top 10: https://owasp.org/www-project-top-ten/
+
+**Code References:**
+- Login page: `app/login/page.tsx` (MAJOR REFACTOR)
+- Auth callback: `app/auth/callback/route.ts` (MINOR UPDATE)
+- Middleware: `middleware.ts` (UPDATE - email verification check)
+- Auth client: `lib/auth/client.ts` (EXTEND)
+
+---
+
+## 12. Versiehistorie
+
+| Versie | Datum | Auteur | Wijziging |
+|--------|-------|--------|-----------|
+| v1.0 | 18-01-2025 | Colin | InitiΓ«le versie - Volledige auth flow bouwplan |
+
+---
+
+**Status:** β³ Ready for Implementation
+**Next Steps:** Start met E1.S1 (Design nieuwe page structuur)
+**Portfolio Value:** π₯π₯π₯π₯π₯ (High - showcases full-stack auth expertise)
diff --git a/docs/specs/archive/bouwplan-auth-incremental-v1.0.md b/docs/specs/archive/bouwplan-auth-incremental-v1.0.md
new file mode 100644
index 0000000..1ff695d
--- /dev/null
+++ b/docs/specs/archive/bouwplan-auth-incremental-v1.0.md
@@ -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 (
+
+
+
βοΈ
+
Email Verstuurd!
+
+ Check je inbox voor de reset link. De link is 1 uur geldig.
+
+
+ Niet ontvangen? Check je spam folder.
+
+
+ β Terug naar login
+
+
+
+ )
+ }
+
+ return (
+
+
+
+
+ Wachtwoord Vergeten?
+
+
+ Geen probleem! Vul je email in en we sturen je een reset link.
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+
+ β Terug naar login
+
+
+
+
+ )
+}
+```
+
+### 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 (
+
+
+
β
+
Wachtwoord Gewijzigd!
+
+ Je wordt doorgestuurd naar de login pagina...
+
+
+
+ )
+ }
+
+ return (
+
+
+
+
+ Nieuw Wachtwoord Instellen
+
+
+ Kies een sterk wachtwoord (minimaal 8 tekens).
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+ )
+}
+```
+
+### E1.S3 - Email Template Configureren
+
+**Waar:** Supabase Dashboard β Authentication β Email Templates β Reset Password
+
+**Template:**
+```html
+Wachtwoord Resetten
+
+Je hebt een wachtwoord reset aangevraagd voor je Mini EPD account.
+
+
+ Reset Wachtwoord
+
+
+
+ Deze link is 1 uur geldig. Heb je deze reset niet aangevraagd? Negeer deze email.
+
+```
+
+---
+
+## 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 (
+
+
+
+ Stel een Wachtwoord In (Optioneel)
+
+
+ Met een wachtwoord kun je sneller inloggen zonder magic link.
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+ )
+}
+```
+
+### 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 */}
+
+
+
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 */}
+
+
+```
+
+---
+
+## 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`
diff --git a/docs/specs/bouwplan-auth-hook-duplicate-email-v1.0.md b/docs/specs/bouwplan-auth-hook-duplicate-email-v1.0.md
new file mode 100644
index 0000000..5801c00
--- /dev/null
+++ b/docs/specs/bouwplan-auth-hook-duplicate-email-v1.0.md
@@ -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`
+
diff --git a/lib/auth/client.ts b/lib/auth/client.ts
index 2a3f57f..2c28e17 100644
--- a/lib/auth/client.ts
+++ b/lib/auth/client.ts
@@ -18,31 +18,137 @@ export function createClient() {
}
/**
- * Send magic link to user's email
- * Auto-creates account if user doesn't exist
+ * Sign up with email + password
+ * Returns data with user and session (if email confirmation is disabled)
+ *
+ * Note: If email confirmation is enabled and email already exists,
+ * Supabase returns success but no user object (security feature)
*/
-export async function loginWithMagicLink(email: string) {
+export async function signUpWithPassword(email: string, password: string) {
const supabase = createClient()
-
- const { data, error } = await supabase.auth.signInWithOtp({
+ const { data, error } = await supabase.auth.signUp({
email,
+ password,
options: {
- emailRedirectTo: `${window.location.origin}/auth/callback`,
- shouldCreateUser: true, // Auto-create account on first login
+ emailRedirectTo: `${window.location.origin}/auth/callback`
}
})
- if (error) throw error
-
- return {
- success: true,
- message: 'Check je email voor de magic link!',
- data
+ // DEBUG: Log response to see what Supabase returns when hook throws error
+ if (process.env.NODE_ENV === 'development') {
+ console.log('π SignUp Response:', {
+ hasError: !!error,
+ errorMessage: error?.message,
+ errorCode: error?.code,
+ errorStatus: (error as any)?.status,
+ hasUser: !!data?.user,
+ hasSession: !!data?.session,
+ identities: data?.user?.identities?.length || 0
+ })
}
+
+ if (error) {
+ // Check for explicit duplicate email errors (from Auth Hook or Supabase)
+ const errorMessage = error.message?.toLowerCase() || ''
+ const errorCode = error.code?.toLowerCase() || ''
+
+ if (errorMessage.includes('already registered') ||
+ errorMessage.includes('already exists') ||
+ errorMessage.includes('al geregistreerd') || // Auth Hook (NL)
+ errorMessage.includes('emailadres is al') || // Extra check voor hook message
+ errorCode === 'user_already_registered' ||
+ (error as any)?.status === 400) { // Hook errors typically have 400 status
+ // If it's from Auth Hook, preserve the original message
+ const duplicateError = new Error(error.message || 'Dit emailadres is al geregistreerd. Probeer in te loggen of gebruik "Wachtwoord vergeten?".')
+ ;(duplicateError as any).code = 'user_already_registered'
+ throw duplicateError
+ }
+ throw error
+ }
+
+ // Check if user was created but has no identities (indicates duplicate email)
+ // This happens when Supabase creates a user object but doesn't actually create the account
+ // because the email already exists (security feature - email enumeration protection)
+ if (data.user && (!data.user.identities || data.user.identities.length === 0)) {
+ // User object exists but no identities = duplicate email detected by Supabase
+ // Try to login to confirm and get proper error message
+ const loginAttempt = await supabase.auth.signInWithPassword({
+ email,
+ password
+ })
+
+ if (loginAttempt.data?.user) {
+ // Login succeeded - account already exists!
+ const duplicateError = new Error('Dit emailadres is al geregistreerd. Je bent nu ingelogd.')
+ ;(duplicateError as any).code = 'user_already_registered'
+ ;(duplicateError as any).data = loginAttempt.data
+ throw duplicateError
+ } else {
+ // Login failed - account exists but password is wrong
+ // This is the duplicate email case that the hook should have caught
+ const duplicateError = new Error('Dit emailadres is al geregistreerd. Probeer in te loggen of gebruik "Wachtwoord vergeten?".')
+ ;(duplicateError as any).code = 'user_already_registered'
+ throw duplicateError
+ }
+ }
+
+ // If email confirmation is enabled, Supabase doesn't return a user/session
+ // for new signups (email is sent instead)
+ // However, if email already exists, Supabase also returns no user but doesn't send email
+ // Try to detect this by attempting a login
+ if (!data.user && !data.session) {
+ // Try to login to check if account already exists
+ // This is a fallback detection method if the hook doesn't work
+ const loginAttempt = await supabase.auth.signInWithPassword({
+ email,
+ password
+ })
+
+ if (loginAttempt.data?.user) {
+ // Login succeeded - account already exists!
+ // This means the hook didn't catch it, but we can detect it client-side
+ const duplicateError = new Error('Dit emailadres is al geregistreerd. Je bent nu ingelogd.')
+ ;(duplicateError as any).code = 'user_already_registered'
+ ;(duplicateError as any).data = loginAttempt.data // Include session data
+ throw duplicateError
+ }
+
+ // Login failed - could be new account OR wrong password for existing account
+ // If it's a wrong password, we can't distinguish from a new account
+ // (Supabase security feature - email enumeration protection)
+ // Return the signup data (which has no user/session) and let UI show generic message
+ // Note: If hook is working, we should have gotten an error above, so this is fallback
+ }
+
+ return data
}
/**
- * Login with email + password (for demo accounts)
+ * Send password reset email
+ */
+export async function resetPasswordForEmail(email: string) {
+ const supabase = createClient()
+ const { error } = await supabase.auth.resetPasswordForEmail(email, {
+ redirectTo: `${window.location.origin}/auth/callback?next=/update-password`
+ })
+
+ if (error) throw error
+ return true
+}
+
+/**
+ * Update user password (requires active session)
+ */
+export async function updateUserPassword(password: string) {
+ const supabase = createClient()
+ const { error } = await supabase.auth.updateUser({ password })
+
+ if (error) throw error
+ return true
+}
+
+/**
+ * Login with email + password (for demo accounts and regular users)
*/
export async function loginWithPassword(email: string, password: string) {
const supabase = createClient()
diff --git a/package.json b/package.json
index 4afa27a..c92318c 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,8 @@
"build": "next build --webpack",
"start": "next start",
"lint": "eslint",
- "types:generate": "npx supabase gen types typescript --project-id dqugbrpwtisgyxscpefg > lib/supabase/types.ts"
+ "types:generate": "npx supabase gen types typescript --project-id dqugbrpwtisgyxscpefg > lib/supabase/types.ts",
+ "setup:auth-hook": "tsx scripts/setup-auth-hook.ts"
},
"dependencies": {
"@radix-ui/react-icons": "^1.3.2",
diff --git a/scripts/apply-migration.ts b/scripts/apply-migration.ts
new file mode 100644
index 0000000..88e6e0d
--- /dev/null
+++ b/scripts/apply-migration.ts
@@ -0,0 +1,71 @@
+#!/usr/bin/env tsx
+/**
+ * Apply Migration Script
+ * Executes a SQL migration file directly against the Supabase database
+ */
+
+import { createClient } from '@supabase/supabase-js'
+import { readFileSync } from 'fs'
+import { join } from 'path'
+
+const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
+const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!
+
+if (!supabaseUrl || !supabaseServiceKey) {
+ console.error('β Missing Supabase credentials')
+ console.error(' Make sure NEXT_PUBLIC_SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are set')
+ process.exit(1)
+}
+
+const supabase = createClient(supabaseUrl, supabaseServiceKey, {
+ auth: {
+ autoRefreshToken: false,
+ persistSession: false
+ }
+})
+
+async function applyMigration(migrationFile: string) {
+ console.log(`π Applying migration: ${migrationFile}\n`)
+
+ try {
+ // Read migration file
+ const migrationPath = join(process.cwd(), 'supabase/migrations', migrationFile)
+ const sql = readFileSync(migrationPath, 'utf-8')
+
+ // Execute SQL
+ const { data, error } = await supabase.rpc('exec_sql', { sql_query: sql })
+
+ if (error) {
+ // Try alternative approach - direct query
+ const statements = sql
+ .split(';')
+ .map(s => s.trim())
+ .filter(s => s.length > 0 && !s.startsWith('--'))
+
+ for (const statement of statements) {
+ const { error: stmtError } = await supabase.rpc(statement)
+ if (stmtError) {
+ console.error('β Error executing statement:', stmtError.message)
+ throw stmtError
+ }
+ }
+ }
+
+ console.log('β
Migration applied successfully!')
+ console.log('\nπ Next steps:')
+ console.log(' 1. Run: pnpm run setup:auth-hook')
+ console.log(' 2. Configure hook link in Supabase Dashboard')
+
+ } catch (error: any) {
+ console.error('β Migration failed:', error.message)
+ console.log('\nπ‘ Alternative: Apply manually via Supabase Dashboard')
+ console.log(' 1. Go to: https://supabase.com/dashboard/project/_/sql')
+ console.log(' 2. Copy contents of:', migrationFile)
+ console.log(' 3. Paste and run in SQL Editor')
+ process.exit(1)
+ }
+}
+
+// Get migration file from command line or use latest
+const migrationFile = process.argv[2] || '20251119094908_auth_hook_duplicate_email.sql'
+applyMigration(migrationFile)
diff --git a/scripts/setup-auth-hook.ts b/scripts/setup-auth-hook.ts
new file mode 100644
index 0000000..0d477d6
--- /dev/null
+++ b/scripts/setup-auth-hook.ts
@@ -0,0 +1,95 @@
+#!/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 { config } from 'dotenv'
+import { createClient } from '@supabase/supabase-js'
+
+// Load environment variables from .env.local
+config({ path: '.env.local' })
+
+const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
+const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!
+
+if (!supabaseUrl || !supabaseServiceKey) {
+ console.error('β Missing environment variables')
+ console.error(' Make sure .env.local is loaded with:')
+ console.error(' - NEXT_PUBLIC_SUPABASE_URL')
+ console.error(' - SUPABASE_SERVICE_ROLE_KEY')
+ process.exit(1)
+}
+
+const supabase = createClient(supabaseUrl, supabaseServiceKey, {
+ auth: {
+ autoRefreshToken: false,
+ persistSession: false
+ }
+})
+
+async function setupAuthHook() {
+ console.log('π Auth Hook Setup Script\n')
+
+ // Extract project ID from URL
+ const projectId = supabaseUrl.match(/https:\/\/([^.]+)\.supabase\.co/)?.[1]
+
+ // Check if function exists by trying to call it with a test payload
+ // This is more reliable than querying pg_proc directly
+ try {
+ const testPayload = {
+ user: {
+ email: 'test@example.com'
+ }
+ }
+
+ const { data, error } = await supabase.rpc('hook_check_duplicate_email', {
+ event: testPayload
+ })
+
+ // If function doesn't exist, we'll get a "function does not exist" error
+ 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(' 1. Ga naar: https://supabase.com/dashboard/project/' + projectId + '/sql')
+ console.log(' 2. Open migration file: supabase/migrations/20251119094908_auth_hook_duplicate_email.sql')
+ console.log(' 3. Kopieer de inhoud en plak in SQL Editor')
+ console.log(' 4. Klik "RUN" om de functie aan te maken')
+ console.log('\n Of via CLI (als je Supabase CLI hebt geconfigureerd):')
+ console.log(' npx supabase db push')
+ return
+ }
+
+ console.log('β
Function exists: hook_check_duplicate_email')
+ console.log(' Test call succeeded with response:', data || '{}')
+
+ } catch (error: any) {
+ console.error('β οΈ Could not verify function:', error.message)
+ console.log(' Continuing with setup instructions...\n')
+ }
+
+ console.log('\nπ Stap 2: Configureer hook link in Supabase Dashboard:')
+ console.log(' 1. Ga naar: https://supabase.com/dashboard/project/' + projectId + '/auth/hooks')
+ console.log(' 2. Klik "Add a new hook" of "Enable Hooks"')
+ console.log(' 3. Selecteer:')
+ console.log(' - Hook Type: "Send a hook on before a user is created" (before-user-created)')
+ console.log(' - Select hook: "Postgres Function"')
+ console.log(' - Schema: "public"')
+ console.log(' - Function Name: "hook_check_duplicate_email"')
+ console.log(' 4. Klik "Create hook" of "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 na Dashboard configuratie!')
+ console.log('\nπ§ͺ Test de hook:')
+ console.log(' 1. Ga naar je signup pagina')
+ console.log(' 2. Probeer te registreren met een bestaand emailadres')
+ console.log(' 3. Je zou een error moeten zien: "Dit emailadres is al geregistreerd..."')
+}
+
+setupAuthHook().catch(console.error)
diff --git a/scripts/test-hook-function.ts b/scripts/test-hook-function.ts
new file mode 100644
index 0000000..ca87f75
--- /dev/null
+++ b/scripts/test-hook-function.ts
@@ -0,0 +1,72 @@
+#!/usr/bin/env tsx
+/**
+ * Test Auth Hook Function
+ * Tests the hook function directly to verify it works
+ */
+
+import { config } from 'dotenv'
+import { createClient } from '@supabase/supabase-js'
+
+// Load environment variables
+config({ path: '.env.local' })
+
+const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
+const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!
+
+const supabase = createClient(supabaseUrl, supabaseServiceKey, {
+ auth: {
+ autoRefreshToken: false,
+ persistSession: false
+ }
+})
+
+async function testHookFunction() {
+ console.log('π§ͺ Testing Auth Hook Function\n')
+
+ // Test 1: New email (should allow)
+ console.log('Test 1: New email (should return empty object)')
+ const test1 = await supabase.rpc('hook_check_duplicate_email', {
+ event: { user: { email: 'newemail@test.com' } }
+ })
+ console.log('Result:', test1.data)
+ console.log('Error:', test1.error)
+
+ // Test 2: Existing email (should block)
+ console.log('\nTest 2: Existing email colin@ikbenlit.nl (should return error)')
+ const test2 = await supabase.rpc('hook_check_duplicate_email', {
+ event: { user: { email: 'colin@ikbenlit.nl' } }
+ })
+ console.log('Result:', test2.data)
+ console.log('Error:', test2.error)
+
+ // Test 3: Case variant (should block)
+ console.log('\nTest 3: Case variant Colin@IkBenLit.nl (should return error)')
+ const test3 = await supabase.rpc('hook_check_duplicate_email', {
+ event: { user: { email: 'Colin@IkBenLit.nl' } }
+ })
+ console.log('Result:', test3.data)
+ console.log('Error:', test3.error)
+
+ // Test 4: Empty email (should block)
+ console.log('\nTest 4: Empty email (should return error)')
+ const test4 = await supabase.rpc('hook_check_duplicate_email', {
+ event: { user: { email: '' } }
+ })
+ console.log('Result:', test4.data)
+ console.log('Error:', test4.error)
+
+ // Check if colin@ikbenlit.nl exists
+ console.log('\nπ Checking if colin@ikbenlit.nl exists in auth.users:')
+ const { data: users, error: usersError } = await supabase.rpc('exec_sql', {
+ sql: "SELECT email, created_at FROM auth.users WHERE lower(email) = 'colin@ikbenlit.nl' LIMIT 1"
+ })
+
+ if (usersError) {
+ console.log('Could not query users directly (expected, requires special permissions)')
+ console.log('Error:', usersError.message)
+ } else {
+ console.log('Users found:', users)
+ }
+}
+
+testHookFunction().catch(console.error)
diff --git a/scripts/test-signup-direct.ts b/scripts/test-signup-direct.ts
new file mode 100644
index 0000000..006c8f0
--- /dev/null
+++ b/scripts/test-signup-direct.ts
@@ -0,0 +1,47 @@
+#!/usr/bin/env tsx
+/**
+ * Test Signup Direct
+ * Test wat Supabase teruggeeft bij duplicate email signup
+ */
+
+import { config } from 'dotenv'
+import { createClient } from '@supabase/supabase-js'
+
+config({ path: '.env.local' })
+
+const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
+const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
+
+const supabase = createClient(supabaseUrl, supabaseAnonKey)
+
+async function testSignup() {
+ console.log('π§ͺ Testing Direct Signup with Existing Email\n')
+ console.log('URL:', supabaseUrl)
+ console.log('Testing with email: colin@ikbenlit.nl\n')
+
+ const { data, error } = await supabase.auth.signUp({
+ email: 'colin@ikbenlit.nl',
+ password: 'TestPassword123!',
+ options: {
+ emailRedirectTo: `http://localhost:3000/auth/callback`
+ }
+ })
+
+ console.log('π Response:')
+ console.log('Data:', JSON.stringify(data, null, 2))
+ console.log('\nError:', JSON.stringify(error, null, 2))
+
+ console.log('\nπ Analysis:')
+ if (error) {
+ console.log('β
Hook is working! Error received:', error.message)
+ } else if (data.user && data.session) {
+ console.log('β οΈ User created with session (email confirmation disabled)')
+ } else if (data.user) {
+ console.log('β οΈ User object returned without session')
+ } else {
+ console.log('β No error but no user - Hook might not be working')
+ console.log(' This is the "silent fail" scenario')
+ }
+}
+
+testSignup().catch(console.error)
diff --git a/supabase/migrations/20251119094908_auth_hook_duplicate_email.sql b/supabase/migrations/20251119094908_auth_hook_duplicate_email.sql
new file mode 100644
index 0000000..ac3b9b2
--- /dev/null
+++ b/supabase/migrations/20251119094908_auth_hook_duplicate_email.sql
@@ -0,0 +1,71 @@
+-- ============================================================================
+-- 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).';