"use client"; /** * UserMenu — личный кабинет в правом верхнем углу top-bar (gendsgn.ru/). * * Avatar-кнопка с инициалом из username + dropdown: * - текущий username * - role badge («Админ» / «Пилот») * - кнопка «Выйти» — invalidate Caddy basic_auth cached creds в browser * * Используется в `TopNav.rightSlot`. Если /me грузится или 401 (dev без * Caddy) — рендерим null (нечего показывать). * * Logout flow — basic_auth quirk: * 1. Отправляем fetch с заведомо неправильными Basic creds → 401 от Caddy * → браузер забывает cached creds для этого origin. * 2. Hard reload (`window.location.href = "/"`) — basic_auth prompt * покажется заново. * Это работает в Safari/Chrome/Firefox; legacy `ClearAuthenticationCache` * не нужен. */ import { useEffect, useRef, useState } from "react"; import { LogOut } from "lucide-react"; import { useMe } from "@/lib/useMe"; /** * 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); const containerRef = useRef(null); const buttonRef = useRef(null); // Click outside → закрыть dropdown. useEffect(() => { if (!open) return; function handleClick(e: MouseEvent) { if ( containerRef.current && !containerRef.current.contains(e.target as Node) ) { setOpen(false); } } document.addEventListener("mousedown", handleClick); return () => document.removeEventListener("mousedown", handleClick); }, [open]); // Escape key → закрыть + вернуть focus на avatar. useEffect(() => { if (!open) return; function handleKey(e: KeyboardEvent) { if (e.key === "Escape") { setOpen(false); buttonRef.current?.focus(); } } document.addEventListener("keydown", handleKey); return () => document.removeEventListener("keydown", handleKey); }, [open]); // Loading / 401 / no data — ничего не рендерим. // TopNav сам обрабатывает 401 (dev без Caddy) — показывает все nav items. // Тут просто прячемся, чтобы не было «пустого» avatar без username. if (isLoading || error || !data) return null; const initial = data.username[0]?.toUpperCase() ?? "?"; const roleLabel = data.role === "admin" ? "Админ" : "Пилот"; return (
{open && (
{data.username}
{roleLabel}

)}
); }