Oliver-ai-bot_2.0/frontend/store/useAuthStore.ts
Vadym Samoilenko 94e3c66167 Fix spinner, OLIVER brand redesign, Cloud Run document processor
- Fix eternal Loading spinner: track Zustand hydration state, wait for
  both hydration and pathname before auth redirect decisions
- OLIVER Brand: Montserrat font, gold/orange/black palette, 4px radius,
  dark mode default, OLIVER wordmark logo, remove blue/purple gradients
- Cloud Run HTTP service for document processing (cloud_run_service.py,
  Dockerfile.cloud-run) with OIDC auth client
- Knowledge endpoints: delegate to Cloud Run when CLOUD_RUN_PROCESSOR_URL
  is set, fallback to Celery
- Gitignore: junk files, context handover docs, root node_modules

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 10:37:21 +00:00

107 lines
2.8 KiB
TypeScript

// ============================================
// Authentication Store (Zustand with Persistence)
// ============================================
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import type { User } from '@/types';
interface AuthState {
// State
user: User | null;
accessToken: string | null;
refreshToken: string | null;
isAuthenticated: boolean;
_hasHydrated: boolean;
// Actions
login: (user: User, accessToken: string, refreshToken: string) => void;
logout: () => void;
updateAccessToken: (accessToken: string) => void;
updateUser: (user: User) => void;
setHasHydrated: (v: boolean) => void;
}
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
// Initial state
user: null,
accessToken: null,
refreshToken: null,
isAuthenticated: false,
_hasHydrated: false,
// Login action
login: (user, accessToken, refreshToken) => {
set({
user,
accessToken,
refreshToken,
isAuthenticated: true,
});
// Also store in localStorage for API client
if (typeof window !== 'undefined') {
localStorage.setItem('access_token', accessToken);
localStorage.setItem('refresh_token', refreshToken);
localStorage.setItem('user', JSON.stringify(user));
}
},
// Logout action
logout: () => {
set({
user: null,
accessToken: null,
refreshToken: null,
isAuthenticated: false,
});
// Clear localStorage
if (typeof window !== 'undefined') {
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
localStorage.removeItem('user');
}
},
// Update access token (used during token refresh)
updateAccessToken: (accessToken) => {
set({ accessToken });
// Update localStorage
if (typeof window !== 'undefined') {
localStorage.setItem('access_token', accessToken);
}
},
// Update user (used when fetching updated profile)
updateUser: (user) => {
set({ user });
// Update localStorage
if (typeof window !== 'undefined') {
localStorage.setItem('user', JSON.stringify(user));
}
},
setHasHydrated: (v) => {
set({ _hasHydrated: v });
},
}),
{
name: 'auth-storage', // localStorage key
partialize: (state) => ({
// Only persist these fields (exclude _hasHydrated)
user: state.user,
accessToken: state.accessToken,
refreshToken: state.refreshToken,
isAuthenticated: state.isAuthenticated,
}),
onRehydrateStorage: () => (state) => {
state?.setHasHydrated(true);
},
}
)
);