Files
forkmessager/client-web/src/modules/auth/application/authStore.ts
Халимов Рустам 4ae7dd60ce Рабочий чат
2026-04-01 23:18:55 +03:00

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 } });
}
},
}));