diff --git a/src/api/userApi.ts b/src/api/userApi.ts index 8c70535..7c58d37 100644 --- a/src/api/userApi.ts +++ b/src/api/userApi.ts @@ -11,11 +11,10 @@ export interface User { fullName: string; language: string; enabled: boolean; - privilege?: string; // Deprecated - use roleLabels instead roleLabels?: string[]; // Array of role labels from backend (e.g., ["user"]) authenticationAuthority: string; mandateId: string; - [key: string]: any; // Allow additional properties + [key: string]: any; // Allow additional properties (may include deprecated 'privilege' from backend) } export type UserUpdateData = Partial>; @@ -92,7 +91,6 @@ export async function fetchCurrentUser( hasData: !!response, username: response?.username, roleLabels: response?.roleLabels, - privilege: response?.privilege, allKeys: response ? Object.keys(response) : [], fullResponse: response }); diff --git a/src/components/Sidebar/SidebarUser.tsx b/src/components/Sidebar/SidebarUser.tsx index 1a5e2e8..68aa4dd 100644 --- a/src/components/Sidebar/SidebarUser.tsx +++ b/src/components/Sidebar/SidebarUser.tsx @@ -68,7 +68,7 @@ const SidebarUser: React.FC = ({ isMinimized = false }) => { fullName: cached.fullName || cached.username.split('@')[0] || cached.username, language: cached.language || 'de', // Default language enabled: cached.enabled ?? true, // Assume enabled if logged in - privilege: cached.privilege || 'user', + roleLabels: cached.roleLabels || [], authenticationAuthority: cached.authenticationAuthority || 'local', mandateId: cached.mandateId || '' }; @@ -97,7 +97,7 @@ const SidebarUser: React.FC = ({ isMinimized = false }) => { fullName: cached.fullName || cached.username.split('@')[0] || cached.username, language: cached.language || 'de', enabled: cached.enabled ?? true, - privilege: cached.privilege || 'user', + roleLabels: cached.roleLabels || [], authenticationAuthority: cached.authenticationAuthority || 'local', mandateId: cached.mandateId || '' }; diff --git a/src/core/PageManager/PageManager.tsx b/src/core/PageManager/PageManager.tsx index 01ead05..f5dc60f 100644 --- a/src/core/PageManager/PageManager.tsx +++ b/src/core/PageManager/PageManager.tsx @@ -68,11 +68,32 @@ const PageManager: React.FC = ({ return; } - // Check page access + // Check page access (RBAC + privilegeChecker) console.log('🔍 PageManager: Checking access before rendering:', currentPath); - checkPageAccess(pageData).then(hasAccess => { + + // First check client-side privilegeChecker if provided + const checkPrivilege = async (): Promise => { + if (pageData.privilegeChecker) { + try { + const result = await pageData.privilegeChecker(); + if (!result) { + console.log('⛔ PageManager: Page blocked by privilegeChecker:', currentPath); + return false; + } + } catch (error) { + console.error('❌ PageManager: Error checking privilegeChecker:', error); + return false; + } + } + return true; + }; + + Promise.all([checkPrivilege(), checkPageAccess(pageData)]).then(([hasPrivilege, hasRBACAccess]) => { + const hasAccess = hasPrivilege && hasRBACAccess; console.log('🔍 PageManager: Access check complete:', { path: currentPath, + hasPrivilege, + hasRBACAccess, hasAccess }); diff --git a/src/core/PageManager/SidebarProvider.tsx b/src/core/PageManager/SidebarProvider.tsx index 4cade1d..614aad6 100644 --- a/src/core/PageManager/SidebarProvider.tsx +++ b/src/core/PageManager/SidebarProvider.tsx @@ -123,14 +123,30 @@ export const SidebarProvider: React.FC = ({ children }) => // Process parent groups for (const [_parentPath, parentGroup] of parentGroups.entries()) { - // Filter subpages by RBAC access + // Filter subpages by RBAC access and privilegeChecker const accessibleSubpages = []; for (const subpage of parentGroup.subpages) { try { + // Check RBAC access const hasSubpageRBACAccess = await canView('UI', subpage.path); - if (hasSubpageRBACAccess) { - accessibleSubpages.push(subpage); + if (!hasSubpageRBACAccess) { + continue; } + + // Check client-side privilegeChecker if provided + if (subpage.privilegeChecker) { + try { + const hasPrivilege = await subpage.privilegeChecker(); + if (!hasPrivilege) { + continue; + } + } catch (error) { + console.error(`Error checking privilegeChecker for subpage ${subpage.path}:`, error); + continue; + } + } + + accessibleSubpages.push(subpage); } catch (error) { console.error(`Error checking RBAC access for subpage ${subpage.path}:`, error); } @@ -165,8 +181,7 @@ export const SidebarProvider: React.FC = ({ children }) => console.log('👤 SidebarProvider: Current user info:', { username: cachedUser?.username, roleLabels: cachedUser?.roleLabels, - roleLabelsLength: Array.isArray(cachedUser?.roleLabels) ? cachedUser.roleLabels.length : 0, - privilege: cachedUser?.privilege + roleLabelsLength: Array.isArray(cachedUser?.roleLabels) ? cachedUser.roleLabels.length : 0 }); // Process each main page @@ -191,6 +206,20 @@ export const SidebarProvider: React.FC = ({ children }) => console.log('⛔ SidebarProvider: Page hidden due to RBAC:', pageData.path); continue; } + + // Check client-side privilegeChecker if provided + if (pageData.privilegeChecker) { + try { + const hasPrivilege = await pageData.privilegeChecker(); + if (!hasPrivilege) { + console.log('⛔ SidebarProvider: Page hidden due to privilegeChecker:', pageData.path); + continue; + } + } catch (error) { + console.error(`❌ SidebarProvider: Error checking privilegeChecker for ${pageData.path}:`, error); + continue; + } + } } catch (error) { console.error(`❌ SidebarProvider: Error checking RBAC access for ${pageData.path}:`, error); continue; @@ -226,12 +255,27 @@ export const SidebarProvider: React.FC = ({ children }) => hasAccess: hasSubpageRBACAccess }); - if (hasSubpageRBACAccess) { - accessibleSubpages.push(subpage); - console.log('✅ SidebarProvider: Subpage added:', subpage.path); - } else { + if (!hasSubpageRBACAccess) { console.log('⛔ SidebarProvider: Subpage hidden due to RBAC:', subpage.path); + continue; } + + // Check client-side privilegeChecker if provided + if (subpage.privilegeChecker) { + try { + const hasPrivilege = await subpage.privilegeChecker(); + if (!hasPrivilege) { + console.log('⛔ SidebarProvider: Subpage hidden due to privilegeChecker:', subpage.path); + continue; + } + } catch (error) { + console.error(`❌ SidebarProvider: Error checking privilegeChecker for subpage ${subpage.path}:`, error); + continue; + } + } + + accessibleSubpages.push(subpage); + console.log('✅ SidebarProvider: Subpage added:', subpage.path); } catch (error) { console.error(`❌ SidebarProvider: Error checking RBAC access for subpage ${subpage.path}:`, error); } diff --git a/src/core/PageManager/data/pages/pek-tables.ts b/src/core/PageManager/data/pages/pek-tables.ts index 205d08c..154b735 100644 --- a/src/core/PageManager/data/pages/pek-tables.ts +++ b/src/core/PageManager/data/pages/pek-tables.ts @@ -1,6 +1,7 @@ import { GenericPageData } from '../../pageInterface'; import { FaTable, FaPlus } from 'react-icons/fa'; import { createProjectsTableHook, createParzellenTableHook } from '../../../../hooks/usePekTables'; +import { getUserDataCache } from '../../../../utils/userCache'; export const pekTablesPageData: GenericPageData = { id: 'pek-tables', @@ -188,6 +189,14 @@ export const pekTablesPageData: GenericPageData = { // Sidebar order: 11, showInSidebar: true, + + // Privilege checker: deny access for "user" role + privilegeChecker: async () => { + const userData = getUserDataCache(); + const roleLabels = Array.isArray(userData?.roleLabels) ? userData.roleLabels : []; + // Deny access if user has "user" role + return !roleLabels.includes('user'); + }, // Lifecycle hooks onActivate: async () => { diff --git a/src/core/PageManager/data/pages/pek.ts b/src/core/PageManager/data/pages/pek.ts index 48f4fff..92ef895 100644 --- a/src/core/PageManager/data/pages/pek.ts +++ b/src/core/PageManager/data/pages/pek.ts @@ -5,6 +5,7 @@ import PekLocationInput from './pek/PekLocationInput'; import PekMapView from './pek/PekMapView'; import { usePek } from '../../../../hooks/usePek'; import PekPageWrapper from './pek/PekPageWrapper'; +import { getUserDataCache } from '../../../../utils/userCache'; // Hook factory for PEK page const createPekHook = () => { @@ -102,6 +103,14 @@ export const pekPageData: GenericPageData = { order: 10, showInSidebar: true, + // Privilege checker: deny access for "user" role + privilegeChecker: async () => { + const userData = getUserDataCache(); + const roleLabels = Array.isArray(userData?.roleLabels) ? userData.roleLabels : []; + // Deny access if user has "user" role + return !roleLabels.includes('user'); + }, + // Custom component wrapper with PekProvider customComponent: PekPageWrapper, diff --git a/src/core/PageManager/pageInterface.ts b/src/core/PageManager/pageInterface.ts index f683e6d..d2c8dd0 100644 --- a/src/core/PageManager/pageInterface.ts +++ b/src/core/PageManager/pageInterface.ts @@ -367,6 +367,9 @@ export interface GenericPageData { // Custom component override (optional) customComponent?: React.ComponentType; + // Privilege checker - if provided, page will only render if checker returns true + privilegeChecker?: PrivilegeChecker; + // Drag and drop configuration dragDropConfig?: DragDropConfig; } diff --git a/src/hooks/useUsers.ts b/src/hooks/useUsers.ts index adc7c60..fd45a56 100644 --- a/src/hooks/useUsers.ts +++ b/src/hooks/useUsers.ts @@ -32,13 +32,11 @@ export function useCurrentUser() { if (cachedUser && cachedUser.username) { // Check if cached user has roleLabels - if empty, refetch from API const hasRoleLabels = Array.isArray(cachedUser.roleLabels) && cachedUser.roleLabels.length > 0; - const hasPrivilege = !!cachedUser.privilege; - if (!hasRoleLabels && !hasPrivilege) { - console.warn('⚠️ Cached user data has no roleLabels or privilege, refetching from API:', { + if (!hasRoleLabels) { + console.warn('⚠️ Cached user data has no roleLabels, refetching from API:', { username: cachedUser.username, - roleLabels: cachedUser.roleLabels, - privilege: cachedUser.privilege + roleLabels: cachedUser.roleLabels }); // Clear cache and continue to fetch from API clearUserDataCache(); @@ -47,8 +45,7 @@ export function useCurrentUser() { setUser(cachedUser); console.log('✅ Using cached user data from sessionStorage (persists during session):', { username: cachedUser.username, - roleLabels: cachedUser.roleLabels, - privilege: cachedUser.privilege + roleLabels: cachedUser.roleLabels }); return; } @@ -85,17 +82,15 @@ export function useCurrentUser() { console.log('📦 User data received from API:', { username: data?.username, roleLabels: data?.roleLabels, - privilege: data?.privilege, hasRoleLabels: !!data?.roleLabels, roleLabelsLength: Array.isArray(data?.roleLabels) ? data.roleLabels.length : 0, roleLabelsContent: Array.isArray(data?.roleLabels) ? data.roleLabels : 'not an array', - hasPrivilege: !!data?.privilege, allKeys: data ? Object.keys(data) : [], fullData: JSON.stringify(data, null, 2) }); // Always cache user data - permissions are checked via RBAC API, not client-side - // roleLabels/privilege are optional metadata for display/logging purposes + // roleLabels are optional metadata for display/logging purposes if (!data || !data.username) { console.error('❌ User data from API is invalid:', { username: data?.username, @@ -107,13 +102,11 @@ export function useCurrentUser() { // Check if API returned roleLabels - if not, log warning but still cache const hasRoleLabels = Array.isArray(data.roleLabels) && data.roleLabels.length > 0; - const hasPrivilege = !!data.privilege; - if (!hasRoleLabels && !hasPrivilege) { - console.warn('⚠️ User data from API has no roleLabels or privilege - this may cause RBAC issues:', { + if (!hasRoleLabels) { + console.warn('⚠️ User data from API has no roleLabels - this may cause RBAC issues:', { username: data.username, roleLabels: data.roleLabels, - privilege: data.privilege, allKeys: Object.keys(data), fullResponse: JSON.stringify(data, null, 2) }); @@ -127,9 +120,7 @@ export function useCurrentUser() { username: data.username, roleLabels: data.roleLabels, roleLabelsLength: Array.isArray(data.roleLabels) ? data.roleLabels.length : 0, - privilege: data.privilege, - hasRoleLabels, - hasPrivilege + hasRoleLabels }); setUser(data); } catch (error: any) { @@ -302,13 +293,11 @@ export function useCurrentUser() { if (cachedUser && cachedUser.username) { // Check if cached user has roleLabels - if empty, refetch from API const hasRoleLabels = Array.isArray(cachedUser.roleLabels) && cachedUser.roleLabels.length > 0; - const hasPrivilege = !!cachedUser.privilege; - if (!hasRoleLabels && !hasPrivilege) { - console.warn('⚠️ Cached user data has no roleLabels or privilege, refetching from API:', { + if (!hasRoleLabels) { + console.warn('⚠️ Cached user data has no roleLabels, refetching from API:', { username: cachedUser.username, - roleLabels: cachedUser.roleLabels, - privilege: cachedUser.privilege + roleLabels: cachedUser.roleLabels }); // Clear cache and refetch clearUserDataCache(); @@ -320,8 +309,7 @@ export function useCurrentUser() { setUser(cachedUser); console.log('✅ Using cached user data from sessionStorage on mount (persists during session):', { username: cachedUser.username, - roleLabels: cachedUser.roleLabels, - privilege: cachedUser.privilege + roleLabels: cachedUser.roleLabels }); } diff --git a/src/utils/privilegeCheckers.ts b/src/utils/privilegeCheckers.ts index 2c69c3d..0f207e8 100644 --- a/src/utils/privilegeCheckers.ts +++ b/src/utils/privilegeCheckers.ts @@ -11,10 +11,10 @@ import type { PermissionContext } from '../hooks/usePermissions'; * Now supports both client-side checks (roles, localStorage) and backend RBAC integration. */ -// Function to get current user privilege from sessionStorage cache -const getCurrentUserPrivilege = (): string | null => { +// Function to get current user role labels from sessionStorage cache +const getCurrentUserRoleLabels = (): string[] => { const userData = getUserDataCache(); - return userData?.privilege || null; + return Array.isArray(userData?.roleLabels) ? userData.roleLabels : []; }; // Generic privilege checker for localStorage-based data with expiration @@ -148,8 +148,9 @@ export const createCombinedPrivilegeChecker = ( } // If RBAC allows, also check client-side roles as additional validation - const userPrivilege = getCurrentUserPrivilege(); - if (userPrivilege && requiredRoles.includes(userPrivilege)) { + const userRoleLabels = getCurrentUserRoleLabels(); + const hasRequiredRole = requiredRoles.some(role => userRoleLabels.includes(role)); + if (hasRequiredRole) { return true; } @@ -229,8 +230,8 @@ export const privilegeCheckers = { adminRole: createRolePrivilegeChecker( ['admin', 'sysadmin'], () => { - const userPrivilege = getCurrentUserPrivilege(); - return Promise.resolve(userPrivilege ? [userPrivilege] : []); + const userRoleLabels = getCurrentUserRoleLabels(); + return Promise.resolve(userRoleLabels); } ), @@ -238,8 +239,8 @@ export const privilegeCheckers = { sysadminRole: createRolePrivilegeChecker( ['sysadmin'], () => { - const userPrivilege = getCurrentUserPrivilege(); - return Promise.resolve(userPrivilege ? [userPrivilege] : []); + const userRoleLabels = getCurrentUserRoleLabels(); + return Promise.resolve(userRoleLabels); } ), @@ -271,8 +272,8 @@ export const privilegeCheckers = { userRole: createRolePrivilegeChecker( ['user', 'admin', 'sysadmin'], () => { - const userPrivilege = getCurrentUserPrivilege(); - return Promise.resolve(userPrivilege ? [userPrivilege] : []); + const userRoleLabels = getCurrentUserRoleLabels(); + return Promise.resolve(userRoleLabels); } ), @@ -280,8 +281,8 @@ export const privilegeCheckers = { viewerRole: createRolePrivilegeChecker( ['viewer', 'user', 'admin', 'sysadmin'], () => { - const userPrivilege = getCurrentUserPrivilege(); - return Promise.resolve(userPrivilege ? [userPrivilege] : []); + const userRoleLabels = getCurrentUserRoleLabels(); + return Promise.resolve(userRoleLabels); } ),