"use client"; import { createContext, useContext, useState, useEffect, ReactNode } from "react"; import { supabase, ensureValidSession } from "@/lib/supabase"; import bcrypt from "bcryptjs"; interface NotificationPreferences { emailUpdates: boolean; buildLikes: boolean; comments: boolean; followers: boolean; newsletter: boolean; } interface PrivacySettings { profilePublic: boolean; showBuilds: boolean; } interface CustomUser { user_id: number; user_name: string; email: string; user_type: "admin" | "user" | "moderator"; created_at: string; avatar_url: string | null; supabase_id: string | null; bio: string | null; website: string | null; location: string | null; privacy_settings: PrivacySettings | null; notification_preferences: NotificationPreferences | null; } interface AuthContextType { user: CustomUser | null; updateUser: (data: Partial) => void; login: (email: string, password: string, rememberMe?: boolean, skipVerification?: boolean) => Promise<{ success: boolean; error?: string; requiresVerification?: boolean; userId?: number }>; sendLoginVerificationCode: (email: string, userId: number) => Promise<{ success: boolean; error?: string }>; verifyLoginCode: (email: string, code: string) => Promise<{ success: boolean; error?: string }>; register: (username: string, email: string, password: string, verificationToken: string) => Promise<{ success: boolean; error?: string }>; sendVerificationCode: (email: string) => Promise<{ success: boolean; error?: string }>; verifyCode: (email: string, code: string) => Promise<{ success: boolean; token?: string; error?: string }>; sendEmailChangeCode: (userId: number, newEmail: string) => Promise<{ success: boolean; error?: string }>; verifyEmailChangeCode: (userId: number, newEmail: string, code: string) => Promise<{ success: boolean; error?: string }>; sendPasswordChangeCode: (userId: number) => Promise<{ success: boolean; error?: string; message?: string }>; verifyPasswordChangeCode: (userId: number, code: string, newPassword: string, confirmNewPassword: string) => Promise<{ success: boolean; error?: string; message?: string }>; updatePassword: (newPassword: string) => Promise<{ success: boolean; error?: string }>; logout: () => Promise; isLoading: boolean; } const AuthContext = createContext(undefined); // helper to load privacy settings from dedicated table, falling back to JSON async function loadPrivacySettings(userId: number) { try { const { data: priv } = await supabase .from('privacy_settings') .select('profile_public, show_builds') .eq('user_id', userId) .maybeSingle(); if (priv) { return { profilePublic: priv.profile_public, showBuilds: priv.show_builds }; } } catch (err) { console.error('Error loading privacy settings:', err); } return null; } export function SupabaseAuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState(null); const [isLoading, setIsLoading] = useState(true); const updateUser = (data: Partial) => { setUser((prev) => { if (!prev) return prev return { ...prev, ...data } }) } // -------------------------- // Load session on startup // -------------------------- useEffect(() => { const init = async () => { try { const { data: { session }, error } = await supabase.auth.getSession(); if (error) { console.error('Error getting session:', error); // If it's a JWT expiration error, try to refresh if (error.message?.includes('JWT') || error.message?.includes('expired')) { console.log('🔄 Attempting to refresh expired session...'); const { data: { session: refreshedSession }, error: refreshError } = await supabase.auth.refreshSession(); if (!refreshError && refreshedSession) { // Retry with refreshed session const { data: profile } = await supabase .from("users") .select("*") .eq("supabase_id", refreshedSession.user.id) .single(); if (profile) { // merge privacy from dedicated table const privacy = await loadPrivacySettings(profile.user_id); const profileData = { ...profile, privacy_settings: privacy, notification_preferences: profile.notification_preferences ? JSON.parse(profile.notification_preferences) : null, }; setUser(profileData as CustomUser); console.log('✅ Session refreshed and profile loaded:', profile.user_name, 'privacy:', privacy); setIsLoading(false); return; } } } setIsLoading(false); return; } if (session) { // Check if session is expired const expiresAt = session.expires_at; if (expiresAt && expiresAt * 1000 < Date.now()) { console.log('âš ī¸ Session expired, refreshing...'); const { data: { session: refreshedSession }, error: refreshError } = await supabase.auth.refreshSession(); if (refreshError || !refreshedSession) { console.error('❌ Failed to refresh expired session:', refreshError); setUser(null); setIsLoading(false); return; } // Use refreshed session const supabaseUser = refreshedSession.user; const { data: profile, error: profileError } = await supabase .from("users") .select("*") .eq("supabase_id", supabaseUser.id) .single(); if (profileError) { // Check if it's a JWT expiration error if (profileError.message?.includes('JWT') || profileError.message?.includes('expired') || profileError.code === 'PGRST301') { console.warn('âš ī¸ JWT expired during profile fetch - attempting to refresh session'); // Try to refresh the session const { data: { session: newSession }, error: refreshError } = await supabase.auth.refreshSession(); if (refreshError || !newSession) { console.error('❌ Failed to refresh session:', refreshError); setUser(null); setIsLoading(false); return; } // Retry profile fetch with refreshed session const { data: retryProfile } = await supabase .from("users") .select("*") .eq("supabase_id", newSession.user.id) .single(); if (retryProfile) { setUser(retryProfile as CustomUser); console.log('✅ Profile loaded after session refresh:', retryProfile.user_name); } } else { // Check if error has meaningful content const hasMessage = profileError?.message && typeof profileError.message === 'string' && profileError.message.trim().length > 0; const hasCode = profileError?.code && typeof profileError.code === 'string' && profileError.code.trim().length > 0; const hasDetails = profileError?.details && typeof profileError.details === 'string' && profileError.details.trim().length > 0; const hasHint = profileError?.hint && typeof profileError.hint === 'string' && profileError.hint.trim().length > 0; // Check if error object is truly empty const errorKeys = Object.keys(profileError).filter(key => profileError[key] !== undefined && profileError[key] !== null && profileError[key] !== ''); const isEmpty = errorKeys.length === 0 || (!hasMessage && !hasCode && !hasDetails && !hasHint); const isNotFound = profileError.code === 'PGRST116'; // Only log if error has meaningful content if (!isEmpty && !isNotFound) { console.error('Error fetching profile (first check):', { message: profileError.message || 'N/A', code: profileError.code || 'N/A', details: profileError.details || 'N/A' }); } // Clear session silently await supabase.auth.signOut(); } } else if (profile) { const privacy = await loadPrivacySettings(profile.user_id); const profileData = { ...profile, privacy_settings: privacy, notification_preferences: profile.notification_preferences ? JSON.parse(profile.notification_preferences) : null, }; setUser(profileData as CustomUser); console.log('✅ Session refreshed and profile loaded:', profile.user_name, 'privacy:', privacy); } } else { // Session is valid, proceed normally const supabaseUser = session.user; const { data: profile, error: profileError } = await supabase .from("users") .select("*") .eq("supabase_id", supabaseUser.id) .single(); if (profileError) { // Check if error has meaningful content const hasMessage = profileError?.message && typeof profileError.message === 'string' && profileError.message.trim().length > 0; const hasCode = profileError?.code && typeof profileError.code === 'string' && profileError.code.trim().length > 0; const hasDetails = profileError?.details && typeof profileError.details === 'string' && profileError.details.trim().length > 0; const hasHint = profileError?.hint && typeof profileError.hint === 'string' && profileError.hint.trim().length > 0; // Check if error object is truly empty const errorKeys = Object.keys(profileError).filter(key => profileError[key] !== undefined && profileError[key] !== null && profileError[key] !== ''); const isEmpty = errorKeys.length === 0 || (!hasMessage && !hasCode && !hasDetails && !hasHint); const isNotFound = profileError.code === 'PGRST116'; // ONLY log if error has actual meaningful content if (!isEmpty && !isNotFound && (hasMessage || hasCode || hasDetails || hasHint)) { console.error('Error fetching profile (second check):', { message: profileError.message || 'N/A', code: profileError.code || 'N/A', details: profileError.details || 'N/A' }); } // Clear session silently await supabase.auth.signOut(); } else if (profile) { const privacy = await loadPrivacySettings(profile.user_id); const profileData = { ...profile, privacy_settings: privacy, notification_preferences: profile.notification_preferences || null, }; setUser(profileData as CustomUser); console.log('✅ Session restored - user logged in:', profile.user_name, 'privacy:', privacy); } } } else { console.log('â„šī¸ No active session found'); } } catch (err) { console.error('Session initialization error:', err); } finally { setIsLoading(false); } }; init(); // Listen for auth state changes, but only clear user on explicit sign out const { data: listener } = supabase.auth.onAuthStateChange(async (event, session) => { console.log('Auth state changed:', event, session ? 'session exists' : 'no session'); if (event === 'SIGNED_OUT') { // Only clear user on explicit sign out console.log('🔴 User signed out - clearing session'); setUser(null); localStorage.removeItem('buildmate-remember-me'); localStorage.removeItem('buildmate-session-expires'); } else if (event === 'TOKEN_REFRESHED') { // Token refreshed - keep user logged in and reload profile if needed console.log('🔄 Session token refreshed - keeping user logged in'); if (session?.user) { try { const { data: profile } = await supabase .from("users") .select("*") .eq("supabase_id", session.user.id) .single(); if (profile) { const profileData = { ...profile, privacy_settings: profile.privacy_settings ? JSON.parse(profile.privacy_settings) : null, notification_preferences: profile.notification_preferences ? JSON.parse(profile.notification_preferences) : null, }; setUser(profileData as CustomUser); console.log('✅ Profile reloaded after token refresh:', profile.user_name); } } catch (error) { console.error('Error reloading profile after token refresh:', error); } } } else if (event === "USER_UPDATED" && session?.user) { console.log("🔄 USER_UPDATED detected"); // Just refresh local state setUser(prev => prev ? { ...prev, email: session.user.email ?? prev.email } : prev ); console.log("✅ Local state refreshed after USER_UPDATED"); } else if (event === 'SIGNED_IN' || event === 'INITIAL_SESSION') { if (!session?.user) return; try { const { data: profile, error: profileError } = await supabase .from("users") .select("*") .eq("supabase_id", session.user.id) .single(); if (profileError) { console.error('Error fetching profile (auth state change):', profileError); return; } if (profile) { const privacy = await loadPrivacySettings(profile.user_id); const profileData = { ...profile, privacy_settings: privacy, notification_preferences: typeof profile.notification_preferences === "object" ? profile.notification_preferences : null, }; setUser(profileData as CustomUser); console.log('✅ User session active:', profile.user_name, 'privacy:', privacy); } } catch (error) { console.error('Error in auth state change handler:', error); } } // Don't clear user on other events (like SIGNED_IN, USER_UPDATED, etc.) }); return () => listener.subscription.unsubscribe(); }, []); // -------------------------- // SEND VERIFICATION CODE // -------------------------- const sendVerificationCode = async (email: string) => { try { const response = await fetch('/api/auth/send-verification-code', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email }), }) // Check if response is OK and has content if (!response.ok) { const errorText = await response.text() try { const errorData = JSON.parse(errorText) return { success: false, error: errorData.error || errorData.message || 'Failed to send verification code' } } catch { return { success: false, error: errorText || `Server error: ${response.status}` } } } // Check if response has content before parsing const contentType = response.headers.get('content-type') if (!contentType || !contentType.includes('application/json')) { const text = await response.text() return { success: false, error: text || 'Invalid response from server' } } const data = await response.json() return data } catch (err: any) { console.error('Send verification code error:', err) return { success: false, error: err.message || 'Failed to send verification code. Please try again.' } } } // -------------------------- // VERIFY CODE // -------------------------- const verifyCode = async (email: string, code: string) => { try { const response = await fetch('/api/auth/verify-code', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, code }), }) // Check if response is OK and has content if (!response.ok) { const errorText = await response.text() try { const errorData = JSON.parse(errorText) return { success: false, error: errorData.error || errorData.message || 'Invalid verification code' } } catch { return { success: false, error: errorText || `Server error: ${response.status}` } } } // Check if response has content before parsing const contentType = response.headers.get('content-type') if (!contentType || !contentType.includes('application/json')) { const text = await response.text() return { success: false, error: text || 'Invalid response from server' } } const data = await response.json() return data } catch (err: any) { console.error('Verify code error:', err) return { success: false, error: err.message || 'Failed to verify code. Please try again.' } } } // -------------------------- // REGISTER USER // -------------------------- const register = async ( username: string, email: string, password: string, verificationToken: string ) => { try { const emailKey = email.trim().toLowerCase(); // 1ī¸âƒŖ Check token in signup_verification_codes table const { data: codeEntry, error: tokenError } = await supabase .from('signup_verification_codes') .select('*') .eq('email', emailKey) .eq('token', verificationToken) .eq('verified', true) .maybeSingle(); if (tokenError) { console.error('Supabase token lookup error:', tokenError); return { success: false, error: 'Failed to verify token' }; } if (!codeEntry) { return { success: false, error: 'Email verification required. Please verify your email first.' }; } if (codeEntry.token_expires_at && codeEntry.token_expires_at < Date.now()) { return { success: false, error: 'Verification token has expired. Please request a new code.' }; } // 2ī¸âƒŖ Create Supabase auth user const { data: authData, error: authError } = await supabase.auth.signUp({ email: emailKey, password }); if (authError) { console.error('SUPABASE SIGNUP ERROR:', authError); return { success: false, error: authError.message }; } const supabaseId = authData.user?.id; if (!supabaseId) return { success: false, error: 'Supabase user missing!' }; // 3ī¸âƒŖ Hash password for custom users table const hashedPassword = await bcrypt.hash(password, 12); // 4ī¸âƒŖ Insert user into custom users table const { data: userRows, error: insertError } = await supabase .from('users') .insert({ user_name: username, email: emailKey, password: hashedPassword, user_type: 'user', avatar_url: '', supabase_id: supabaseId }) .select('user_id'); if (insertError) { console.error('Error inserting into users table:', insertError); return { success: false, error: insertError.message }; } const newUserId = userRows?.[0]?.user_id; if (newUserId) { // 5ī¸âƒŖ Insert default notification preferences const defaultPrefs = { build_likes: true, comments: true, followers: true, newsletter: false, email_updates: false, }; await supabase.from('notification_preferences').insert({ user_id: newUserId, ...defaultPrefs, }); // 6ī¸âƒŖ Insert default privacy settings await supabase.from('privacy_settings').insert({ user_id: newUserId, profile_public: true, show_builds: true, }); // 7ī¸âƒŖ Optionally send welcome email try { await fetch('/api/emails/send-welcome-email', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: emailKey, username }), }); console.log('✅ Welcome email sent'); } catch (e) { console.error('Failed to send welcome email:', e); } } // 8ī¸âƒŖ Optionally mark verification token as used (so it can't be reused) await supabase .from('signup_verification_codes') .update({ token_expires_at: Date.now() }) .eq('id', codeEntry.id); // Registration complete — do NOT auto-login return { success: true, message: 'Account created successfully! You can now login.' }; } catch (err: any) { console.error('Register error:', err); return { success: false, error: err.message || 'Internal server error' }; } }; // -------------------------- // SEND LOGIN VERIFICATION CODE // -------------------------- const sendLoginVerificationCode = async (email: string, userId: number) => { try { const response = await fetch('/api/auth/send-login-verification-code', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, userId }), }) if (!response.ok) { const errorText = await response.text() try { const errorData = JSON.parse(errorText) return { success: false, error: errorData.error || 'Failed to send verification code' } } catch { return { success: false, error: errorText || `Server error: ${response.status}` } } } const contentType = response.headers.get('content-type') if (!contentType || !contentType.includes('application/json')) { const text = await response.text() return { success: false, error: text || 'Invalid response from server' } } const data = await response.json() return data } catch (err: any) { console.error('Send login verification code error:', err) return { success: false, error: err.message || 'Failed to send verification code. Please try again.' } } } // -------------------------- // VERIFY LOGIN CODE // -------------------------- const verifyLoginCode = async (email: string, code: string) => { try { const response = await fetch('/api/auth/verify-login-code', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, code }), }) if (!response.ok) { const errorText = await response.text() try { const errorData = JSON.parse(errorText) return { success: false, error: errorData.error || 'Invalid verification code' } } catch { return { success: false, error: errorText || `Server error: ${response.status}` } } } const contentType = response.headers.get('content-type') if (!contentType || !contentType.includes('application/json')) { const text = await response.text() return { success: false, error: text || 'Invalid response from server' } } const data = await response.json() return data } catch (err: any) { console.error('Verify login code error:', err) return { success: false, error: err.message || 'Failed to verify code. Please try again.' } } } // -------------------------- // LOGIN USER // -------------------------- const login = async ( email: string, password: string, rememberMe: boolean = false, skipVerification: boolean = false ) => { setIsLoading(true); try { const { data: authData, error } = await supabase.auth.signInWithPassword({ email, password }); if (error) return { success: false, error: error.message }; const supabaseId = authData.user.id; const { data: profile } = await supabase .from("users") .select("*") .eq("supabase_id", supabaseId) .single(); if (!profile) return { success: false, error: "Profile not found" }; if (!skipVerification) { await supabase.auth.signOut(); return { success: true, requiresVerification: true, userId: profile.user_id }; } const privacy = await loadPrivacySettings(profile.user_id); const profileData = { ...profile, privacy_settings: privacy, notification_preferences: profile.notification_preferences || null, }; setUser(profileData as CustomUser); // Remember me if (rememberMe) { localStorage.setItem('buildmate-remember-me', 'true') const expirationDate = new Date() expirationDate.setDate(expirationDate.getDate() + 30) localStorage.setItem('buildmate-session-expires', expirationDate.toISOString()) } else { localStorage.setItem('buildmate-remember-me', 'false') localStorage.removeItem('buildmate-session-expires') } return { success: true, userType: profile.user_type }; // <-- return user_type } finally { setIsLoading(false); } }; // -------------------------- // SEND EMAIL CHANGE CODE // -------------------------- const sendEmailChangeCode = async (userId: number, newEmail: string) => { try { const response = await fetch('/api/auth/send-email-change-code', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId, newEmail }), }); if (!response.ok) { const text = await response.text(); try { const data = JSON.parse(text); return { success: false, error: data.error || 'Failed to send email change code' }; } catch { return { success: false, error: text || `Server error: ${response.status}` }; } } const data = await response.json(); return data; } catch (err: any) { console.error('Send email change code error:', err); return { success: false, error: err.message || 'Failed to send email change code' }; } }; // -------------------------- // VERIFY EMAIL CHANGE CODE // -------------------------- const verifyEmailChangeCode = async (userId: number, newEmail: string, code: string) => { try { const response = await fetch('/api/auth/verify-email-change-code', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId, newEmail, code }), }); if (!response.ok) { const text = await response.text(); try { const data = JSON.parse(text); return { success: false, error: data.error || 'Failed to verify email change code' }; } catch { return { success: false, error: text || `Server error: ${response.status}` }; } } const data = await response.json(); // Update local state immediately if (data.success) { setUser((prev) => prev ? { ...prev, email: newEmail } : prev); } return data; } catch (err: any) { console.error('Verify email change code error:', err); return { success: false, error: err.message || 'Failed to verify email change code' }; } }; // -------------------------- // SEND PASSWORD CHANGE CODE // -------------------------- const sendPasswordChangeCode = async (userId: number) => { try { const response = await fetch('/api/auth/send-password-change-code', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId }), }); if (!response.ok) { const text = await response.text(); try { const data = JSON.parse(text); return { success: false, error: data.error || 'Failed to send password change code' }; } catch { return { success: false, error: text || `Server error: ${response.status}` }; } } const data = await response.json(); return data; } catch (err: any) { console.error('Send password change code error:', err); return { success: false, error: err.message || 'Failed to send password change code' }; } }; // -------------------------- // VERIFY PASSWORD CHANGE CODE // -------------------------- const verifyPasswordChangeCode = async (userId: number, code: string, newPassword: string, confirmNewPassword: string) => { try { const response = await fetch('/api/auth/verify-password-change-code', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId, code, newPassword, confirmNewPassword }), }); if (!response.ok) { const text = await response.text(); try { const data = JSON.parse(text); return { success: false, error: data.error || 'Failed to verify password change code' }; } catch { return { success: false, error: text || `Server error: ${response.status}` }; } } const data = await response.json(); return data; } catch (err: any) { console.error('Verify password change code error:', err); return { success: false, error: err.message || 'Failed to verify password change code' }; } }; // -------------------------- // UPDATE PASSWORD // -------------------------- const updatePassword = async (newPassword: string) => { setIsLoading(true); try { const { data, error } = await supabase.auth.updateUser({ password: newPassword }); if (error) return { success: false, error: error.message }; console.log('🔹 Supabase updateUser response:', data); console.log('✅ Password update requested — USER_UPDATED will sync state'); // Optional: update email locally if you want setUser(prev => prev ? { ...prev, email: data.user?.email ?? prev.email } : prev); // Return success immediately return { success: true }; } catch (err: any) { console.error('Unexpected error updating password:', err); return { success: false, error: err.message || 'Failed to update password' }; } finally { setIsLoading(false); } }; // -------------------------- // LOGOUT // -------------------------- const logout = async () => { setIsLoading(true); try { console.log('🔴 Logging out user...'); await supabase.auth.signOut(); setUser(null); // Clear remember me preferences localStorage.removeItem('buildmate-remember-me'); localStorage.removeItem('buildmate-session-expires'); console.log('✅ User logged out successfully'); } catch (error) { console.error('Logout error:', error); // Still clear local state even if signOut fails setUser(null); localStorage.removeItem('buildmate-remember-me'); localStorage.removeItem('buildmate-session-expires'); } finally { setIsLoading(false); } }; return ( {children} ); } export function useAuth() { const context = useContext(AuthContext); if (!context) throw new Error("useAuth must be used within SupabaseAuthProvider"); return context; }