import { create } from 'zustand'; import type { User, Quota, TeamInfo } from '../types'; import { authApi } from '../lib/api'; interface AuthState { user: User | null; accessToken: string | null; refreshToken: string | null; isAuthenticated: boolean; isLoading: boolean; quota: Quota | null; team: TeamInfo | null; teamDisabled: boolean; mustChangePassword: boolean; login: (username: string, password: string) => Promise; logout: () => void; refreshAccessToken: () => Promise; fetchUserInfo: () => Promise; initialize: () => Promise; clearMustChangePassword: () => void; } export const useAuthStore = create((set, get) => ({ user: null, accessToken: localStorage.getItem('access_token'), refreshToken: localStorage.getItem('refresh_token'), isAuthenticated: !!localStorage.getItem('access_token'), isLoading: true, quota: null, team: null, teamDisabled: false, mustChangePassword: false, login: async (username, password) => { const { data } = await authApi.login(username, password); localStorage.setItem('access_token', data.tokens.access); localStorage.setItem('refresh_token', data.tokens.refresh); set({ user: data.user, accessToken: data.tokens.access, refreshToken: data.tokens.refresh, isAuthenticated: true, mustChangePassword: data.user.must_change_password || false, }); // Fetch quota after login await get().fetchUserInfo(); }, logout: () => { // 先用当前 token 通知后端清除 ActiveSession,再清本地状态 const token = localStorage.getItem('access_token'); if (token) { fetch('/api/v1/auth/logout', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, }).catch(() => {}); } localStorage.removeItem('access_token'); localStorage.removeItem('refresh_token'); set({ user: null, accessToken: null, refreshToken: null, isAuthenticated: false, quota: null, team: null, teamDisabled: false, mustChangePassword: false, }); }, refreshAccessToken: async () => { const refresh = get().refreshToken; if (!refresh) throw new Error('No refresh token'); const { data } = await authApi.refreshToken(refresh); localStorage.setItem('access_token', data.access); if (data.refresh) { localStorage.setItem('refresh_token', data.refresh); set({ accessToken: data.access, refreshToken: data.refresh }); } else { set({ accessToken: data.access }); } }, fetchUserInfo: async () => { const { data } = await authApi.getMe(); const { quota, team, team_disabled, ...user } = data; set({ user, quota, team: team || null, teamDisabled: team_disabled || false, isAuthenticated: true, mustChangePassword: user.must_change_password || false, }); }, initialize: async () => { const token = localStorage.getItem('access_token'); if (token) { // Retry up to 3 times for network errors (e.g. request aborted during page refresh) for (let attempt = 0; attempt < 3; attempt++) { try { await get().fetchUserInfo(); break; } catch (err) { const status = (err as { response?: { status?: number } })?.response?.status; if (status === 401 || status === 403) { get().logout(); break; } // Network error / aborted — wait briefly and retry if (attempt < 2) { await new Promise(r => setTimeout(r, 500)); } } } } set({ isLoading: false }); }, clearMustChangePassword: () => { set({ mustChangePassword: false }); }, }));