66 lines
2.7 KiB
TypeScript
66 lines
2.7 KiB
TypeScript
"use client";
|
||
|
||
/**
|
||
* `useMe()` — TanStack-хук для GET /api/v1/me.
|
||
*
|
||
* Backend возвращает `UserScope` (см. `backend/app/core/auth.py`):
|
||
* { username, role: "admin"|"pilot"|"analyst", allowed_paths: string[], deny_paths: string[] }
|
||
*
|
||
* Failure modes:
|
||
* - 401 — нет X-Authenticated-User (локально без Caddy basic_auth). Фронт
|
||
* трактует это как «dev-режим» и в этом случае route-guard ничего не
|
||
* блокирует (см. RouteGuard).
|
||
* - 403 — header есть, но юзер не в `auth/roles.yaml`. Фронт показывает
|
||
* полноэкранный NoAccessScreen и больше ничего.
|
||
*
|
||
* Кешируем «вечно» (`staleTime: Infinity`, без рефетча): роль не меняется
|
||
* в рамках сессии. Если basic_auth обновится — юзер перезагрузит страницу.
|
||
*/
|
||
|
||
import { useQuery } from "@tanstack/react-query";
|
||
|
||
import { apiFetchWithStatus, HTTPError } from "@/lib/api";
|
||
|
||
// Error type для useQuery: либо `HTTPError` (от apiFetchWithStatus при 4xx/5xx),
|
||
// либо обычный `Error` (network fail, fetch reject). RouteGuard / TopNav проверяют
|
||
// статус через `error instanceof HTTPError` — это работает в runtime независимо
|
||
// от type-level decl.
|
||
|
||
// Должен совпадать с `Role` в `backend/app/core/auth.py` (Literal["admin", "pilot", "analyst"]).
|
||
// Источник правды — `auth/roles.yaml`; при добавлении новой роли расширить тут и
|
||
// `ROLE_LABELS` в `components/auth/UserMenu.tsx`.
|
||
export type Role = "admin" | "pilot" | "analyst";
|
||
|
||
export interface UserScope {
|
||
username: string;
|
||
role: Role;
|
||
allowed_paths: string[];
|
||
deny_paths: string[];
|
||
}
|
||
|
||
export const ME_QUERY_KEY = ["auth", "me"] as const;
|
||
|
||
async function fetchMe(): Promise<UserScope> {
|
||
const { body } = await apiFetchWithStatus<UserScope>("/api/v1/me");
|
||
return body;
|
||
}
|
||
|
||
export function useMe() {
|
||
return useQuery<UserScope, Error>({
|
||
queryKey: ME_QUERY_KEY,
|
||
queryFn: fetchMe,
|
||
staleTime: Infinity,
|
||
gcTime: Infinity,
|
||
refetchOnWindowFocus: false,
|
||
refetchOnReconnect: false,
|
||
refetchOnMount: false,
|
||
// 401 (dev без Caddy) и 403 (unknown user) — финальные состояния,
|
||
// ретраить бесполезно. Ретраим только сетевые сбои.
|
||
retry: (failureCount, error) => {
|
||
if (error instanceof HTTPError && (error.status === 401 || error.status === 403)) {
|
||
return false;
|
||
}
|
||
return failureCount < 2;
|
||
},
|
||
});
|
||
}
|