feat(auth): UserMenu — личный кабинет top-right (PR D) #590

Merged
Light1YT merged 1 commit from feat/user-menu into main 2026-05-26 10:48:57 +00:00
Owner

Summary

Личный кабинет в правом верхнем углу: avatar с инициалом username, dropdown с role badge + кнопка «Выйти».

Что внутри (4 files, +432 / -19)

  • frontend/src/components/auth/UserMenu.tsx — NEW (201 LOC), lucide-react.LogOut icon
  • tradein-mvp/frontend/src/components/auth/UserMenu.tsx — NEW MIRROR (223 LOC), inline LogOut SVG (нет lucide dep)
  • frontend/src/app/page.tsx<UserMenu /> в <TopNav rightSlot>, заменил dead «Запросить пилот» CTA (pricing card CTA остался — не дублировался)
  • tradein-mvp/frontend/src/components/trade-in/Topbar.tsx<UserMenu /> в конце nav

Logout flow (basic_auth quirk)

async function logout() {
  // 1. 401 trigger для invalidation cached basic_auth creds в browser
  await fetch(`${API_BASE_URL}/api/v1/me`, {
    headers: { Authorization: `Basic ${btoa("logout:logout")}` },
    cache: "no-store",
  });
  // 2. Hard reload — новый basic_auth prompt
  window.location.href = "/";
}

Использует API_BASE_URL (тот же что в apiFetchWithStatus) — single source of truth.

Style — UI tokens only

  • Avatar: var(--accent-soft) bg + var(--accent) text, 32×32 circle
  • Role badge: var(--accent-soft)/var(--accent), uppercase 11px, letter-spacing 0.04em (matches kpi-label pattern)
  • «Выйти»: text-only, var(--danger), weight 500
  • Dropdown: var(--bg-card) + var(--border-card), radius 8, padding 12, popover shadow (cards без shadow, popover это исключение per ui-tokens.md)

Accessibility

  • aria-label="Личный кабинет" на avatar button
  • aria-haspopup="menu", aria-expanded
  • role="menu" на dropdown, role="menuitem" на «Выйти»
  • Escape → закрывается + focus возвращается на avatar
  • Click outside → закрывается (mousedown listener на document)
  • Mirror parity verified в code-review

Code-review verdict

REQUEST_CHANGES → 1 critical (process.env.NEXT_PUBLIC_BASE_PATH direct → fixed to API_BASE_URL inline) → LGTM.

Nits (не блокируют merge):

  • wordBreak: break-word вместо break-all для emails — follow-up
  • Hover state на «Выйти» — follow-up
  • --shadow-popover token — backlog

Test plan (post-deploy)

  • Safari incognito → login admintest → видишь «A» avatar + dropdown с «Админ»
  • Click «Выйти» → basic_auth prompt появляется снова
  • То же в Chrome incognito → проверить что logout reliably очищает creds
  • Pilot scope: pilottest на /trade-in/ → видит «P» + «Пилот» badge
  • Mobile breakpoint — avatar не wrap в шапке
