Files
triqura-ecd/lib/auth/client.ts
2025-11-19 14:23:54 +01:00

271 lines
8.4 KiB
TypeScript

/**
* Supabase Auth Client Utilities
*
* Client-side auth helpers for login, logout, and session management
*/
import { createBrowserClient } from '@supabase/ssr'
import type { Database } from '@/lib/database.types'
/**
* Create Supabase client for browser/client-side use
*/
export function createClient() {
return createBrowserClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
/**
* 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 signUpWithPassword(email: string, password: string) {
const supabase = createClient()
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: `${window.location.origin}/auth/callback`
}
})
// 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
}
/**
* Send password reset email
*/
export async function resetPasswordForEmail(email: string) {
const supabase = createClient()
// Force Supabase to bounce back via the auth callback with next=/update-password
const params = new URLSearchParams({ next: '/update-password' })
const redirectTo = `${window.location.origin}/auth/callback?${params.toString()}`
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo
})
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()
const { data, error } = await supabase.auth.signInWithPassword({
email,
password
})
if (error) {
// Provide user-friendly error messages
const errorMessage = error.message?.toLowerCase() || ''
// Invalid login credentials (account doesn't exist OR wrong password)
if (errorMessage.includes('invalid login credentials') ||
errorMessage.includes('invalid credentials') ||
error.status === 400) {
const friendlyError = new Error('Email of wachtwoord is onjuist. Controleer je gegevens en probeer opnieuw.')
;(friendlyError as any).code = 'invalid_credentials'
throw friendlyError
}
// Email not confirmed
if (errorMessage.includes('email not confirmed')) {
const friendlyError = new Error('Je email is nog niet geverifieerd. Check je inbox voor de verificatie link.')
;(friendlyError as any).code = 'email_not_confirmed'
throw friendlyError
}
// Generic error fallback
throw error
}
return {
success: true,
user: data.user,
session: data.session
}
}
/**
* Logout current user
*/
export async function logout() {
const supabase = createClient()
const { error } = await supabase.auth.signOut()
if (error) throw error
// Redirect to login
window.location.href = '/login'
}
/**
* Get current session
*/
export async function getSession() {
const supabase = createClient()
const { data: { session }, error } = await supabase.auth.getSession()
if (error) throw error
return session
}
/**
* Get current user
*/
export async function getUser() {
const supabase = createClient()
const { data: { user }, error } = await supabase.auth.getUser()
if (error) throw error
return user
}
/**
* Check if user is authenticated
*/
export async function isAuthenticated() {
const session = await getSession()
return !!session
}
/**
* Subscribe to auth state changes
*/
export function onAuthStateChange(
callback: (event: string, session: any) => void
) {
const supabase = createClient()
const { data: { subscription } } = supabase.auth.onAuthStateChange(
callback
)
return subscription
}
/**
* Check if current user is a demo user
* @deprecated Demo users are no longer supported
*/
export async function isDemoUser(): Promise<boolean> {
return false
}
/**
* Get demo user access level
* @deprecated Demo users are no longer supported
*/
export async function getDemoAccessLevel(): Promise<'read_only' | 'interactive' | 'presenter' | null> {
return null
}