feat: implemented priviledge checker into real estate pages

This commit is contained in:
Ida Dittrich 2026-01-12 11:44:10 +01:00
parent 239fd328bc
commit be3844f33e
9 changed files with 126 additions and 53 deletions

View file

@ -11,11 +11,10 @@ export interface User {
fullName: string; fullName: string;
language: string; language: string;
enabled: boolean; enabled: boolean;
privilege?: string; // Deprecated - use roleLabels instead
roleLabels?: string[]; // Array of role labels from backend (e.g., ["user"]) roleLabels?: string[]; // Array of role labels from backend (e.g., ["user"])
authenticationAuthority: string; authenticationAuthority: string;
mandateId: 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<Omit<User, 'id' | 'mandateId'>>; export type UserUpdateData = Partial<Omit<User, 'id' | 'mandateId'>>;
@ -92,7 +91,6 @@ export async function fetchCurrentUser(
hasData: !!response, hasData: !!response,
username: response?.username, username: response?.username,
roleLabels: response?.roleLabels, roleLabels: response?.roleLabels,
privilege: response?.privilege,
allKeys: response ? Object.keys(response) : [], allKeys: response ? Object.keys(response) : [],
fullResponse: response fullResponse: response
}); });

View file

@ -68,7 +68,7 @@ const SidebarUser: React.FC<SidebarUserProps> = ({ isMinimized = false }) => {
fullName: cached.fullName || cached.username.split('@')[0] || cached.username, fullName: cached.fullName || cached.username.split('@')[0] || cached.username,
language: cached.language || 'de', // Default language language: cached.language || 'de', // Default language
enabled: cached.enabled ?? true, // Assume enabled if logged in enabled: cached.enabled ?? true, // Assume enabled if logged in
privilege: cached.privilege || 'user', roleLabels: cached.roleLabels || [],
authenticationAuthority: cached.authenticationAuthority || 'local', authenticationAuthority: cached.authenticationAuthority || 'local',
mandateId: cached.mandateId || '' mandateId: cached.mandateId || ''
}; };
@ -97,7 +97,7 @@ const SidebarUser: React.FC<SidebarUserProps> = ({ isMinimized = false }) => {
fullName: cached.fullName || cached.username.split('@')[0] || cached.username, fullName: cached.fullName || cached.username.split('@')[0] || cached.username,
language: cached.language || 'de', language: cached.language || 'de',
enabled: cached.enabled ?? true, enabled: cached.enabled ?? true,
privilege: cached.privilege || 'user', roleLabels: cached.roleLabels || [],
authenticationAuthority: cached.authenticationAuthority || 'local', authenticationAuthority: cached.authenticationAuthority || 'local',
mandateId: cached.mandateId || '' mandateId: cached.mandateId || ''
}; };

View file

@ -68,11 +68,32 @@ const PageManager: React.FC<PageManagerProps> = ({
return; return;
} }
// Check page access // Check page access (RBAC + privilegeChecker)
console.log('🔍 PageManager: Checking access before rendering:', currentPath); console.log('🔍 PageManager: Checking access before rendering:', currentPath);
checkPageAccess(pageData).then(hasAccess => {
// First check client-side privilegeChecker if provided
const checkPrivilege = async (): Promise<boolean> => {
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:', { console.log('🔍 PageManager: Access check complete:', {
path: currentPath, path: currentPath,
hasPrivilege,
hasRBACAccess,
hasAccess hasAccess
}); });

View file