## Summary Личный кабинет в правом верхнем углу: avatar с инициалом username, dropdown с role badge + кнопка «Выйти». ## Что внутри (4 files, +432 / -19) - `frontend/src/components/auth/UserMenu.tsx` — NEW (201 LOC), `lucide-react.LogOut` icon - `tradein-mvp/frontend/src/components/auth/UserMenu.tsx` — NEW MIRROR (223 LOC), inline LogOut SVG (нет lucide dep) - `frontend/src/app/page.tsx` — `<UserMenu />` в `<TopNav rightSlot>`, заменил dead «Запросить пилот» CTA (pricing card CTA остался — не дублировался) - `tradein-mvp/frontend/src/components/trade-in/Topbar.tsx` — `<UserMenu />` в конце nav ## Logout flow (basic_auth quirk) ```ts async function logout() { // 1. 401 trigger для invalidation cached basic_auth creds в browser await fetch(`${API_BASE_URL}/api/v1/me`, { headers: { Authorization: `Basic ${btoa("logout:logout")}` }, cache: "no-store", }); // 2. Hard reload — новый basic_auth prompt window.location.href = "/"; } ``` Использует `API_BASE_URL` (тот же что в `apiFetchWithStatus`) — single source of truth. ## Style — UI tokens only - Avatar: `var(--accent-soft)` bg + `var(--accent)` text, 32×32 circle - Role badge: `var(--accent-soft)`/`var(--accent)`, uppercase 11px, letter-spacing 0.04em (matches kpi-label pattern) - «Выйти»: text-only, `var(--danger)`, weight 500 - Dropdown: `var(--bg-card)` + `var(--border-card)`, radius 8, padding 12, popover shadow (cards без shadow, popover это исключение per ui-tokens.md) ## Accessibility - `aria-label="Личный кабинет"` на avatar button - `aria-haspopup="menu"`, `aria-expanded` - `role="menu"` на dropdown, `role="menuitem"` на «Выйти» - Escape → закрывается + focus возвращается на avatar - Click outside → закрывается (mousedown listener на document) - Mirror parity verified в code-review ## Code-review verdict REQUEST_CHANGES → 1 critical (`process.env.NEXT_PUBLIC_BASE_PATH` direct → fixed to `API_BASE_URL` inline) → LGTM. Nits (не блокируют merge): - `wordBreak: break-word` вместо `break-all` для emails — follow-up - Hover state на «Выйти» — follow-up - `--shadow-popover` token — backlog ## Test plan (post-deploy) - [ ] Safari incognito → login `admintest` → видишь «A» avatar + dropdown с «Админ» - [ ] Click «Выйти» → basic_auth prompt появляется снова - [ ] То же в Chrome incognito → проверить что logout reliably очищает creds - [ ] Pilot scope: `pilottest` на `/trade-in/` → видит «P» + «Пилот» badge - [ ] Mobile breakpoint — avatar не wrap в шапке
Light1YT added 1 commit 2026-05-26 10:48:55 +00:00
Личный кабинет в правом верхнем углу top-bar (gendsgn.ru + /trade-in/).

Компонент:
- Avatar 32×32 (первая буква username в круге, `var(--accent-soft)` фон,
  `var(--accent)` text)
- Dropdown по клику: username, role badge («Админ» / «Пилот»),
  кнопка «Выйти» (text-only, `var(--danger)`)
- Закрытие по Escape, click-outside; focus возвращается на avatar
- `aria-label="Личный кабинет"`, `role="menu"`, `role="menuitem"`

Logout flow (basic_auth quirk):
1. Fetch `/api/v1/me` (через `API_BASE_URL` чтобы single source of truth
   с `apiFetchWithStatus`) с явно invalid Basic creds `logout:logout`
   → 401 от Caddy → браузер очищает cached basic_auth для origin
2. `window.location.href = "/"` — hard reload, новый basic_auth prompt

Integration:
- Main `frontend/`: `<UserMenu />` в `<TopNav rightSlot>`. Pricing CTA
  (`PilotRequestModal` trigger) на странице остался — не дублировался
  в TopNav.
- Tradein `frontend/`: `<UserMenu />` в конце `.top-nav` в `Topbar.tsx`
  (после tabs). MIRROR с inline LogOut SVG (нет lucide-react dep) +
  `API_BASE_URL` import для logout fetch (consistency с rest of tradein).

Manual smoke план (post-deploy):
- Safari incognito → admintest → авitar «A» + role «Админ»
- Click «Выйти» → basic_auth prompt появляется снова
- Same в Chrome / Firefox (cross-browser logout reliability см. PR review)

Code-reviewer LGTM (1 critical fixed inline — tradein logout fetch
теперь использует `API_BASE_URL` вместо `process.env.NEXT_PUBLIC_BASE_PATH`).
Light1YT merged commit 56a7a36c9a into main 2026-05-26 10:48:57 +00:00
Light1YT deleted branch feat/user-menu 2026-05-26 10:48:58 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#590
No description provided.