import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { supabase } from '../lib/supabaseClient'; import useTicketStore from './ticketStore'; const useAuthStore = create( persist( (set, get) => ({ // --- AUTH STATE --- user: null, profile: null, loading: false, // --- SUPABASE AUTH METHODS --- // Helper to fetch profile linked to auth user getProfile: async (user) => { if (!user) return null; const metadata = user.user_metadata || {}; const currentProfile = get().profile; // 1. Resolve FROM METADATA or PERSISTED state // Priority 1: If we have a persisted session for THIS user and it's active, keep it // to prevent temporary lobbies during refresh/tab switching. if (currentProfile && currentProfile.id === user.id && currentProfile.status === 'active') { console.log("Active profile retained from state."); // Background fetch to ensure session is still valid/synced get()._syncProfile(user.id); return currentProfile; } // Priority 2: Use Auth Metadata (Instant fallback) const isMasterAdmin = user.email === 'masteradmin@helpdesk.ai'; const instantProfile = { id: user.id, email: user.email, full_name: isMasterAdmin ? 'Master Admin' : (metadata.full_name || 'User'), role: isMasterAdmin ? 'master_admin' : (metadata.role || 'user'), status: isMasterAdmin ? 'active' : 'pending_email_verification', company: metadata.company || '' }; // 2. Sync with Database First before setting a fallback // This prevents flashes of 'pending_email_verification' when returning from magic links const dbProfile = await get()._syncProfile(user.id); if (dbProfile) { return dbProfile; } console.log("Falling back to instant profile resolved from metadata:", instantProfile.role); set({ profile: instantProfile }); return instantProfile; }, // Helper for DB-side profile syncing _syncProfile: async (userId) => { try { const { data, error } = await supabase .from('profiles') .select('*') .eq('id', userId) .single(); if (data) { console.log("Database profile found, upgrading state."); set({ profile: data }); return data; } if (error && error.code !== 'PGRST116') { console.warn("DB Profile fetch error:", error.message); } } catch (e) { console.error("Background profile fetch error:", e); } return null; }, getCurrentUser: async () => { try { const { data: { user }, error } = await supabase.auth.getUser(); if (error) throw error; if (user) { set({ user }); // Don't 'await' here because we want 'loading: false' ASAP get().getProfile(user); } else { set({ user: null, profile: null }); } return user; // eslint-disable-next-line no-unused-vars } catch (error) { set({ user: null, profile: null }); return null; } finally { set({ loading: false }); } }, login: async (email, password) => { set({ loading: true }); console.log("Attempting login for:", email); try { const { data, error } = await supabase.auth.signInWithPassword({ email, password, }); if (error) throw error; const user = data.user; set({ user }); console.log("Login successful, resolving profile..."); // This will resolve instantly from metadata AND try to update from DB const profile = await get().getProfile(user); // Block login entirely if email is unverified (for both users and admins) if (profile?.status === 'pending_email_verification') { await supabase.auth.signOut(); set({ user: null, profile: null }); throw new Error("Please verify your email address before continuing. Check your inbox."); } return { user, profile }; } catch (error) { console.error("Login operation failed:", error.message); throw error; } finally { set({ loading: false }); } }, signInWithMagicLink: async (email) => { set({ loading: true }); console.log("Attempting magic link / OTP login for:", email); try { const { error } = await supabase.auth.signInWithOtp({ email, options: { shouldCreateUser: false, // Only existing users } }); if (error) throw error; return true; } catch (error) { console.error("Magic link operation failed:", error.message); throw error; } finally { set({ loading: false }); } }, verifyOtpAndLogin: async (email, token, type = 'magiclink') => { set({ loading: true }); console.log("Attempting OTP verification for:", email); try { const { data, error } = await supabase.auth.verifyOtp({ email, token, type, }); if (error) throw error; const user = data.user; set({ user }); console.log("OTP Login successful, resolving profile..."); const profile = await get().getProfile(user); if (profile?.status === 'pending_email_verification') { await supabase.auth.signOut(); set({ user: null, profile: null }); throw new Error("Please verify your email address before continuing."); } return { user, profile }; } catch (error) { console.error("OTP verification failed:", error.message); throw error; } finally { set({ loading: false }); } }, signup: async (email, password, fullName, role = 'user', company = '', extraMetadata = {}, emailRedirectTo = undefined) => { set({ loading: true }); console.log("Starting signup for:", email); try { // 1. Auth Signup with Metadata console.log("Step 1: Auth.signUp..."); const { data, error } = await supabase.auth.signUp({ email, password, options: { data: { full_name: fullName, role: role, company: company, ...extraMetadata }, ...(emailRedirectTo && { emailRedirectTo }) } }); if (error) { console.error("Auth.signUp error:", error.message); throw error; } if (data.user) { console.log("Step 2: User created, resolving profile..."); set({ user: data.user }); await get().getProfile(data.user); } console.log("Signup complete!"); return data.user; } catch (error) { console.error("Signup operation failed:", error.message); throw error; } finally { set({ loading: false }); } }, logout: async () => { set({ loading: true }); try { const { error } = await supabase.auth.signOut(); if (error) throw error; set({ user: null, profile: null }); // clear persisted ticket state to prevent cross-user data leakage useTicketStore.getState().clearTicket(); useTicketStore.setState({ notifications: [], tickets: [] }); } finally { set({ loading: false }); } }, updateProfile: async (updates) => { const { profile } = get(); if (!profile?.id) return; set({ loading: true }); try { const { data, error } = await supabase .from('profiles') .update(updates) .eq('id', profile.id) .select() .single(); if (error) throw error; if (data) { set({ profile: data }); return data; } } catch (err) { console.error("Profile update failed:", err); throw err; } finally { set({ loading: false }); } }, _initialized: false, initialize: () => { if (get()._initialized) return; set({ _initialized: true }); get().getCurrentUser(); supabase.auth.onAuthStateChange(async (event, session) => { console.log("Auth state change:", event); if (session?.user) { set({ user: session.user }); get().getProfile(session.user); } else { set({ user: null, profile: null }); } set({ loading: false }); }); } }), { name: 'auth-storage', partialize: (state) => ({ // We keep profile persisted for quick UI transitions, // but session is handled by Supabase cookie/localStorage profile: state.profile }), } ) ); export default useAuthStore;