feat(rbac): frontend route-guard + nav filter (PR B — frontend) #586

Merged
Light1YT merged 1 commit from feat/rbac-prb-frontend into main 2026-05-26 06:51:45 +00:00
Owner

Summary

PR B (frontend) к backend RBAC из PR #585.

Реализует требования из Telegram-сессии 2026-05-25:

  • «сверху в баре странички в зависимости от роли» — top-nav filter
  • «если доступа нет — слать нахуй» — fullscreen «доступа нет» на denied direct URL
  • «человек без ролей вообще ничего не видит» — RouteGuard renders only NoAccessScreen для 403 от /me

Что включено (14 files, +834/-91)

Main frontend/:

  • src/lib/useMe.ts — TanStack useMe(), staleTime: Infinity, читает GET /api/v1/me
  • src/lib/isPathAllowed.ts — JS port _glob_to_regex из backend/app/core/auth.py
  • src/lib/__tests__/isPathAllowed.test.ts — parity tests vs backend test_rbac.py
  • src/components/auth/RouteGuard.tsx — wraps app, 403 → NoAccessScreen, loading → null
  • src/components/auth/NoAccessScreen.tsx — fullscreen «Доступа нет» с <Lock> lucide
  • src/components/landing/TopNav.tsx — extracted nav, filtered via isPathAllowed(scope, href)
  • src/app/{layout.tsx, page.tsx} — wrap в RouteGuard, inline nav → <TopNav>

Tradein mirror (tradein-mvp/frontend/):

  • src/lib/{useMe,isPathAllowed}.ts — MIRROR с basePath-aware URL
  • src/components/auth/{RouteGuard,NoAccessScreen}.tsx — MIRROR, tokenized colors per .claude/rules/ui-tokens.md
  • src/components/trade-in/Topbar.tsx — scraper tabs (Avito/Cian/Yandex/N1) hidden для pilot
  • src/app/layout.tsx — wrap в RouteGuard

