import { create } from 'zustand'; import api, { setOnUnauthorizedCallback } from '@/lib/api'; /** * User type matching the API response */ interface User { user_id: number; email?: string; firstName?: string | null; lastName?: string | null; phone?: string | null; max_role?: string | number; privileges?: string; } /** * Auth Store State * Manages authentication state using Zustand */ interface AuthState { user: User | null; isLoading: boolean; isInitializing: boolean; isAuthenticated: boolean; error: string | null; } /** * Auth Store Actions * All auth-related operations */ interface AuthActions { // Authentication flow checkEmail: (email: string) => Promise<{ exists: boolean; name?: string | null }>; requestOtp: (email: string) => Promise<{ success: boolean }>; verifyOtp: (email: string, code: string) => Promise; logout: () => Promise; // State management clearError: () => void; refreshAuth: () => Promise; updateUser: (updates: Partial) => void; // Internal helpers _checkAuth: () => Promise; _setUnauthorizedCallback: () => void; } /** * Combined Auth Store Type */ type AuthStore = AuthState & AuthActions; /** * Initial state for auth store */ const initialState: AuthState = { user: null, isLoading: false, isInitializing: true, isAuthenticated: false, error: null, }; /** * Zustand Auth Store for Web * * Manages authentication state with localStorage for web browsers. * * @example * ```tsx * import { useAuthStore } from '@/stores/authStore'; * * function MyComponent() { * const { user, isAuthenticated, logout } = useAuthStore(); * * return ( *
* {isAuthenticated ? ( *

Welcome {user?.firstName}

* ) : ( *

Please login

* )} *
* ); * } * ``` */ export const useAuthStore = create((set, get) => ({ ...initialState, /** * Internal: Check authentication status on app start * Called automatically when store initializes */ _checkAuth: async () => { try { const isAuth = await api.isAuthenticated(); if (isAuth) { const user = await api.getStoredUser(); set({ user, isLoading: false, isInitializing: false, isAuthenticated: !!user, error: null, }); // Set cookie for middleware if (user) { const token = await api.getToken(); if (token) { document.cookie = `accessToken=${token}; path=/; max-age=${60 * 60 * 24 * 7}; SameSite=Lax`; } } } else { set({ user: null, isLoading: false, isInitializing: false, isAuthenticated: false, error: null, }); // Clear cookie document.cookie = 'accessToken=; path=/; max-age=0'; } } catch { set({ user: null, isLoading: false, isInitializing: false, isAuthenticated: false, error: 'Failed to check authentication', }); // Clear cookie on error document.cookie = 'accessToken=; path=/; max-age=0'; } }, /** * Internal: Setup callback for 401 unauthorized responses * Called automatically when store initializes */ _setUnauthorizedCallback: () => { setOnUnauthorizedCallback(() => { api.logout().then(() => { set({ user: null, isLoading: false, isInitializing: false, isAuthenticated: false, error: 'Session expired. Please login again.', }); // Clear cookie document.cookie = 'accessToken=; path=/; max-age=0'; // Redirect to login if (typeof window !== 'undefined') { window.location.href = '/login'; } }); }); }, /** * Check if email exists in database * Step 1 of OTP flow */ checkEmail: async (email: string) => { set({ isLoading: true, error: null }); try { const response = await api.checkEmail(email); if (response.ok && response.data) { set({ isLoading: false }); return { exists: response.data.exists, name: response.data.name }; } // API failed - assume new user set({ isLoading: false }); return { exists: false }; } catch { set({ isLoading: false, error: 'Network error. Please check your connection.', }); return { exists: false }; } }, /** * Request OTP code via email * Step 2 of OTP flow */ requestOtp: async (email: string) => { set({ isLoading: true, error: null }); try { const response = await api.requestOTP(email); if (response.ok) { set({ isLoading: false }); return { success: true }; } // API failed set({ isLoading: false, error: 'Failed to send verification code. Please try again.', }); return { success: false }; } catch { set({ isLoading: false, error: 'Network error. Please check your connection.', }); return { success: false }; } }, /** * Verify OTP code and authenticate user * Step 3 of OTP flow - completes authentication */ verifyOtp: async (email: string, code: string) => { set({ isLoading: true, error: null }); try { const verifyResponse = await api.verifyOTP(email, code); if (verifyResponse.ok && verifyResponse.data) { const user: User = { user_id: parseInt(verifyResponse.data.user.id, 10), email: email, firstName: verifyResponse.data.user.first_name || null, lastName: verifyResponse.data.user.last_name || null, max_role: 'USER', privileges: '', }; set({ user, isLoading: false, isInitializing: false, isAuthenticated: true, error: null, }); // Set cookie for middleware document.cookie = `accessToken=${verifyResponse.data.token}; path=/; max-age=${60 * 60 * 24 * 7}; SameSite=Lax`; return true; } // Wrong OTP code set({ isLoading: false, error: 'Invalid verification code. Please try again.', }); return false; } catch (error) { set({ isLoading: false, error: error instanceof Error ? error.message : 'Verification failed', }); return false; } }, /** * Logout user and clear all auth state * Calls api.logout() which clears localStorage tokens */ logout: async () => { set({ isLoading: true }); try { await api.logout(); } finally { set({ user: null, isLoading: false, isInitializing: false, isAuthenticated: false, error: null, }); // Clear cookie document.cookie = 'accessToken=; path=/; max-age=0'; // Redirect to login if (typeof window !== 'undefined') { window.location.href = '/login'; } } }, /** * Clear error state * Used to dismiss error messages */ clearError: () => { set({ error: null }); }, /** * Refresh authentication state from storage * Re-checks tokens and user data */ refreshAuth: async () => { await get()._checkAuth(); }, /** * Update user profile in state * Only updates local state - does NOT call API * Use api.updateProfile() separately to persist changes */ updateUser: (updates: Partial) => { const currentUser = get().user; if (!currentUser) return; set({ user: { ...currentUser, ...updates }, }); }, })); /** * Initialize auth store * Call this once at app startup (in layout.tsx or _app.tsx) * * @example * ```tsx * // In app/layout.tsx * 'use client'; * import { useEffect } from 'react'; * import { initAuthStore } from '@/stores/authStore'; * * export default function RootLayout() { * useEffect(() => { * initAuthStore(); * }, []); * } * ``` */ export function initAuthStore() { const store = useAuthStore.getState(); store._setUnauthorizedCallback(); store._checkAuth(); } /** * Selector hooks for optimized re-renders * Use these instead of full store when you only need specific values */ export const useAuthUser = () => useAuthStore((state) => state.user); export const useIsAuthenticated = () => useAuthStore((state) => state.isAuthenticated); export const useAuthLoading = () => useAuthStore((state) => state.isLoading); export const useAuthError = () => useAuthStore((state) => state.error);