import { create } from 'zustand'; import { AuthApi } from '../infrastructure/authApi'; import { connectSocket, disconnectSocket } from '../../../core/infrastructure/socket'; import type { User } from '../../../core/domain/types'; interface AuthState { token: string | null; user: User | null; isLoading: boolean; error: string | null; login: (username: string, password: string) => Promise; register: (username: string, displayName: string, password: string, bio?: string) => Promise; logout: () => void; checkAuth: () => Promise; updateUser: (data: Partial) => void; config: any; fetchConfig: () => Promise; } export const useAuthStore = create((set, get) => ({ token: (() => { const t = localStorage.getItem('knot_token'); if (t) AuthApi.setToken(t); return t; })(), user: null, isLoading: true, error: null, config: null, fetchConfig: async () => { if (!get().token) return; try { const res = await AuthApi.getConfig(); set({ config: res }); } catch {} }, login: async (username, password) => { try { set({ error: null, isLoading: true }); const { token, user } = await AuthApi.login(username, password); localStorage.setItem('knot_token', token); AuthApi.setToken(token); connectSocket(token); set({ token, user, isLoading: false }); await get().fetchConfig(); } catch (err: unknown) { const msg = err instanceof Error ? err.message : String(err); set({ error: msg, isLoading: false }); throw err; } }, register: async (username, displayName, password, bio) => { try { set({ error: null, isLoading: true }); const { token, user } = await AuthApi.register(username, displayName, password, bio); localStorage.setItem('knot_token', token); AuthApi.setToken(token); connectSocket(token); set({ token, user, isLoading: false }); await get().fetchConfig(); } catch (err: unknown) { const msg = err instanceof Error ? err.message : String(err); set({ error: msg, isLoading: false }); throw err; } }, logout: () => { localStorage.removeItem('knot_token'); AuthApi.setToken(null); disconnectSocket(); set({ token: null, user: null }); }, checkAuth: async () => { const token = get().token; if (!token) { set({ isLoading: false }); return; } // Retry up to 3 times in case server is still starting let lastError: unknown; for (let attempt = 0; attempt < 3; attempt++) { try { AuthApi.setToken(token); const { user, token: newToken } = await AuthApi.getMe(); if (newToken && newToken.length > 0) { localStorage.setItem('knot_token', newToken); AuthApi.setToken(newToken); set({ token: newToken }); } connectSocket(token); set({ user, isLoading: false }); await get().fetchConfig(); return; } catch (err) { lastError = err; // Only retry on network/server errors, not on auth errors (401/403) const msg = err instanceof Error ? err.message : ''; if (msg.includes('Требуется авторизация') || msg.includes('Недействительный токен')) { break; } if (attempt < 2) { await new Promise(r => setTimeout(r, 1000 * (attempt + 1))); } } } console.warn('checkAuth failed:', lastError); // Explicitly check for 401 Unauthorized or 403 Forbidden const status = (lastError as any)?.status; const errorMsg = lastError instanceof Error ? lastError.message : String(lastError); if ( status === 401 || status === 403 || errorMsg.includes('auth') || errorMsg.includes('Недействительный токен') || errorMsg.includes('Требуется авторизация') ) { localStorage.removeItem('knot_token'); set({ token: null, user: null, isLoading: false }); } else { // Keep the token but stop loading if we're just offline/network error/500 set({ isLoading: false }); } }, updateUser: (data) => { const { user } = get(); if (user) { set({ user: { ...user, ...data } }); } }, }));