From a760bb8716efd8b77196b1b9ad48479a79994515 Mon Sep 17 00:00:00 2001 From: Maximilian Kaufmann Date: Tue, 30 Jun 2026 11:42:05 +0300 Subject: [PATCH] fix(rbac): editors can manage Companies + reach the Registry (2.4.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A user with the Editor role saw only «Профиль» in the menu and could not add companies, even though the backend has allowed editors to create companies since 2.3.0 (POST /api/v1/assets = editor+admin). The «Компании» page and its menu link were gated admin-only, and there was no Registry link in the menu — so editors had no navigation to their workspace. Frontend-only fix (backend, schema, contracts unchanged): - RoleGuard now accepts multiple roles (`role={["admin","editor"]}`). - /admin/assets (Companies) opens to editors; AssetsPage hides the edit/archive «Действия» column for non-admins (mirrors backend: create=editor+admin, edit/archive=admin). Editors create + view; admins also edit/archive. - Header menu: a «Реестр» link for all non-super_admin roles, and «Компании» for editors + admins (moved out of the admin-only block). - Document Types stay admin-only by design (a type sets a global notification schedule); editors still pick from existing types when creating a document. Verified: tsc + biome clean; no test regressions (the 8 failing registry tests are pre-existing useSearch-mock failures, confirmed via git stash). Built and deployed to PreProd (version.json 2.4.2, login 200). Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 10 ++++ web/package.json | 2 +- web/src/app/router.tsx | 6 ++- web/src/features/auth/RoleGuard.tsx | 11 ++-- web/src/pages/admin/assets/AssetsPage.tsx | 66 +++++++++++++---------- web/src/shared/layout/Header.tsx | 56 ++++++++++++++----- 6 files changed, 104 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8deac7d..e8fc1b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ Versioning policy: see [CONTRIBUTING.md](CONTRIBUTING.md#versioning). ## [Unreleased] +## [2.4.2] — 2026-06-30 + +### Fixed +- **Редактор снова может работать со справочниками и реестром.** У пользователя с ролью **Редактор** в меню был только «Профиль», а страница управления **Компаниями** была закрыта (admin-only гард), хотя бэкенд с 2.3.0 разрешает редактору создавать компании. Теперь: + - в меню редактора (и наблюдателя) есть пункт **«Реестр»**, а у редактора — ещё и **«Компании»**; + - страница **«Компании»** доступна редактору: он может **создавать и просматривать** компании; **редактирование и архивирование — только администратор** (колонка действий скрыта для не-админов — зеркалит RBAC бэкенда: `POST /assets` = редактор+админ, `PATCH`/архив = админ); + - **Типы документов** остаются только у администратора (тип задаёт глобальное расписание уведомлений) — редактор по-прежнему выбирает из готовых типов при создании документа. + +> Только фронтенд: бэкенд, схема БД и публичный контракт не менялись (бэкенд уже разрешал редактору создавать компании). PATCH согласно [политике версионирования](CONTRIBUTING.md#versioning) — устранение дефекта доступа. + ## [2.4.1] — 2026-06-30 ### Fixed diff --git a/web/package.json b/web/package.json index d6ee6c4..72856c4 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "@lotsman/web", - "version": "2.4.1", + "version": "2.4.2", "license": "BUSL-1.1", "private": true, "type": "module", diff --git a/web/src/app/router.tsx b/web/src/app/router.tsx index f17f1d7..85c4510 100644 --- a/web/src/app/router.tsx +++ b/web/src/app/router.tsx @@ -182,8 +182,10 @@ function GuardedAdminUsers() { function GuardedAdminAssets() { return ( - {/* biome-ignore lint/a11y/useValidAriaRole: role is a RoleGuard custom prop, not an HTML attribute */} - + {/* Companies are managed by editors too (create + view); the page itself + gates edit/archive to admins. biome-ignore lint/a11y/useValidAriaRole: + role is a RoleGuard custom prop, not an HTML attribute */} + diff --git a/web/src/features/auth/RoleGuard.tsx b/web/src/features/auth/RoleGuard.tsx index 07bb2bb..b918dc1 100644 --- a/web/src/features/auth/RoleGuard.tsx +++ b/web/src/features/auth/RoleGuard.tsx @@ -9,9 +9,8 @@ * navigating to /admin/users gets a 403 from the API, not a confusing redirect. * * Usage: - * - * - * + * + * */ import type * as React from "react"; @@ -19,7 +18,8 @@ import { useAuth } from "./AuthProvider"; import type { UserRole } from "./types"; interface RoleGuardProps { - role: UserRole; + /** A single allowed role, or any-of a list of allowed roles. */ + role: UserRole | UserRole[]; children: React.ReactNode; /** Optional fallback rendered when role does not match */ fallback?: React.ReactNode; @@ -29,7 +29,8 @@ export function RoleGuard({ role, children, fallback = null }: RoleGuardProps) { const { claims } = useAuth(); if (!claims) return null; - if (claims.role !== role) return <>{fallback}; + const allowed = Array.isArray(role) ? role : [role]; + if (!allowed.includes(claims.role)) return <>{fallback}; return <>{children}; } diff --git a/web/src/pages/admin/assets/AssetsPage.tsx b/web/src/pages/admin/assets/AssetsPage.tsx index 7dae9e6..eacc57e 100644 --- a/web/src/pages/admin/assets/AssetsPage.tsx +++ b/web/src/pages/admin/assets/AssetsPage.tsx @@ -2,8 +2,10 @@ // Copyright (c) 2026 Maximilian Kaufmann. See LICENSE (Business Source License 1.1). /** - * AssetsPage — admin-only (RoleGuard) management of partner companies. - * US-12..US-15: list, create, edit, archive assets. + * AssetsPage — management of partner companies (Компании). + * Editors and admins can list + create; editing and archiving are admin-only + * (the «Действия» column is hidden for non-admins, mirroring the backend RBAC: + * POST /assets = editor+admin, PATCH/archive = admin). US-12..US-15. */ import { @@ -16,6 +18,7 @@ import { format, parseISO } from "date-fns"; import { ru } from "date-fns/locale"; import { Archive, Pencil, Plus, RefreshCw } from "lucide-react"; import * as React from "react"; +import { useAuth } from "@/features/auth/AuthProvider"; import { useArchiveAsset, useAssets, @@ -45,6 +48,9 @@ export function AssetsPage() { }; }, [q]); + const { claims } = useAuth(); + const isAdmin = claims?.role === "admin"; + const { data, isLoading, isError, refetch } = useAssets(debouncedQ ? { q: debouncedQ } : {}); const createMutation = useCreateAsset(); const patchMutation = usePatchAsset(); @@ -84,32 +90,38 @@ export function AssetsPage() { ), }), - columnHelper.display({ - id: "actions", - header: "Действия", - cell: ({ row }) => ( -
- - -
- ), - }), + // Edit + archive are admin-only — hide the «Действия» column for editors, + // who can create + view companies but not mutate existing ones. + ...(isAdmin + ? [ + columnHelper.display({ + id: "actions", + header: "Действия", + cell: ({ row }) => ( +
+ + +
+ ), + }), + ] + : []), ], - [], + [isAdmin], ); const table = useReactTable({ diff --git a/web/src/shared/layout/Header.tsx b/web/src/shared/layout/Header.tsx index 18d593b..cdc3b7f 100644 --- a/web/src/shared/layout/Header.tsx +++ b/web/src/shared/layout/Header.tsx @@ -3,8 +3,10 @@ import { Link } from "@tanstack/react-router"; import { + Building2, Calendar, ChevronDown, + ClipboardList, Compass, LogOut, Radio, @@ -150,6 +152,32 @@ function UserMenu() { + {/* Registry — primary workspace, available to all roles */} + {!isSuperAdmin && ( + setOpen(false)} + className={cn( + "flex w-full items-center gap-2 px-3 py-2 text-sm", + "hover:bg-accent focus-visible:outline-none focus-visible:bg-accent", + )} + > + + {t("nav.registry")} + + )} + {/* Profile link */} + {/* Companies — editors and admins manage companies (create + view) */} + {(claims.role === "admin" || claims.role === "editor") && ( + setOpen(false)} + className={cn( + "flex w-full items-center gap-2 px-3 py-2 text-sm", + "hover:bg-accent focus-visible:outline-none focus-visible:bg-accent", + )} + > + + {t("nav.admin_assets")} + + )} + {/* Admin links — admin only (NOT super_admin) */} {claims.role === "admin" && ( <> @@ -179,18 +223,6 @@ function UserMenu() { {t("nav.admin_users")} - setOpen(false)} - className={cn( - "flex w-full items-center gap-2 px-3 py-2 text-sm", - "hover:bg-accent focus-visible:outline-none focus-visible:bg-accent", - )} - > - - {t("nav.admin_assets")} -