import { create } from 'zustand' import { persist } from 'zustand/middleware' import * as Sentry from '@sentry/react' import type { User, Token, UserCreate, UserLogin, Account, SubscriptionDetails } from '@/types' import { authApi } from '@/api/auth' import { identifyUser, resetAnalytics, analytics } from '@/lib/analytics' import { apiClient } from '@/api/client' import { clearCachedQuota } from '@/hooks/useCachedQuota' import { useBillingStore } from '@/store/billingStore' interface AuthState { user: User | null token: Token | null account: Account | null subscription: SubscriptionDetails | null isAuthenticated: boolean isLoading: boolean error: string | null // Actions login: (credentials: UserLogin) => Promise register: (data: UserCreate) => Promise logout: () => Promise fetchUser: () => Promise setTokens: (token: Token) => void clearError: () => void setLoading: (loading: boolean) => void } export const useAuthStore = create()( persist( (set, get) => ({ user: null, token: null, account: null, subscription: null, isAuthenticated: false, isLoading: false, error: null, login: async (credentials: UserLogin) => { set({ isLoading: true, error: null }) try { const token = await authApi.login(credentials) // Store tokens localStorage.setItem('access_token', token.access_token) localStorage.setItem('refresh_token', token.refresh_token) set({ token, isAuthenticated: true }) // Fetch user info await get().fetchUser() analytics.loginSuccess() } catch (error: unknown) { const axiosErr = error as { response?: { data?: { detail?: unknown } } } const rawDetail = axiosErr.response?.data?.detail const message = (typeof rawDetail === 'string' ? rawDetail : null) || (error instanceof Error ? error.message : 'Login failed') set({ error: message, isLoading: false }) throw error } }, register: async (data: UserCreate) => { set({ isLoading: true, error: null }) try { await authApi.register(data) analytics.accountCreated() // After registration, log the user in await get().login({ email: data.email, password: data.password }) } catch (error: unknown) { const axiosErr = error as { response?: { data?: { detail?: unknown } } } const rawDetail = axiosErr.response?.data?.detail const message = (typeof rawDetail === 'string' ? rawDetail : null) || (error instanceof Error ? error.message : 'Registration failed') set({ error: message, isLoading: false }) throw error } }, logout: async () => { try { await authApi.logout() } catch { // Ignore logout errors } finally { localStorage.removeItem('access_token') localStorage.removeItem('refresh_token') clearCachedQuota() useBillingStore.getState().reset() Sentry.setUser(null) resetAnalytics() set({ user: null, token: null, account: null, subscription: null, isAuthenticated: false, error: null }) } }, fetchUser: async () => { set({ isLoading: true }) try { const [userResult, accountResult, subscriptionResult] = await Promise.allSettled([ authApi.me(), apiClient.get('/accounts/me').then(r => r.data), apiClient.get('/accounts/me/subscription').then(r => r.data), ]) const user = userResult.status === 'fulfilled' ? userResult.value : null const account = accountResult.status === 'fulfilled' ? accountResult.value : null const subscription = subscriptionResult.status === 'fulfilled' ? subscriptionResult.value : null if (!user) { // User fetch failed — propagate the error const reason = userResult.status === 'rejected' ? userResult.reason : new Error('Failed to fetch user') throw reason } // Set Sentry user context for error attribution Sentry.setUser({ id: user.id, email: user.email }) // Identify user in PostHog for product analytics identifyUser({ id: user.id, email: user.email, role: user.role, is_super_admin: user.is_super_admin, account_id: account?.id }) set({ user, account, subscription, isLoading: false }) // Kick off billing-state fetch alongside auth — fire-and-forget so // a billing error never breaks login. The billing store records // its own error state. void useBillingStore.getState().fetch() } catch (error: unknown) { const message = error instanceof Error ? error.message : 'Failed to fetch user' set({ error: message, isLoading: false }) throw error } }, // Storing tokens implies an active session — mark the store as // authenticated so doesn't bounce the user back to // /landing while fetchUser() is still inflight (e.g. immediately after // the OAuth callback exchange). The refresh interceptor in api/client.ts // also calls this; that path is already authenticated, so flipping the // flag has no effect there. setTokens: (token: Token) => set({ token, isAuthenticated: true }), clearError: () => set({ error: null }), setLoading: (loading: boolean) => set({ isLoading: loading }), }), { name: 'auth-storage', partialize: (state) => ({ token: state.token, isAuthenticated: state.isAuthenticated, account: state.account, subscription: state.subscription, }), } ) ) export default useAuthStore