feat: role-based UI access control
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / build (push) Has been cancelled

- Hide Admin sidebar section for non-ADMIN users
- Add RequireAdmin route guard — /admin/* redirects to / for non-admin
- Move App Config from admin section to main Config tab (per-app,
  visible when app selected). VIEWER sees read-only, OPERATOR+ can edit
- Hide diagram node toolbar for VIEWER (onNodeAction conditional)
- Add useIsAdmin/useCanControl helpers to centralize role checks
- Remove App Config from admin sidebar tree

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-06 15:51:15 +02:00
parent e54f308607
commit b1655b366e
10 changed files with 84 additions and 50 deletions

View File

@@ -3,13 +3,18 @@ import type { TabKey, Scope } from '../hooks/useScope';
import { TabKpis } from './TabKpis';
import styles from './ContentTabs.module.css';
const TABS = [
const BASE_TABS = [
{ label: 'Exchanges', value: 'exchanges' },
{ label: 'Dashboard', value: 'dashboard' },
{ label: 'Runtime', value: 'runtime' },
{ label: 'Logs', value: 'logs' },
];
const TABS_WITH_CONFIG = [
...BASE_TABS,
{ label: 'Config', value: 'config' },
];
interface ContentTabsProps {
active: TabKey;
onChange: (tab: TabKey) => void;
@@ -17,10 +22,12 @@ interface ContentTabsProps {
}
export function ContentTabs({ active, onChange, scope }: ContentTabsProps) {
// Config tab only shown when an app is selected
const tabs = scope.appId ? TABS_WITH_CONFIG : BASE_TABS;
return (
<div className={styles.wrapper}>
<Tabs
tabs={TABS}
tabs={tabs}
active={active}
onChange={(v) => onChange(v as TabKey)}
/>

View File

@@ -23,7 +23,7 @@ import { useAgents } from '../api/queries/agents';
import { useSearchExecutions, useAttributeKeys } from '../api/queries/executions';
import { useUsers, useGroups, useRoles } from '../api/queries/admin/rbac';
import type { UserDetail, GroupDetail, RoleDetail } from '../api/queries/admin/rbac';
import { useAuthStore } from '../auth/auth-store';
import { useAuthStore, useIsAdmin } from '../auth/auth-store';
import { useEnvironmentStore } from '../api/environment-store';
import { useState, useMemo, useCallback, useEffect, useRef, createElement } from 'react';
import type { ReactNode } from 'react';
@@ -277,6 +277,9 @@ function LayoutContent() {
const queryClient = useQueryClient();
const { timeRange, autoRefresh, refreshTimeRange } = useGlobalFilters();
// --- Role checks ----------------------------------------------------
const isAdmin = useIsAdmin();
// --- Environment filtering -----------------------------------------
const selectedEnv = useEnvironmentStore((s) => s.environment);
const setSelectedEnvRaw = useEnvironmentStore((s) => s.setEnvironment);
@@ -668,25 +671,27 @@ function LayoutContent() {
</Sidebar.Section>
)}
{/* Admin section — stays in place, expands when on admin pages */}
<Sidebar.Section
icon={createElement(Settings, { size: 16 })}
label="Admin"
open={adminOpen}
onToggle={toggleAdmin}
active={isAdminPage}
>
<SidebarTree
nodes={adminTreeNodes}
selectedPath={location.pathname}
isStarred={isStarred}
onToggleStar={toggleStar}
filterQuery={filterQuery}
persistKey="admin"
autoRevealPath={sidebarRevealPath}
onNavigate={handleSidebarNavigate}
/>
</Sidebar.Section>
{/* Admin section — only visible to ADMIN role */}
{isAdmin && (
<Sidebar.Section
icon={createElement(Settings, { size: 16 })}
label="Admin"
open={adminOpen}
onToggle={toggleAdmin}
active={isAdminPage}
>
<SidebarTree
nodes={adminTreeNodes}
selectedPath={location.pathname}
isStarred={isStarred}
onToggleStar={toggleStar}
filterQuery={filterQuery}
persistKey="admin"
autoRevealPath={sidebarRevealPath}
onNavigate={handleSidebarNavigate}
/>
</Sidebar.Section>
)}
{/* Footer */}
<Sidebar.Footer>

View File

@@ -101,7 +101,6 @@ export function buildAdminTreeNodes(): SidebarTreeNode[] {
{ id: 'admin:rbac', label: 'Users & Roles', path: '/admin/rbac' },
{ id: 'admin:audit', label: 'Audit Log', path: '/admin/audit' },
{ id: 'admin:oidc', label: 'OIDC', path: '/admin/oidc' },
{ id: 'admin:appconfig', label: 'App Config', path: '/admin/appconfig' },
{ id: 'admin:database', label: 'Database', path: '/admin/database' },
{ id: 'admin:clickhouse', label: 'ClickHouse', path: '/admin/clickhouse' },
];