/
afanasevn
/
RedisLab
Обзор
Документация
Войти
/
afanasevn
/
RedisLab
Код
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
client/src/api/http.ts
291 строка
7 KB
IBS\NAfanasev
Update solution commit
24 июн 2026, 19:46
24 июн 2026, 19:46
40761b1
Код
Авторство
О чём код?
const TOKEN_KEY = "redislab.token"; const USER_KEY = "redislab.user"; /** Профиль пользователя из JWT / GET /api/auth/me. */ export type UserProfile = { /** Идентификатор пользователя. */ id: string; /** Email для входа. */ email: string; /** Отображаемое имя. */ displayName: string; /** Роль: User или Admin. */ role: string; }; /** Ответ POST /api/auth/login. */ export type LoginResponse = { /** JWT-токен. */ token: string; /** Профиль вошедшего пользователя. */ user: UserProfile; }; export class ApiError extends Error { status: number; constructor(message: string, status: number) { super(message); this.status = status; } } /** KPI дашборда из GET /api/dashboard/kpi. */ export type DashboardKpi = { /** Общее число задач. */ total: number; /** Задачи со статусом Open. */ open: number; /** Задачи со статусом InProgress. */ inProgress: number; /** Задачи со статусом Closed. */ closed: number; /** Просроченные незакрытые задачи. */ overdue: number; }; /** KPI с метаданными cache-aside. */ export type DashboardKpiResponse = { kpi: DashboardKpi; /** X-Cache: HIT или MISS (Development). */ cacheStatus: string | null; }; async function requestRaw(path: string, init?: RequestInit): Promise<Response> { const token = sessionStorage.getItem(TOKEN_KEY); const headers = new Headers(init?.headers); if (!headers.has("Content-Type") && init?.body) { headers.set("Content-Type", "application/json"); } if (token) { headers.set("Authorization", `Bearer ${token}`); } const response = await fetch(path, { ...init, headers }); if (response.status === 401) { const isAuthMe = path.startsWith("/api/auth/me"); // auth/me обрабатывается в useCurrentUser — без жёсткого редиректа, иначе отменяются параллельные запросы дашборда. if (!isAuthMe) { clearToken(); if (!window.location.pathname.startsWith("/login")) { window.location.assign("/login"); } } throw new ApiError("Unauthorized", 401); } if (response.status === 429) { const body = (await response.json().catch(() => null)) as { error?: string } | null; throw new ApiError(body?.error ?? "Слишком много попыток входа. Повторите через минуту.", 429); } if (!response.ok) { const body = (await response.json().catch(() => null)) as { error?: string } | null; throw new ApiError(body?.error ?? response.statusText, response.status); } return response; } async function request<T>(path: string, init?: RequestInit): Promise<T> { const response = await requestRaw(path, init); if (response.status === 204) { return undefined as T; } return response.json() as Promise<T>; } export const authApi = { login(email: string, password: string) { return request<LoginResponse>("/api/auth/login", { method: "POST", body: JSON.stringify({ email, password }), }); }, me() { return request<UserProfile>("/api/auth/me"); }, }; export const dashboardApi = { async getKpi(): Promise<DashboardKpiResponse> { const response = await requestRaw("/api/dashboard/kpi"); const kpi = (await response.json()) as DashboardKpi; return { kpi, cacheStatus: response.headers.get("X-Cache"), }; }, }; /** Элемент списка задач (GET /api/tasks). */ export type TaskListItem = { id: string; title: string; status: string; assigneeName: string; dueDate: string | null; }; /** Карточка задачи (GET /api/tasks/{id}). */ export type TaskCard = { id: string; title: string; status: string; assigneeName: string; dueDate: string | null; }; /** Карточка с признаком cache hit/miss. */ export type TaskCardResponse = { card: TaskCard; cacheStatus: string | null; }; export const tasksApi = { getList() { return request<TaskListItem[]>("/api/tasks"); }, async getById(id: string): Promise<TaskCardResponse> { const response = await requestRaw(`/api/tasks/${id}`); const card = (await response.json()) as TaskCard; return { card, cacheStatus: response.headers.get("X-Cache"), }; }, updateStatus(id: string, status: string) { return request<void>(`/api/tasks/${id}/status`, { method: "PATCH", body: JSON.stringify({ status }), }); }, delete(id: string) { return request<void>(`/api/tasks/${id}`, { method: "DELETE" }); }, }; /** Сообщение live-ленты (SignalR ActivityReceived). */ export type ActivityFeedItem = { type: string; taskId: string | null; message: string; at: string; }; /** Событие из PostgreSQL (GET /api/activity/recent). */ export type ActivityEventItem = { id: string; eventType: string; taskId: string | null; message: string; occurredAt: string; }; export const activityApi = { getRecent(count = 20) { return request<ActivityEventItem[]>(`/api/activity/recent?count=${count}`); }, }; /** Запись leaderboard (GET /api/leaderboard/top). */ export type LeaderboardEntry = { userId: string; displayName: string; completedCount: number; rank: number; }; export const leaderboardApi = { getTop(count = 5) { return request<LeaderboardEntry[]>(`/api/leaderboard/top?count=${count}`); }, }; /** Обработанное Stream-уведомление после XACK. */ export type ProcessedNotification = { streamId: string; type: string; taskId: string | null; message: string; at: string; processedAt: string; }; export const notificationsApi = { getProcessed(count = 20) { return request<ProcessedNotification[]>(`/api/notifications/processed?count=${count}`); }, }; /** Описание одного Redis-ключа в dev debug. */ export type RedisKeyEntry = { key: string; type: string; summary: string; ttlSeconds: number | null; }; /** Сводка GET /api/dev/redis-info. */ export type RedisDebugInfo = { endpoint: string; dbSize: number; keys: RedisKeyEntry[]; }; export const devApi = { getRedisInfo() { return request<RedisDebugInfo>("/api/dev/redis-info"); }, }; export function getToken() { return sessionStorage.getItem(TOKEN_KEY); } export function saveToken(token: string) { sessionStorage.setItem(TOKEN_KEY, token); } export function saveStoredUser(user: UserProfile) { sessionStorage.setItem(USER_KEY, JSON.stringify(user)); } export function saveSession( token: string, user: UserProfile, ) { saveToken(token); saveStoredUser(user); } export function loadStoredUser(): UserProfile | null { const raw = sessionStorage.getItem(USER_KEY); if (!raw) { return null; } try { return JSON.parse(raw) as UserProfile; } catch { return null; } } export function clearToken() { sessionStorage.removeItem(TOKEN_KEY); sessionStorage.removeItem(USER_KEY); } export function hasToken() { return Boolean(sessionStorage.getItem(TOKEN_KEY)); }