feat(sidebar): role-based visibility для /reviews
Phase 1d enhancement: 'Согласования' пункт сайдбара скрыт если у user'а нет approver/reviewer ролей. Раньше показывался любому authenticated — оператор без reviewer прав видел queue, кликал и получал пустую страницу (или 403 если бы пробовал approve). Visibility rules: - 'Мои черновики' — auth (maker может быть любой) - 'Согласования' — auth + (ordinis:schema:reviewer OR ordinis:record:reviewer OR ordinis:admin super-role) — иначе hidden - 'Outbox' / 'Webhooks' / 'Аудит' — auth (admin-y но backend не gated отдельной ролью) Новый NavItem.reviewerOnly flag — фильтр в visibleSections учитывает оба flag'а (authRequired + reviewerOnly). Применено и к desktop Sidebar и к MobileSidebar (DRY refactor — отдельный task).
This commit is contained in:
@@ -14,6 +14,10 @@ import {
|
|||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useAuth } from 'react-oidc-context'
|
import { useAuth } from 'react-oidc-context'
|
||||||
import { useCanMutate } from '@/auth/useCanMutate'
|
import { useCanMutate } from '@/auth/useCanMutate'
|
||||||
|
import {
|
||||||
|
useCanReviewRecord,
|
||||||
|
useCanReviewSchema,
|
||||||
|
} from '@/auth/usePermissions'
|
||||||
import {
|
import {
|
||||||
useDictionaries,
|
useDictionaries,
|
||||||
useMyDrafts,
|
useMyDrafts,
|
||||||
@@ -52,12 +56,24 @@ type NavItem = {
|
|||||||
badge?: React.ReactNode
|
badge?: React.ReactNode
|
||||||
/** Только аутентифицированным пользователям. */
|
/** Только аутентифицированным пользователям. */
|
||||||
authRequired?: boolean
|
authRequired?: boolean
|
||||||
|
/**
|
||||||
|
* Visible только если user имеет хотя бы одну из workflow ролей
|
||||||
|
* (или {@code ordinis:admin} super-role). Hide если нет.
|
||||||
|
*/
|
||||||
|
reviewerOnly?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const auth = useAuth()
|
const auth = useAuth()
|
||||||
const canMutate = useCanMutate()
|
const canMutate = useCanMutate()
|
||||||
|
// Phase 1d role-based sidebar visibility. /reviews пункт показываем
|
||||||
|
// только тем у кого есть schema:reviewer OR record:reviewer OR
|
||||||
|
// ordinis:admin super-role. Иначе пункт скрыт — у user'а нет смысла
|
||||||
|
// его видеть (clicking всё равно даст empty queue / 403).
|
||||||
|
const canReviewSchema = useCanReviewSchema()
|
||||||
|
const canReviewRecord = useCanReviewRecord()
|
||||||
|
const isReviewer = canReviewSchema || canReviewRecord
|
||||||
const dictsQuery = useDictionaries()
|
const dictsQuery = useDictionaries()
|
||||||
// Loaded just for сидбар counter — light request, кэшируется.
|
// Loaded just for сидбар counter — light request, кэшируется.
|
||||||
const myDrafts = useMyDrafts(0, 1)
|
const myDrafts = useMyDrafts(0, 1)
|
||||||
@@ -105,6 +121,7 @@ export function Sidebar() {
|
|||||||
icon: ClipboardList,
|
icon: ClipboardList,
|
||||||
badge: reviewsCount > 0 ? reviewsCount : undefined,
|
badge: reviewsCount > 0 ? reviewsCount : undefined,
|
||||||
authRequired: true,
|
authRequired: true,
|
||||||
|
reviewerOnly: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
to: '/outbox',
|
to: '/outbox',
|
||||||
@@ -129,7 +146,11 @@ export function Sidebar() {
|
|||||||
const visibleSections = sections
|
const visibleSections = sections
|
||||||
.map((s) => ({
|
.map((s) => ({
|
||||||
...s,
|
...s,
|
||||||
items: s.items.filter((i) => !i.authRequired || canMutate),
|
items: s.items.filter((i) => {
|
||||||
|
if (i.authRequired && !canMutate) return false
|
||||||
|
if (i.reviewerOnly && !isReviewer) return false
|
||||||
|
return true
|
||||||
|
}),
|
||||||
}))
|
}))
|
||||||
.filter((s) => s.items.length > 0)
|
.filter((s) => s.items.length > 0)
|
||||||
|
|
||||||
@@ -418,6 +439,9 @@ export function MobileSidebar({ open, onClose }: { open: boolean; onClose: () =>
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const auth = useAuth()
|
const auth = useAuth()
|
||||||
const canMutate = useCanMutate()
|
const canMutate = useCanMutate()
|
||||||
|
const canReviewSchema = useCanReviewSchema()
|
||||||
|
const canReviewRecord = useCanReviewRecord()
|
||||||
|
const isReviewer = canReviewSchema || canReviewRecord
|
||||||
const dictsQuery = useDictionaries()
|
const dictsQuery = useDictionaries()
|
||||||
const myDrafts = useMyDrafts(0, 1)
|
const myDrafts = useMyDrafts(0, 1)
|
||||||
const reviews = useReviewsBadgeCount(auth.isAuthenticated)
|
const reviews = useReviewsBadgeCount(auth.isAuthenticated)
|
||||||
@@ -455,6 +479,7 @@ export function MobileSidebar({ open, onClose }: { open: boolean; onClose: () =>
|
|||||||
icon: ClipboardList,
|
icon: ClipboardList,
|
||||||
badge: reviewsCount > 0 ? reviewsCount : undefined,
|
badge: reviewsCount > 0 ? reviewsCount : undefined,
|
||||||
authRequired: true,
|
authRequired: true,
|
||||||
|
reviewerOnly: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
to: '/outbox',
|
to: '/outbox',
|
||||||
@@ -476,7 +501,14 @@ export function MobileSidebar({ open, onClose }: { open: boolean; onClose: () =>
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
const visibleSections = sections
|
const visibleSections = sections
|
||||||
.map((s) => ({ ...s, items: s.items.filter((i) => !i.authRequired || canMutate) }))
|
.map((s) => ({
|
||||||
|
...s,
|
||||||
|
items: s.items.filter((i) => {
|
||||||
|
if (i.authRequired && !canMutate) return false
|
||||||
|
if (i.reviewerOnly && !isReviewer) return false
|
||||||
|
return true
|
||||||
|
}),
|
||||||
|
}))
|
||||||
.filter((s) => s.items.length > 0)
|
.filter((s) => s.items.length > 0)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user