@ -123,14 +123,30 @@ export const SidebarProvider: React.FC<SidebarProviderProps> = ({ children }) =>
// Process parent groups // Process parent groups
for (const [_parentPath, parentGroup] of parentGroups.entries()) { for (const [_parentPath, parentGroup] of parentGroups.entries()) {
// Filter subpages by RBAC access // Filter subpages by RBAC access and privilegeChecker
const accessibleSubpages = []; const accessibleSubpages = [];
for (const subpage of parentGroup.subpages) { for (const subpage of parentGroup.subpages) {
try { try {
// Check RBAC access
const hasSubpageRBACAccess = await canView('UI', subpage.path); const hasSubpageRBACAccess = await canView('UI', subpage.path);
if (hasSubpageRBACAccess) { if (!hasSubpageRBACAccess) {
accessibleSubpages.push(subpage); 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) { } catch (error) {
console.error(`Error checking RBAC access for subpage ${subpage.path}:`, error); console.error(`Error checking RBAC access for subpage ${subpage.path}:`, error);
} }
@ -165,8 +181,7 @@ export const SidebarProvider: React.FC<SidebarProviderProps> = ({ children }) =>
console.log('👤 SidebarProvider: Current user info:', { console.log('👤 SidebarProvider: Current user info:', {
username: cachedUser?.username, username: cachedUser?.username,
roleLabels: cachedUser?.roleLabels, roleLabels: cachedUser?.roleLabels,
roleLabelsLength: Array.isArray(cachedUser?.roleLabels) ? cachedUser.roleLabels.length : 0, roleLabelsLength: Array.isArray(cachedUser?.roleLabels) ? cachedUser.roleLabels.length : 0
privilege: cachedUser?.privilege
}); });
// Process each main page // Process each main page
@ -191,6 +206,20 @@ export const SidebarProvider: React.FC<SidebarProviderProps> = ({ children }) =>
console.log('⛔ SidebarProvider: Page hidden due to RBAC:', pageData.path); console.log('⛔ SidebarProvider: Page hidden due to RBAC:', pageData.path);
continue; 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) { } catch (error) {
console.error(`❌ SidebarProvider: Error checking RBAC access for ${pageData.path}:`, error); console.error(`❌ SidebarProvider: Error checking RBAC access for ${pageData.path}:`, error);
continue; continue;
@ -226,12 +255,27 @@ export const SidebarProvider: React.FC<SidebarProviderProps> = ({ children }) =>
hasAccess: hasSubpageRBACAccess hasAccess: hasSubpageRBACAccess
}); });
if (hasSubpageRBACAccess) { 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); accessibleSubpages.push(subpage);
console.log('✅ SidebarProvider: Subpage added:', subpage.path); console.log('✅ SidebarProvider: Subpage added:', subpage.path);
} else {
console.log('⛔ SidebarProvider: Subpage hidden due to RBAC:', subpage.path);
}
} catch (error) { } catch (error) {
console.error(`❌ SidebarProvider: Error checking RBAC access for subpage ${subpage.path}:`, error); console.error(`❌ SidebarProvider: Error checking RBAC access for subpage ${subpage.path}:`, error);
} }

View file

