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:
71
scripts/apply-migration.ts
Normal file
71
scripts/apply-migration.ts
Normal file
@@ -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)
|
||||
95
scripts/setup-auth-hook.ts
Normal file
95
scripts/setup-auth-hook.ts
Normal file
@@ -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)
|
||||
72
scripts/test-hook-function.ts
Normal file
72
scripts/test-hook-function.ts
Normal file
@@ -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)
|
||||
47
scripts/test-signup-direct.ts
Normal file
47
scripts/test-signup-direct.ts
Normal file
@@ -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)
|
||||
Reference in New Issue
Block a user