diff --git a/frontend/src/components/auth/NoAccessScreen.tsx b/frontend/src/components/auth/NoAccessScreen.tsx index 11c0eb0a..37141e73 100644 --- a/frontend/src/components/auth/NoAccessScreen.tsx +++ b/frontend/src/components/auth/NoAccessScreen.tsx @@ -13,6 +13,8 @@ import { Lock } from "lucide-react"; +import { logout } from "@/lib/logout"; + interface NoAccessScreenProps { /** "user" — юзер вне ролей; "path" — путь вне scope */ variant: "user" | "path"; @@ -100,6 +102,26 @@ export function NoAccessScreen({ variant, path }: NoAccessScreenProps) { {path}

) : null} + {/* Always-available logout — иначе locked/no-access юзер заперт: + UserMenu (где обычно «Выйти») на этом экране не рендерится. */} + ); diff --git a/frontend/src/components/auth/UserMenu.tsx b/frontend/src/components/auth/UserMenu.tsx index d30443cb..edb4e864 100644 --- a/frontend/src/components/auth/UserMenu.tsx +++ b/frontend/src/components/auth/UserMenu.tsx @@ -24,6 +24,8 @@ import { useEffect, useRef, useState } from "react"; import { LogOut } from "lucide-react"; +import { logout } from "@/lib/logout"; + import { useMe, type Role } from "@/lib/useMe"; /** @@ -37,29 +39,6 @@ const ROLE_LABELS: Record = { analyst: "Аналитик", }; -/** - * Logout — invalidate basic_auth cache + reload. - * - * Trick: fetch на защищённый endpoint с явно неверным Basic-токеном. - * Caddy ответит 401 (because creds wrong) → браузер выкинет старые cached - * creds для этого origin. Затем hard reload — Caddy снова запросит basic_auth - * prompt, потому что cached creds invalidated. - */ -async function logout(): Promise { - try { - await fetch("/api/v1/me", { - headers: { - Authorization: "Basic " + btoa("logout:logout"), - }, - cache: "no-store", - }); - } catch { - // Сетевая ошибка — всё равно делаем hard reload. - } - // Hard reload форсит новый basic_auth handshake. - window.location.href = "/"; -} - export function UserMenu() { const { data, isLoading, error } = useMe(); const [open, setOpen] = useState(false); diff --git a/frontend/src/lib/logout.ts b/frontend/src/lib/logout.ts new file mode 100644 index 00000000..4a246c20 --- /dev/null +++ b/frontend/src/lib/logout.ts @@ -0,0 +1,25 @@ +/** + * Logout — invalidate basic_auth cache + reload. + * + * Trick: fetch на защищённый endpoint с явно неверным Basic-токеном. + * Caddy ответит 401 (because creds wrong) → браузер выкинет старые cached + * creds для этого origin. Затем hard reload — Caddy снова запросит basic_auth + * prompt, потому что cached creds invalidated. + * + * Это работает в Safari/Chrome/Firefox; legacy `ClearAuthenticationCache` + * не нужен. + */ +export async function logout(): Promise { + try { + await fetch("/api/v1/me", { + headers: { + Authorization: "Basic " + btoa("logout:logout"), + }, + cache: "no-store", + }); + } catch { + // Сетевая ошибка — всё равно делаем hard reload. + } + // Hard reload форсит новый basic_auth handshake. + window.location.href = "/"; +} diff --git a/tradein-mvp/frontend/src/components/auth/NoAccessScreen.tsx b/tradein-mvp/frontend/src/components/auth/NoAccessScreen.tsx index e6f0338a..b0f94ffa 100644 --- a/tradein-mvp/frontend/src/components/auth/NoAccessScreen.tsx +++ b/tradein-mvp/frontend/src/components/auth/NoAccessScreen.tsx @@ -9,6 +9,8 @@ * Token-based styling per `.claude/rules/ui-tokens.md` (см. globals.css). */ +import { logout } from "@/lib/logout"; + interface NoAccessScreenProps { variant: "user" | "path" | "session" | "trial"; path?: string; @@ -116,6 +118,26 @@ export function NoAccessScreen({ variant, path }: NoAccessScreenProps) { {path}

) : null} + {/* Always-available logout — иначе locked/no-access юзер заперт: + UserMenu (где обычно «Выйти») на этом экране не рендерится. */} + ); diff --git a/tradein-mvp/frontend/src/components/auth/UserMenu.tsx b/tradein-mvp/frontend/src/components/auth/UserMenu.tsx index b57fcf93..3e7350b5 100644 --- a/tradein-mvp/frontend/src/components/auth/UserMenu.tsx +++ b/tradein-mvp/frontend/src/components/auth/UserMenu.tsx @@ -21,37 +21,10 @@ import { useEffect, useRef, useState } from "react"; -import { API_BASE_URL } from "@/lib/api"; +import { logout } from "@/lib/logout"; import { useMe } from "@/lib/useMe"; -/** - * Logout — invalidate basic_auth cache + reload. - * - * При basePath=/trade-in мы должны бить именно по защищённому пути - * (`/trade-in/api/v1/me`), иначе Caddy не вытолкнет cached creds для текущего - * scope. После 401 — hard reload на корень сайта (`/`), там basic_auth - * prompt появится заново. - */ -async function logout(): Promise { - // Используем `API_BASE_URL` (тот же который `apiFetchWithStatus` для /me) — - // single source of truth. В проде `API_BASE_URL` = `/trade-in` (через - // `NEXT_PUBLIC_API_BASE_URL` или `NEXT_PUBLIC_BASE_PATH` fallback); в dev "". - try { - await fetch(`${API_BASE_URL}/api/v1/me`, { - headers: { - Authorization: "Basic " + btoa("logout:logout"), - }, - cache: "no-store", - }); - } catch { - // Сетевая ошибка — всё равно делаем hard reload. - } - // Hard reload форсит новый basic_auth handshake. Уходим в корень сайта, - // чтобы prompt не залип в /trade-in scope. - window.location.href = "/"; -} - /** Inline LogOut icon (lucide-react `LogOut` SVG path, stroke 1.5). */ function LogOutIcon() { return ( diff --git a/tradein-mvp/frontend/src/lib/logout.ts b/tradein-mvp/frontend/src/lib/logout.ts new file mode 100644 index 00000000..63b25123 --- /dev/null +++ b/tradein-mvp/frontend/src/lib/logout.ts @@ -0,0 +1,34 @@ +import { API_BASE_URL } from "@/lib/api"; + +/** + * Logout — invalidate basic_auth cache + reload. + * + * Logout flow — basic_auth quirk: + * 1. Отправляем fetch с заведомо неправильными Basic creds → 401 от Caddy + * → браузер забывает cached creds для этого origin. + * 2. Hard reload (`window.location.href = "/"`) — basic_auth prompt + * покажется заново. + * + * При basePath=/trade-in мы должны бить именно по защищённому пути + * (`/trade-in/api/v1/me`), иначе Caddy не вытолкнет cached creds для текущего + * scope. После 401 — hard reload на корень сайта (`/`), там basic_auth + * prompt появится заново. + */ +export async function logout(): Promise { + // Используем `API_BASE_URL` (тот же который `apiFetchWithStatus` для /me) — + // single source of truth. В проде `API_BASE_URL` = `/trade-in` (через + // `NEXT_PUBLIC_API_BASE_URL` или `NEXT_PUBLIC_BASE_PATH` fallback); в dev "". + try { + await fetch(`${API_BASE_URL}/api/v1/me`, { + headers: { + Authorization: "Basic " + btoa("logout:logout"), + }, + cache: "no-store", + }); + } catch { + // Сетевая ошибка — всё равно делаем hard reload. + } + // Hard reload форсит новый basic_auth handshake. Уходим в корень сайта, + // чтобы prompt не залип в /trade-in scope. + window.location.href = "/"; +}