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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user