Security verification (code-reviewer LGTM)

  • pilot → /admin/* → fullscreen NoAccessScreen (children не рендерятся)
  • ghost (403 от /me) → ничего не видно, только NoAccessScreen
  • 401 fallback (dev без Caddy) — pass-through (prod Caddy basic_auth гарантирует header)
  • JS glob translator точно зеркалит backend _glob_to_regex

Test plan

  • code-reviewer LGTM (1 nit fixed inline: tradein NoAccessScreen → UI tokens)
  • npm run type-check в CI на обоих frontends после merge (locally нет node_modules)
  • Post-deploy smoke в браузере:
    • admin → видит «Админ» в top-nav, /admin/scrape работает
    • kopylov → нет «Админ» в top-nav, /admin/scrape (direct URL) → fullscreen
    • неизвестный юзер (если добавить в Caddy basic_auth без roles.yaml entry) → fullscreen без UI
    • tradein top-bar — pilot не видит scraper tabs

Follow-ups (nits, не блокирует merge)

  • pilot direct URL /trade-in/scrapers/avito — UI page рендерится (yaml allows /trade-in/**), но data fetches 403. Если нужен hard-block UI — добавить /trade-in/scrapers/** в pilot.deny в auth/roles.yaml (отдельный PR).
  • Topbar.tsx scraper tabs используют синтетические scopePath (/trade-in/api/v1/admin/scrapers/avito) для filter logic — readability nit, можно переименовать на requiresAdmin: true boolean (отдельный PR).

Depends on #585 (merged).

## Summary PR B (frontend) к backend RBAC из [PR #585](https://git.gendsgn.ru/lekss361/gendesign/pulls/585). Реализует требования из Telegram-сессии 2026-05-25: - «сверху в баре странички в зависимости от роли» — top-nav filter - «если доступа нет — слать нахуй» — fullscreen «доступа нет» на denied direct URL - «человек без ролей вообще ничего не видит» — RouteGuard renders only NoAccessScreen для 403 от /me ## Что включено (14 files, +834/-91) **Main `frontend/`:** - `src/lib/useMe.ts` — TanStack `useMe()`, `staleTime: Infinity`, читает GET `/api/v1/me` - `src/lib/isPathAllowed.ts` — JS port `_glob_to_regex` из `backend/app/core/auth.py` - `src/lib/__tests__/isPathAllowed.test.ts` — parity tests vs backend `test_rbac.py` - `src/components/auth/RouteGuard.tsx` — wraps app, 403 → NoAccessScreen, loading → null - `src/components/auth/NoAccessScreen.tsx` — fullscreen «Доступа нет» с `<Lock>` lucide - `src/components/landing/TopNav.tsx` — extracted nav, filtered via `isPathAllowed(scope, href)` - `src/app/{layout.tsx, page.tsx}` — wrap в RouteGuard, inline nav → `<TopNav>` **Tradein mirror (`tradein-mvp/frontend/`):** - `src/lib/{useMe,isPathAllowed}.ts` — MIRROR с basePath-aware URL - `src/components/auth/{RouteGuard,NoAccessScreen}.tsx` — MIRROR, tokenized colors per `.claude/rules/ui-tokens.md` - `src/components/trade-in/Topbar.tsx` — scraper tabs (Avito/Cian/Yandex/N1) hidden для pilot - `src/app/layout.tsx` — wrap в RouteGuard ## Security verification (code-reviewer LGTM) - ✅ pilot → /admin/* → fullscreen NoAccessScreen (children не рендерятся) - ✅ ghost (403 от /me) → ничего не видно, только NoAccessScreen - ✅ 401 fallback (dev без Caddy) — pass-through (prod Caddy basic_auth гарантирует header) - ✅ JS glob translator точно зеркалит backend `_glob_to_regex` ## Test plan - [x] code-reviewer LGTM (1 nit fixed inline: tradein NoAccessScreen → UI tokens) - [ ] `npm run type-check` в CI на обоих frontends после merge (locally нет node_modules) - [ ] Post-deploy smoke в браузере: - admin → видит «Админ» в top-nav, /admin/scrape работает - kopylov → нет «Админ» в top-nav, /admin/scrape (direct URL) → fullscreen - неизвестный юзер (если добавить в Caddy basic_auth без roles.yaml entry) → fullscreen без UI - tradein top-bar — pilot не видит scraper tabs ## Follow-ups (nits, не блокирует merge) - pilot direct URL `/trade-in/scrapers/avito` — UI page рендерится (yaml allows `/trade-in/**`), но data fetches 403. Если нужен hard-block UI — добавить `/trade-in/scrapers/**` в `pilot.deny` в `auth/roles.yaml` (отдельный PR). - `Topbar.tsx` scraper tabs используют синтетические `scopePath` (`/trade-in/api/v1/admin/scrapers/avito`) для filter logic — readability nit, можно переименовать на `requiresAdmin: true` boolean (отдельный PR). Depends on #585 (merged).
Light1YT added 1 commit 2026-05-26 06:42:42 +00:00
PR B (frontend) к backend RBAC из PR #585. Реализует требования из
Telegram-сессии 2026-05-25: «сверху в баре странички в зависимости от
роли», «если доступа нет — слать нахуй», «человек без ролей вообще
ничего не видит».

Main frontend (`frontend/`):
- `src/lib/useMe.ts` — TanStack `useMe()` hook, `staleTime: Infinity`
  (роль не меняется в рамках сессии); читает GET /api/v1/me.
- `src/lib/isPathAllowed.ts` — JS port `backend/app/core/auth.py`
  `_glob_to_regex`. DSTAR/SSTAR sentinels, `/foo/**` matches `/foo` тоже.
- `src/lib/__tests__/isPathAllowed.test.ts` — parity tests vs backend
  `test_rbac.py::test_is_path_allowed_*` (admin everywhere, pilot blocked
  from /admin/**, unknown role denied).
- `src/components/auth/RouteGuard.tsx` — wraps app в layout. На 403 от /me
  → `<NoAccessScreen variant="user">`. На denied path → `<NoAccessScreen
  variant="path">`. Loading state — `null` чтобы не было FOUC protected
  UI. 401 (dev без Caddy) — pass-through.
- `src/components/auth/NoAccessScreen.tsx` — fullscreen «доступа нет» с
  lucide `<Lock>` иконкой. Token-based styling.
- `src/components/landing/TopNav.tsx` — extracted nav из `page.tsx`.
  Фильтрует items через `isPathAllowed(scope, href)`. Pilot → нет «Админ».
- `src/app/{layout.tsx,page.tsx}` — обернуть в `<RouteGuard>`, inline nav
  → `<TopNav rightSlot=…>`.

Tradein mirror (`tradein-mvp/frontend/`):
- `src/lib/{useMe,isPathAllowed}.ts` — MIRROR. `useMe` использует
  `NEXT_PUBLIC_BASE_PATH`-aware URL (`/trade-in/api/v1/me`).
- `src/components/auth/{RouteGuard,NoAccessScreen}.tsx` — MIRROR.
  RouteGuard prepends `basePath` перед scope check (т.к. `usePathname()`
  strips basePath в Next.js 15 app router).
  NoAccessScreen — tokenized colors (var(--bg-app), --fg-primary, etc.)
  per `.claude/rules/ui-tokens.md`. Без lucide (отдельный bundle без
  shared deps).
- `src/components/trade-in/Topbar.tsx` — scraper tabs (Avito/Cian/Yandex/
  N1) скрыты для pilot (`scopePath` синтетически указывает на
  `/trade-in/api/v1/admin/*` для filter logic).
- `src/app/layout.tsx` — wrap children в `<RouteGuard>`.

Security verification (code-reviewer LGTM):
- pilot → /admin/* blocked: 
- ghost (403) → ничего не видно: 
- 401 fallback (dev) — safe (prod Caddy basic_auth гарантирует header).

UX nit: pilot direct URL `/trade-in/scrapers/avito` (UI page) не блочен
RouteGuard'ом — yaml allows `/trade-in/**`. Но data fetches к
`/trade-in/api/v1/admin/*` будут 403, страница покажет пустое состояние.
Follow-up: добавить `/trade-in/scrapers/**` в `pilot.deny` если нужен
hard-block UI (out-of-scope этого PR).

Tests: 14 files, +834/-91. `npm run type-check` нужно прогнать в CI
после merge (node_modules не доступны локально).
Light1YT merged commit 7a2b055b35 into main 2026-05-26 06:51:45 +00:00
Light1YT deleted branch feat/rbac-prb-frontend 2026-05-26 06:51:46 +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#586
No description provided.