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

@@ -1,5 +1,5 @@
import { useState, useMemo, useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router';
import { useNavigate, useSearchParams, useParams } from 'react-router';
import { Pencil, X } from 'lucide-react';
import {
DataTable, Badge, MonoText, DetailPanel, SectionHeader, Button, Toggle, Spinner, useToast,
@@ -8,6 +8,7 @@ import type { Column } from '@cameleer/design-system';
import { useAllApplicationConfigs, useApplicationConfig, useUpdateApplicationConfig, useProcessorRouteMapping } from '../../api/queries/commands';
import type { ApplicationConfig, TapDefinition, ConfigUpdateResponse } from '../../api/queries/commands';
import { useRouteCatalog } from '../../api/queries/catalog';
import { useCanControl } from '../../auth/auth-store';
import type { AppCatalogEntry, RouteSummary } from '../../api/types';
import styles from './AppConfigPage.module.css';
@@ -72,6 +73,7 @@ function buildColumns(): Column<ConfigRow>[] {
function AppConfigDetail({ appId, onClose }: { appId: string; onClose: () => void }) {
const { toast } = useToast();
const navigate = useNavigate();
const canEdit = useCanControl();
const { data: config, isLoading } = useApplicationConfig(appId);
const updateConfig = useUpdateApplicationConfig();
const { data: catalog } = useRouteCatalog();
@@ -241,9 +243,9 @@ function AppConfigDetail({ appId, onClose }: { appId: string; onClose: () => voi
{updateConfig.isPending ? 'Saving\u2026' : 'Save'}
</Button>
</div>
) : (
) : canEdit ? (
<button className={styles.editBtn} onClick={startEditing} title="Edit configuration"><Pencil size={14} /></button>
)}
) : null}
</div>
{/* Settings */}
@@ -319,17 +321,22 @@ function AppConfigDetail({ appId, onClose }: { appId: string; onClose: () => voi
// ── Main Page ────────────────────────────────────────────────────────────────
export default function AppConfigPage() {
const { appId: routeAppId } = useParams<{ appId?: string }>();
const { data: configs } = useAllApplicationConfigs();
const [searchParams, setSearchParams] = useSearchParams();
const [selectedApp, setSelectedApp] = useState<string | null>(null);
const [selectedApp, setSelectedApp] = useState<string | null>(routeAppId ?? null);
const columns = useMemo(buildColumns, []);
// Sync from route param when it changes (sidebar navigation)
useEffect(() => {
if (routeAppId) setSelectedApp(routeAppId);
}, [routeAppId]);
// Auto-select app from query param (e.g., ?app=caller-app)
useEffect(() => {
const appParam = searchParams.get('app');
if (appParam && !selectedApp) {
setSelectedApp(appParam);
// Clean up the query param
searchParams.delete('app');
searchParams.delete('processor');
setSearchParams(searchParams, { replace: true });