139 lines
4.3 KiB
TypeScript
139 lines
4.3 KiB
TypeScript
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<void>;
|
|
register: (username: string, displayName: string, password: string, bio?: string) => Promise<void>;
|
|
logout: () => void;
|
|
checkAuth: () => Promise<void>;
|
|
updateUser: (data: Partial<User>) => void;
|
|
config: any;
|
|
fetchConfig: () => Promise<void>;
|
|
}
|
|
|
|
export const useAuthStore = create<AuthState>((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 } });
|
|
}
|
|
},
|
|
}));
|