@ -1,6 +1,7 @@
import { GenericPageData } from '../../pageInterface'; import { GenericPageData } from '../../pageInterface';
import { FaTable, FaPlus } from 'react-icons/fa'; import { FaTable, FaPlus } from 'react-icons/fa';
import { createProjectsTableHook, createParzellenTableHook } from '../../../../hooks/usePekTables'; import { createProjectsTableHook, createParzellenTableHook } from '../../../../hooks/usePekTables';
import { getUserDataCache } from '../../../../utils/userCache';
export const pekTablesPageData: GenericPageData = { export const pekTablesPageData: GenericPageData = {
id: 'pek-tables', id: 'pek-tables',
@ -189,6 +190,14 @@ export const pekTablesPageData: GenericPageData = {
order: 11, order: 11,
showInSidebar: true, 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 // Lifecycle hooks
onActivate: async () => { onActivate: async () => {
if (import.meta.env.DEV) console.log('PEK Tables page activated'); if (import.meta.env.DEV) console.log('PEK Tables page activated');

View file

@ -5,6 +5,7 @@ import PekLocationInput from './pek/PekLocationInput';
import PekMapView from './pek/PekMapView'; import PekMapView from './pek/PekMapView';
import { usePek } from '../../../../hooks/usePek'; import { usePek } from '../../../../hooks/usePek';
import PekPageWrapper from './pek/PekPageWrapper'; import PekPageWrapper from './pek/PekPageWrapper';
import { getUserDataCache } from '../../../../utils/userCache';
// Hook factory for PEK page // Hook factory for PEK page
const createPekHook = () => { const createPekHook = () => {
@ -102,6 +103,14 @@ export const pekPageData: GenericPageData = {
order: 10, order: 10,
showInSidebar: true, 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 // Custom component wrapper with PekProvider
customComponent: PekPageWrapper, customComponent: PekPageWrapper,

View file

@ -367,6 +367,9 @@ export interface GenericPageData {
// Custom component override (optional) // Custom component override (optional)
customComponent?: React.ComponentType<any>; customComponent?: React.ComponentType<any>;
// Privilege checker - if provided, page will only render if checker returns true
privilegeChecker?: PrivilegeChecker;
// Drag and drop configuration // Drag and drop configuration
dragDropConfig?: DragDropConfig; dragDropConfig?: DragDropConfig;
} }

View file

@ -32,13 +32,11 @@ export function useCurrentUser() {
if (cachedUser && cachedUser.username) { if (cachedUser && cachedUser.username) {
// Check if cached user has roleLabels - if empty, refetch from API // Check if cached user has roleLabels - if empty, refetch from API
const hasRoleLabels = Array.isArray(cachedUser.roleLabels) && cachedUser.roleLabels.length > 0; const hasRoleLabels = Array.isArray(cachedUser.roleLabels) && cachedUser.roleLabels.length > 0;
const hasPrivilege = !!cachedUser.privilege;
if (!hasRoleLabels && !hasPrivilege) { if (!hasRoleLabels) {
console.warn('⚠️ Cached user data has no roleLabels or privilege, refetching from API:', { console.warn('⚠️ Cached user data has no roleLabels, refetching from API:', {
username: cachedUser.username, username: cachedUser.username,
roleLabels: cachedUser.roleLabels, roleLabels: cachedUser.roleLabels
privilege: cachedUser.privilege
}); });
// Clear cache and continue to fetch from API // Clear cache and continue to fetch from API
clearUserDataCache(); clearUserDataCache();
@ -47,8 +45,7 @@ export function useCurrentUser() {
setUser(cachedUser); setUser(cachedUser);
console.log('✅ Using cached user data from sessionStorage (persists during session):', { console.log('✅ Using cached user data from sessionStorage (persists during session):', {
username: cachedUser.username, username: cachedUser.username,
roleLabels: cachedUser.roleLabels, roleLabels: cachedUser.roleLabels
privilege: cachedUser.privilege
}); });
return; return;
} }
@ -85,17 +82,15 @@ export function useCurrentUser() {
console.log('📦 User data received from API:', { console.log('📦 User data received from API:', {
username: data?.username, username: data?.username,
roleLabels: data?.roleLabels, roleLabels: data?.roleLabels,
privilege: data?.privilege,
hasRoleLabels: !!data?.roleLabels, hasRoleLabels: !!data?.roleLabels,
roleLabelsLength: Array.isArray(data?.roleLabels) ? data.roleLabels.length : 0, roleLabelsLength: Array.isArray(data?.roleLabels) ? data.roleLabels.length : 0,
roleLabelsContent: Array.isArray(data?.roleLabels) ? data.roleLabels : 'not an array', roleLabelsContent: Array.isArray(data?.roleLabels) ? data.roleLabels : 'not an array',
hasPrivilege: !!data?.privilege,
allKeys: data ? Object.keys(data) : [], allKeys: data ? Object.keys(data) : [],
fullData: JSON.stringify(data, null, 2) fullData: JSON.stringify(data, null, 2)
}); });
// Always cache user data - permissions are checked via RBAC API, not client-side // 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) { if (!data || !data.username) {
console.error('❌ User data from API is invalid:', { console.error('❌ User data from API is invalid:', {
username: data?.username, username: data?.username,
@ -107,13 +102,11 @@ export function useCurrentUser() {
// Check if API returned roleLabels - if not, log warning but still cache // Check if API returned roleLabels - if not, log warning but still cache
const hasRoleLabels = Array.isArray(data.roleLabels) && data.roleLabels.length > 0; const hasRoleLabels = Array.isArray(data.roleLabels) && data.roleLabels.length > 0;
const hasPrivilege = !!data.privilege;
if (!hasRoleLabels && !hasPrivilege) { if (!hasRoleLabels) {
console.warn('⚠️ User data from API has no roleLabels or privilege - this may cause RBAC issues:', { console.warn('⚠️ User data from API has no roleLabels - this may cause RBAC issues:', {
username: data.username, username: data.username,
roleLabels: data.roleLabels, roleLabels: data.roleLabels,
privilege: data.privilege,
allKeys: Object.keys(data), allKeys: Object.keys(data),
fullResponse: JSON.stringify(data, null, 2) fullResponse: JSON.stringify(data, null, 2)
}); });
@ -127,9 +120,7 @@ export function useCurrentUser() {
username: data.username, username: data.username,
roleLabels: data.roleLabels, roleLabels: data.roleLabels,
roleLabelsLength: Array.isArray(data.roleLabels) ? data.roleLabels.length : 0, roleLabelsLength: Array.isArray(data.roleLabels) ? data.roleLabels.length : 0,
privilege: data.privilege, hasRoleLabels
hasRoleLabels,
hasPrivilege
}); });
setUser(data); setUser(data);
} catch (error: any) { } catch (error: any) {
@ -302,13 +293,11 @@ export function useCurrentUser() {
if (cachedUser && cachedUser.username) { if (cachedUser && cachedUser.username) {
// Check if cached user has roleLabels - if empty, refetch from API // Check if cached user has roleLabels - if empty, refetch from API
const hasRoleLabels = Array.isArray(cachedUser.roleLabels) && cachedUser.roleLabels.length > 0; const hasRoleLabels = Array.isArray(cachedUser.roleLabels) && cachedUser.roleLabels.length > 0;
const hasPrivilege = !!cachedUser.privilege;
if (!hasRoleLabels && !hasPrivilege) { if (!hasRoleLabels) {
console.warn('⚠️ Cached user data has no roleLabels or privilege, refetching from API:', { console.warn('⚠️ Cached user data has no roleLabels, refetching from API:', {
username: cachedUser.username, username: cachedUser.username,
roleLabels: cachedUser.roleLabels, roleLabels: cachedUser.roleLabels
privilege: cachedUser.privilege
}); });
// Clear cache and refetch // Clear cache and refetch
clearUserDataCache(); clearUserDataCache();
@ -320,8 +309,7 @@ export function useCurrentUser() {
setUser(cachedUser); setUser(cachedUser);
console.log('✅ Using cached user data from sessionStorage on mount (persists during session):', { console.log('✅ Using cached user data from sessionStorage on mount (persists during session):', {
username: cachedUser.username, username: cachedUser.username,
roleLabels: cachedUser.roleLabels, roleLabels: cachedUser.roleLabels
privilege: cachedUser.privilege
}); });
} }

View file

@ -11,10 +11,10 @@ import type { PermissionContext } from '../hooks/usePermissions';
* Now supports both client-side checks (roles, localStorage) and backend RBAC integration. * Now supports both client-side checks (roles, localStorage) and backend RBAC integration.
*/ */
// Function to get current user privilege from sessionStorage cache // Function to get current user role labels from sessionStorage cache
const getCurrentUserPrivilege = (): string | null => { const getCurrentUserRoleLabels = (): string[] => {
const userData = getUserDataCache(); const userData = getUserDataCache();
return userData?.privilege || null; return Array.isArray(userData?.roleLabels) ? userData.roleLabels : [];
}; };
// Generic privilege checker for localStorage-based data with expiration // 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 // If RBAC allows, also check client-side roles as additional validation
const userPrivilege = getCurrentUserPrivilege(); const userRoleLabels = getCurrentUserRoleLabels();
if (userPrivilege && requiredRoles.includes(userPrivilege)) { const hasRequiredRole = requiredRoles.some(role => userRoleLabels.includes(role));
if (hasRequiredRole) {
return true; return true;
} }
@ -229,8 +230,8 @@ export const privilegeCheckers = {
adminRole: createRolePrivilegeChecker( adminRole: createRolePrivilegeChecker(
['admin', 'sysadmin'], ['admin', 'sysadmin'],
() => { () => {
const userPrivilege = getCurrentUserPrivilege(); const userRoleLabels = getCurrentUserRoleLabels();
return Promise.resolve(userPrivilege ? [userPrivilege] : []); return Promise.resolve(userRoleLabels);
} }
), ),
@ -238,8 +239,8 @@ export const privilegeCheckers = {
sysadminRole: createRolePrivilegeChecker( sysadminRole: createRolePrivilegeChecker(
['sysadmin'], ['sysadmin'],
() => { () => {
const userPrivilege = getCurrentUserPrivilege(); const userRoleLabels = getCurrentUserRoleLabels();
return Promise.resolve(userPrivilege ? [userPrivilege] : []); return Promise.resolve(userRoleLabels);
} }
), ),
@ -271,8 +272,8 @@ export const privilegeCheckers = {
userRole: createRolePrivilegeChecker( userRole: createRolePrivilegeChecker(
['user', 'admin', 'sysadmin'], ['user', 'admin', 'sysadmin'],
() => { () => {
const userPrivilege = getCurrentUserPrivilege(); const userRoleLabels = getCurrentUserRoleLabels();
return Promise.resolve(userPrivilege ? [userPrivilege] : []); return Promise.resolve(userRoleLabels);
} }
), ),
@ -280,8 +281,8 @@ export const privilegeCheckers = {
viewerRole: createRolePrivilegeChecker( viewerRole: createRolePrivilegeChecker(
['viewer', 'user', 'admin', 'sysadmin'], ['viewer', 'user', 'admin', 'sysadmin'],
() => { () => {
const userPrivilege = getCurrentUserPrivilege(); const userRoleLabels = getCurrentUserRoleLabels();
return Promise.resolve(userPrivilege ? [userPrivilege] : []); return Promise.resolve(userRoleLabels);
} }
), ),