fix: consolidated priviledgechecker and usepermissions hook

This commit is contained in:
Ida Dittrich 2025-12-30 11:04:03 +01:00
parent 14273c248a
commit d3c950d735
25 changed files with 343 additions and 200 deletions

View file

@ -4,12 +4,14 @@ import styles from './FormGeneratorControls.module.css';
import { Button } from '../../UiComponents/Button';
import { IoIosRefresh } from "react-icons/io";
import { FaTrash } from "react-icons/fa";
import { isCheckboxType } from '../../../utils/attributeTypeMapper';
import type { AttributeType } from '../../../utils/attributeTypeMapper';
// Generic field/column config interface
export interface FilterableField {
key: string;
label: string;
type?: 'string' | 'number' | 'date' | 'boolean' | 'enum' | 'readonly';
type?: AttributeType;
filterable?: boolean;
filterOptions?: string[];
}
@ -215,7 +217,7 @@ export function FormGeneratorControls({
<div className={styles.filtersContainer}>
{filterableFields.map(field => (
<div key={field.key} className={styles.filterGroup}>
{field.type === 'boolean' ? (
{field.type && isCheckboxType(field.type) ? (
<div className={styles.customSelectContainer}>
<select
value={filters[field.key] || ''}

View file

@ -2,13 +2,23 @@ import React, { useState, useEffect, useCallback } from 'react';
import { useLanguage } from '../../../providers/language/LanguageContext';
import api from '../../../api';
import styles from './FormGeneratorForm.module.css';
import {
attributeTypeToInputType,
isTextareaType,
isSelectType,
isMultiselectType,
isCheckboxType,
isFileType,
isNumberType,
isDateTimeType,
getDefaultValueForType
} from '../../../utils/attributeTypeMapper';
import type { AttributeType } from '../../../utils/attributeTypeMapper';
// Attribute definition interface (matches backend structure)
export interface AttributeDefinition {
name: string;
type: 'text' | 'email' | 'date' | 'checkbox' | 'select' | 'multiselect' | 'number' | 'textarea' |
'timestamp' | 'time' | 'url' | 'password' | 'file' | 'integer' | 'float' | 'string' |
'boolean' | 'enum' | 'readonly';
type: AttributeType;
label: string;
description?: string;
required?: boolean;
@ -177,12 +187,8 @@ export function FormGeneratorForm<T extends Record<string, any>>({
filteredAttrs.forEach(attr => {
if (attr.default !== undefined) {
initialData[attr.name] = attr.default;
} else if (attr.type === 'checkbox' || attr.type === 'boolean') {
initialData[attr.name] = false;
} else if (attr.type === 'multiselect') {
initialData[attr.name] = [];
} else {
initialData[attr.name] = '';
initialData[attr.name] = getDefaultValueForType(attr.type);
}
});
setFormData(initialData as T);
@ -332,7 +338,7 @@ export function FormGeneratorForm<T extends Record<string, any>>({
}
// Number/Float validation
if (attr.type === 'number' || attr.type === 'float') {
if (isNumberType(attr.type)) {
if (isNaN(Number(value))) {
newErrors[attr.name] = t('formgen.form.invalidNumber', `${attr.label} must be a valid number`);
return;
@ -358,7 +364,7 @@ export function FormGeneratorForm<T extends Record<string, any>>({
}
// Select/Multiselect option validation
if (attr.type === 'select' || attr.type === 'enum') {
if (isSelectType(attr.type)) {
const options = normalizeOptions(attr);
if (options.length > 0 && !options.some(opt => String(opt.value) === String(value))) {
newErrors[attr.name] = t('formgen.form.invalidOption', 'Invalid option selected');
@ -367,7 +373,7 @@ export function FormGeneratorForm<T extends Record<string, any>>({
}
// Timestamp/Date validation
if (attr.type === 'timestamp' || attr.type === 'date' || attr.type === 'time') {
if (isDateTimeType(attr.type)) {
const dateValue = new Date(String(value));
if (isNaN(dateValue.getTime())) {
newErrors[attr.name] = t('formgen.form.invalidDate', 'Invalid date format');
@ -451,13 +457,13 @@ export function FormGeneratorForm<T extends Record<string, any>>({
// Readonly/Display field
if (isReadonly) {
let displayValue = value;
if (attr.type === 'checkbox' || attr.type === 'boolean') {
if (isCheckboxType(attr.type)) {
displayValue = value ? t('common.yes', 'Yes') : t('common.no', 'No');
} else if (attr.type === 'select' || attr.type === 'enum') {
} else if (isSelectType(attr.type)) {
const options = normalizeOptions(attr);
const selectedOption = options.find(opt => String(opt.value) === String(value));
displayValue = selectedOption ? selectedOption.label : value;
} else if (attr.type === 'multiselect') {
} else if (isMultiselectType(attr.type)) {
const options = normalizeOptions(attr);
const selectedValues = Array.isArray(value) ? value : (value ? [value] : []);
displayValue = selectedValues.map(v => {
@ -479,7 +485,7 @@ export function FormGeneratorForm<T extends Record<string, any>>({
}
// Select/Enum field
if (attr.type === 'select' || attr.type === 'enum') {
if (isSelectType(attr.type)) {
const options = normalizeOptions(attr);
const isLoading = typeof attr.options === 'string' && loadingOptions[attr.name];
@ -508,7 +514,7 @@ export function FormGeneratorForm<T extends Record<string, any>>({
}
// Multiselect field
if (attr.type === 'multiselect') {
if (isMultiselectType(attr.type)) {
const options = normalizeOptions(attr);
const currentValues = Array.isArray(value) ? value : (value ? [value] : []);
const isLoading = typeof attr.options === 'string' && loadingOptions[attr.name];
@ -562,7 +568,7 @@ export function FormGeneratorForm<T extends Record<string, any>>({
}
// Checkbox/Boolean field
if (attr.type === 'checkbox' || attr.type === 'boolean') {
if (isCheckboxType(attr.type)) {
return (
<div className={styles.fieldGroup} key={attr.name}>
<label className={styles.checkboxLabel}>
@ -583,7 +589,7 @@ export function FormGeneratorForm<T extends Record<string, any>>({
}
// Textarea field
if (attr.type === 'textarea') {
if (isTextareaType(attr.type)) {
const minRows = attr.minRows || 4;
const maxRows = attr.maxRows || 8;
const minHeight = minRows * 1.5 * 16;
@ -635,7 +641,7 @@ export function FormGeneratorForm<T extends Record<string, any>>({
}
// File field
if (attr.type === 'file') {
if (isFileType(attr.type)) {
return (
<div className={styles.floatingLabelInput} key={attr.name}>
<input
@ -658,14 +664,7 @@ export function FormGeneratorForm<T extends Record<string, any>>({
}
// Default input field (text, email, date, time, url, password, number, integer, float)
const inputType = attr.type === 'email' ? 'email' :
attr.type === 'date' ? 'date' :
attr.type === 'time' ? 'time' :
attr.type === 'timestamp' ? 'datetime-local' :
attr.type === 'url' ? 'url' :
attr.type === 'password' ? 'password' :
(attr.type === 'number' || attr.type === 'integer' || attr.type === 'float') ? 'number' :
'text';
const inputType = attributeTypeToInputType(attr.type);
return (
<div className={styles.floatingLabelInput} key={attr.name}>
@ -674,7 +673,7 @@ export function FormGeneratorForm<T extends Record<string, any>>({
value={value || ''}
onChange={(e) => {
let newValue: any = e.target.value;
if (attr.type === 'number' || attr.type === 'integer' || attr.type === 'float') {
if (isNumberType(attr.type)) {
newValue = e.target.value === '' ? '' : Number(e.target.value);
}
handleFieldChange(attr.name, newValue);

View file

@ -13,12 +13,18 @@ import {
import { formatUnixTimestamp } from '../../../utils/time';
import TextField from '../../UiComponents/TextField/TextField';
import { FormGeneratorControls } from '../FormGeneratorControls';
import {
isSelectType,
isCheckboxType,
attributeTypeToInputType
} from '../../../utils/attributeTypeMapper';
import type { AttributeType } from '../../../utils/attributeTypeMapper';
// Types for the FormGeneratorList
export interface FieldConfig {
key: string;
label: string;
type?: 'string' | 'number' | 'date' | 'boolean' | 'enum' | 'readonly';
type?: AttributeType;
editable?: boolean;
required?: boolean;
formatter?: (value: any, row: any) => React.ReactNode;
@ -456,7 +462,7 @@ export function FormGeneratorList<T extends Record<string, any>>({
);
}
if (field.type === 'enum' && field.options) {
if (isSelectType(field.type || 'string') && field.options) {
return (
<select
key={field.key}
@ -472,7 +478,7 @@ export function FormGeneratorList<T extends Record<string, any>>({
);
}
if (field.type === 'boolean') {
if (isCheckboxType(field.type || 'string')) {
return (
<input
key={field.key}
@ -490,7 +496,7 @@ export function FormGeneratorList<T extends Record<string, any>>({
key={field.key}
value={value || ''}
onChange={(newValue) => onFieldChange?.(row, field.key, newValue)}
type={field.type === 'date' ? 'date' : field.type === 'number' ? 'number' : 'text'}
type={attributeTypeToInputType(field.type || 'string')}
required={field.required}
readonly={!field.editable}
className={styles.fieldInput}

View file

@ -13,12 +13,16 @@ import {
import { formatUnixTimestamp } from '../../../utils/time';
import { FormGeneratorControls } from '../FormGeneratorControls';
import { CopyableTruncatedValue } from '../../UiComponents/CopyableTruncatedValue';
import {
isDateTimeType
} from '../../../utils/attributeTypeMapper';
import type { AttributeType } from '../../../utils/attributeTypeMapper';
// Types for the FormGeneratorTable
export interface ColumnConfig {
key: string;
label: string;
type?: 'string' | 'number' | 'date' | 'boolean' | 'enum';
type?: AttributeType;
width?: number;
minWidth?: number;
maxWidth?: number;
@ -526,7 +530,8 @@ export function FormGeneratorTable<T extends Record<string, any>>({
const isLikelyTimestamp = typeof value === 'number' && value > 0 && value < 4102444800000;
// If it's a timestamp field or looks like a timestamp, format as date
if ((isTimestampField || isLikelyTimestamp) && typeof value === 'number') {
// Also check if column type is a date/time type
if ((isTimestampField || isLikelyTimestamp || (column.type && isDateTimeType(column.type))) && typeof value === 'number') {
try {
// Handle Unix timestamps in seconds (backend format)
let timestamp: number;
@ -557,6 +562,8 @@ export function FormGeneratorTable<T extends Record<string, any>>({
switch (column.type) {
case 'date':
case 'timestamp':
case 'time':
try {
// Handle Unix timestamps in seconds (backend format)
let timestamp: number;

View file

@ -1,5 +1,12 @@
export { default as FormGenerator } from './FormGenerator';
export type { ColumnConfig, FormGeneratorProps } from './FormGenerator';
// Re-export FormGenerator components
export * from './FormGeneratorTable';
export * from './FormGeneratorList';
export * from './FormGeneratorForm';
export * from './FormGeneratorControls';
// Alias FormGeneratorTable as FormGenerator for backward compatibility
export { FormGeneratorTable as FormGenerator, FormGeneratorTableComponent as FormGeneratorComponent } from './FormGeneratorTable';
export type { FormGeneratorTableProps as FormGeneratorProps, ColumnConfig } from './FormGeneratorTable';
// Re-export action button components and types
export * from './ActionButtons';

View file

@ -1,56 +0,0 @@
/* ViewForm container */
.viewForm {
width: 100%;
}
/* Field styling */
.fieldGroup {
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #f3f4f6;
}
.fieldGroup:last-child {
border-bottom: none;
margin-bottom: 0;
}
.fieldLabel {
display: block;
font-weight: 600;
color: #374151;
margin-bottom: 6px;
font-size: 14px;
text-transform: capitalize;
}
.fieldValue {
color: #6b7280;
font-size: 14px;
line-height: 1.5;
word-break: break-word;
padding: 4px 0;
}
/* Special styling for different value types */
.fieldValue:empty::before {
content: 'N/A';
color: #9ca3af;
font-style: italic;
}
/* Responsive design */
@media (max-width: 640px) {
.fieldGroup {
margin-bottom: 12px;
padding-bottom: 8px;
}
.fieldLabel {
font-size: 13px;
}
.fieldValue {
font-size: 13px;
}
}

View file

@ -1,46 +0,0 @@
import styles from './ViewForm.module.css';
// Field configuration interface for ViewForm
export interface ViewFieldConfig {
key: string;
label: string;
formatter?: (value: any) => string;
}
// ViewForm props - for display-only purposes
export interface ViewFormProps<T = any> {
data: T;
fields: ViewFieldConfig[];
className?: string;
}
// ViewForm component - displays data in read-only format
export function ViewForm<T extends Record<string, any>>({
data,
fields,
className = ''
}: ViewFormProps<T>) {
// Render field in view-only mode
const renderField = (field: ViewFieldConfig) => {
const value = data[field.key];
return (
<div className={styles.fieldGroup} key={field.key}>
<label className={styles.fieldLabel}>{field.label}</label>
<div className={styles.fieldValue}>
{field.formatter ? field.formatter(value) : (value || 'N/A')}
</div>
</div>
);
};
return (
<div className={`${styles.viewForm} ${className}`}>
{fields.map(field => renderField(field))}
</div>
);
}
export default ViewForm;

View file

@ -5,7 +5,3 @@ export type { PopupProps, PopupAction } from './Popup';
// FormGeneratorForm component (recommended for backend-driven forms)
export { FormGeneratorForm } from '../../FormGenerator/FormGeneratorForm';
export type { FormGeneratorFormProps, AttributeDefinition, AttributeOption } from '../../FormGenerator/FormGeneratorForm';
// ViewForm component
export { ViewForm } from './ViewForm';
export type { ViewFormProps } from './ViewForm';

View file

@ -0,0 +1,43 @@
.viewForm {
display: flex;
flex-direction: column;
gap: 1.5rem;
padding: 1rem 0;
}
.fieldGroup {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.fieldLabel {
font-weight: 600;
font-size: 0.875rem;
color: var(--text-secondary, #666);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.fieldValue {
font-size: 1rem;
color: var(--text-primary, #333);
padding: 0.75rem;
background-color: var(--background-secondary, #f5f5f5);
border-radius: 4px;
min-height: 2.5rem;
display: flex;
align-items: center;
word-break: break-word;
}
/* Dark theme support */
[data-theme="dark"] .fieldLabel {
color: var(--text-secondary, #aaa);
}
[data-theme="dark"] .fieldValue {
color: var(--text-primary, #e0e0e0);
background-color: var(--background-secondary, #2a2a2a);
}

View file

@ -0,0 +1,114 @@
import styles from './ViewForm.module.css';
import {
isCheckboxType,
isSelectType,
isMultiselectType,
isDateTimeType
} from '../../../utils/attributeTypeMapper';
import type { AttributeType } from '../../../utils/attributeTypeMapper';
// Field configuration interface for ViewForm
export interface ViewFieldConfig {
key: string;
label: string;
type?: AttributeType;
formatter?: (value: any) => string;
options?: Array<{ value: string | number; label: string }>; // For select/enum types
}
// ViewForm props - for display-only purposes
export interface ViewFormProps<T = any> {
data: T;
fields: ViewFieldConfig[];
className?: string;
}
// ViewForm component - displays data in read-only format
export function ViewForm<T extends Record<string, any>>({
data,
fields,
className = ''
}: ViewFormProps<T>) {
// Format value based on field type
const formatValue = (field: ViewFieldConfig, value: any): string => {
// Use custom formatter if provided
if (field.formatter) {
return field.formatter(value);
}
// Handle null/undefined
if (value === null || value === undefined) {
return 'N/A';
}
// Type-based formatting
if (field.type) {
// Boolean/Checkbox types
if (isCheckboxType(field.type)) {
return value ? 'Yes' : 'No';
}
// Select/Enum types
if (isSelectType(field.type) && field.options) {
const option = field.options.find(opt => String(opt.value) === String(value));
return option ? option.label : String(value);
}
// Multiselect types
if (isMultiselectType(field.type) && field.options) {
const selectedValues = Array.isArray(value) ? value : (value ? [value] : []);
if (selectedValues.length === 0) {
return 'None';
}
return selectedValues.map(v => {
const option = field.options!.find(opt => String(opt.value) === String(v));
return option ? option.label : String(v);
}).join(', ');
}
// Date/Time/Timestamp types
if (isDateTimeType(field.type)) {
try {
const date = value instanceof Date ? value : new Date(value);
if (!isNaN(date.getTime())) {
return date.toLocaleString();
}
} catch {
// Fall through to default
}
}
}
// Default: convert to string
if (Array.isArray(value)) {
return value.length > 0 ? value.join(', ') : 'None';
}
return String(value);
};
// Render field in view-only mode
const renderField = (field: ViewFieldConfig) => {
const value = data[field.key];
const formattedValue = formatValue(field, value);
return (
<div className={styles.fieldGroup} key={field.key}>
<label className={styles.fieldLabel}>{field.label}</label>
<div className={styles.fieldValue}>
{formattedValue}
</div>
</div>
);
};
return (
<div className={`${styles.viewForm} ${className}`}>
{fields.map(field => renderField(field))}
</div>
);
}
export default ViewForm;

View file

@ -0,0 +1,3 @@
export { ViewForm, default as DefaultViewForm } from './ViewForm';
export type { ViewFormProps, ViewFieldConfig } from './ViewForm';

View file

@ -26,7 +26,7 @@ const PageManager: React.FC<PageManagerProps> = ({
const currentPath = getCurrentPath();
// Check if user has access to a page using RBAC
// Check if user has access to a page using backend RBAC permissions
const checkPageAccess = async (pageData: GenericPageData): Promise<boolean> => {
try {
return await canView('UI', pageData.path);

View file

@ -1,7 +1,6 @@
import { useCallback } from 'react';
import { GenericPageData } from '../../pageInterface';
import { FaGoogle, FaMicrosoft, FaLink } from 'react-icons/fa';
import { privilegeCheckers } from '../../../../utils/privilegeCheckers';
import { useConnections } from '../../../../hooks/useConnections';
// Helper function to convert attribute definitions to column config
@ -233,9 +232,6 @@ export const connectionsPageData: GenericPageData = {
}
],
// Privilege system
privilegeChecker: privilegeCheckers.viewerRole,
// Page behavior
persistent: false,
preload: false,

View file

@ -3,7 +3,6 @@ import { LuTicket } from 'react-icons/lu';
import { IoMdSend } from 'react-icons/io';
import { MdStop } from 'react-icons/md';
import { HiOutlineCollection } from 'react-icons/hi';
import { privilegeCheckers } from '../../../../utils/privilegeCheckers';
import { createDashboardHook } from '../../../../hooks/usePlayground';
export const dashboardPageData: GenericPageData = {
@ -81,9 +80,6 @@ export const dashboardPageData: GenericPageData = {
}
],
// Privilege system
privilegeChecker: privilegeCheckers.viewerRole,
// Page behavior
persistent: true,
preserveState: true,

View file

@ -1,7 +1,6 @@
import { useCallback } from 'react';
import { GenericPageData } from '../../pageInterface';
import { FaRegFileAlt, FaUpload } from 'react-icons/fa';
import { privilegeCheckers } from '../../../../utils/privilegeCheckers';
import { useUserFiles, useFileOperations } from '../../../../hooks/useFiles';
// Helper function to convert attribute definitions to column config
@ -272,9 +271,6 @@ export const filesPageData: GenericPageData = {
}
],
// Privilege system
privilegeChecker: privilegeCheckers.viewerRole,
// Page behavior
persistent: false,
preload: false,

View file

@ -1,7 +1,6 @@
import { GenericPageData } from '../../pageInterface';
import { FaTable, FaBuilding } from 'react-icons/fa';
import { IoMdSend } from 'react-icons/io';
import { privilegeCheckers } from '../../../../utils/privilegeCheckers';
import { usePekTablesContext } from '../../../../contexts/PekTablesContext';
import PekTablesDropdown from './pek-tables/PekTablesDropdown';
import PekTablesPageWrapper from './pek-tables/PekTablesPageWrapper';
@ -104,9 +103,6 @@ export const pekTablesPageData: GenericPageData = {
}
],
// Privilege system
privilegeChecker: privilegeCheckers.viewerRole,
// Page behavior
persistent: false,
preload: false,

View file

@ -1,7 +1,6 @@
import { GenericPageData } from '../../pageInterface';
import { FaBuilding } from 'react-icons/fa';
import { IoMdSend } from 'react-icons/io';
import { privilegeCheckers } from '../../../../utils/privilegeCheckers';
import PekLocationInput from './pek/PekLocationInput';
import PekMapView from './pek/PekMapView';
import { usePek } from '../../../../hooks/usePek';
@ -93,9 +92,6 @@ export const pekPageData: GenericPageData = {
}
],
// Privilege system
privilegeChecker: privilegeCheckers.viewerRole,
// Page behavior
persistent: false,
preload: false,

View file

@ -1,7 +1,6 @@
import { useCallback } from 'react';
import { GenericPageData } from '../../pageInterface';
import { FaLightbulb, FaPlus } from 'react-icons/fa';
import { privilegeCheckers } from '../../../../utils/privilegeCheckers';
import { usePrompts, usePromptOperations } from '../../../../hooks/usePrompts';
// Helper function to convert attribute definitions to column config
@ -267,9 +266,6 @@ export const promptsPageData: GenericPageData = {
}
],
// Privilege system
privilegeChecker: privilegeCheckers.viewerRole,
// Page behavior
persistent: false,
preload: false,

View file

@ -1,6 +1,5 @@
import { GenericPageData } from '../../pageInterface';
import { FaCog } from 'react-icons/fa';
import { privilegeCheckers } from '../../../../utils/privilegeCheckers';
import { createSettingsHook } from '../../../../hooks/useSettings';
export const settingsPageData: GenericPageData = {
@ -14,9 +13,6 @@ export const settingsPageData: GenericPageData = {
title: 'settings.title',
subtitle: 'Manage your account settings and preferences',
// Privilege system
privilegeChecker: privilegeCheckers.viewerRole,
// Page behavior
persistent: false,
preserveState: false,

View file

@ -1,7 +1,6 @@
import { GenericPageData } from '../../pageInterface';
import { FaDownload, FaTrash, FaSearch } from 'react-icons/fa';
import { IoIosDocument } from 'react-icons/io';
import { privilegeCheckers } from '../../../../utils/privilegeCheckers';
export const speechTranscriptsPageData: GenericPageData = {
id: '8-1',
@ -99,9 +98,6 @@ export const speechTranscriptsPageData: GenericPageData = {
}
],
// Privilege system
privilegeChecker: privilegeCheckers.speechSignup,
// Page behavior
persistent: false,
preload: false,

View file

@ -1,6 +1,5 @@
import { GenericPageData } from '../../pageInterface';
import { FaRegFileAlt, FaMicrophone, FaCog, FaHistory } from 'react-icons/fa';
import { privilegeCheckers } from '../../../../utils/privilegeCheckers';
export const speechPageData: GenericPageData = {
id: 'start-speech',
@ -50,8 +49,7 @@ export const speechPageData: GenericPageData = {
onClick: () => {
console.log('Opening transcript history...');
// Navigate to transcripts
},
privilegeChecker: privilegeCheckers.speechSignup
}
}
],
@ -111,12 +109,8 @@ export const speechPageData: GenericPageData = {
}
],
// Privilege system
privilegeChecker: privilegeCheckers.viewerRole,
// Subpage support
hasSubpages: true,
subpagePrivilegeChecker: privilegeCheckers.speechSignup,
// Page behavior
persistent: false,

View file

@ -1,7 +1,6 @@
import { useCallback } from 'react';
import { GenericPageData } from '../../pageInterface';
import { FaUsers, FaPlus } from 'react-icons/fa';
import { privilegeCheckers } from '../../../../utils/privilegeCheckers';
import { useOrgUsers, useUserOperations } from '../../../../hooks/useUsers';
// Helper function to convert attribute definitions to column config
@ -268,9 +267,6 @@ export const teamMembersPageData: GenericPageData = {
}
],
// Privilege system - only admin and sysadmin can access
privilegeChecker: privilegeCheckers.adminRole,
// Page behavior
persistent: false,
preload: false,

View file

@ -1,7 +1,6 @@
import { useCallback } from 'react';
import { GenericPageData } from '../../pageInterface';
import { FaProjectDiagram } from 'react-icons/fa';
import { privilegeCheckers } from '../../../../utils/privilegeCheckers';
import { useUserWorkflows, useWorkflowOperations } from '../../../../hooks/useWorkflows';
// Helper function to convert attribute definitions to column config
@ -221,9 +220,6 @@ export const workflowsPageData: GenericPageData = {
}
],
// Privilege system
privilegeChecker: privilegeCheckers.viewerRole,
// Page behavior
persistent: false,
preload: false,

View file

@ -49,7 +49,6 @@ export interface PageButton {
icon?: IconType;
onClick?: (hookData?: any) => void | Promise<void>;
disabled?: boolean | ((hookData?: any) => boolean | { disabled: boolean; message?: string });
privilegeChecker?: PrivilegeChecker;
// Form configuration for create buttons
formConfig?: {
fields: ButtonFormField[];
@ -128,7 +127,6 @@ export interface PageContent {
items?: (string | LanguageText)[]; // For lists
language?: string; // For code blocks
customComponent?: React.ComponentType<any>;
privilegeChecker?: PrivilegeChecker;
// Table-specific properties
tableConfig?: TableContentConfig;
// Input form-specific properties
@ -275,9 +273,6 @@ export interface GenericPageData {
// Content sections
content?: PageContent[];
// Privilege system
privilegeChecker?: PrivilegeChecker;
// Page behavior
persistent?: boolean;
preserveState?: boolean;
@ -287,7 +282,6 @@ export interface GenericPageData {
// Subpage support
hasSubpages?: boolean;
subpagePrivilegeChecker?: PrivilegeChecker;
// Lifecycle hooks
onActivate?: () => void | Promise<void>;

View file

@ -1,11 +1,14 @@
import { PrivilegeChecker } from '../core/PageManager/pageInterface';
import { getUserDataCache } from './userCache';
import type { PermissionContext } from '../hooks/usePermissions';
/**
* Privilege Checkers
*
* Read-only access to user data for privilege checking.
* Does not manage user data storage - that's handled by authentication hooks.
*
* Now supports both client-side checks (roles, localStorage) and backend RBAC integration.
*/
// Function to get current user privilege from sessionStorage cache
@ -96,6 +99,123 @@ export const createCustomPrivilegeChecker = (
return checkFunction;
};
/**
* Create a privilege checker that uses backend RBAC permissions
* This integrates privilegeCheckers with usePermissions for backend-controlled access
*
* @param canViewFunction - The canView function from usePermissions hook
* @param context - Permission context ('UI', 'DATA', or 'RESOURCE')
* @param item - The item/resource path to check permissions for
* @returns A PrivilegeChecker function that checks backend RBAC permissions
*/
export const createRBACPrivilegeChecker = (
canViewFunction: (context: PermissionContext, item: string) => Promise<boolean>,
context: PermissionContext,
item: string
): PrivilegeChecker => {
return async (): Promise<boolean> => {
try {
return await canViewFunction(context, item);
} catch (error) {
console.error(`Error checking RBAC privilege for ${context}:${item}:`, error);
return false;
}
};
};
/**
* Create a privilege checker that combines RBAC with client-side role checks
* First checks backend RBAC, then falls back to client-side role check if RBAC allows
*
* @param canViewFunction - The canView function from usePermissions hook
* @param context - Permission context ('UI', 'DATA', or 'RESOURCE')
* @param item - The item/resource path to check permissions for
* @param requiredRoles - Fallback client-side roles to check if RBAC passes
* @returns A PrivilegeChecker function that checks both RBAC and roles
*/
export const createCombinedPrivilegeChecker = (
canViewFunction: (context: PermissionContext, item: string) => Promise<boolean>,
context: PermissionContext,
item: string,
requiredRoles: string[]
): PrivilegeChecker => {
return async (): Promise<boolean> => {
try {
// First check backend RBAC
const hasRBACAccess = await canViewFunction(context, item);
if (!hasRBACAccess) {
return false;
}
// If RBAC allows, also check client-side roles as additional validation
const userPrivilege = getCurrentUserPrivilege();
if (userPrivilege && requiredRoles.includes(userPrivilege)) {
return true;
}
// If no role match, still allow if RBAC said yes (backend is source of truth)
return hasRBACAccess;
} catch (error) {
console.error(`Error checking combined privilege for ${context}:${item}:`, error);
return false;
}
};
};
/**
* Helper to create RBAC-based privilege checkers for page data
* These checkers will use backend RBAC permissions via usePermissions
*
* Usage in page data:
* import { createRBACPageChecker } from '@/utils/privilegeCheckers';
*
* // In PageManager, initialize with canView function:
* const rbacCheckers = createRBACPageCheckers(canView);
*
* // In page data:
* privilegeChecker: rbacCheckers.forPage('administration/workflows')
*/
export const createRBACPageCheckers = (
canViewFunction: (context: PermissionContext, item: string) => Promise<boolean>
) => {
return {
/**
* Create a privilege checker for a specific page path
* Checks backend RBAC permissions for UI context
*/
forPage: (pagePath: string): PrivilegeChecker => {
return createRBACPrivilegeChecker(canViewFunction, 'UI', pagePath);
},
/**
* Create a privilege checker that combines RBAC with role requirements
* First checks backend RBAC, then validates user role
*/
forPageWithRole: (
pagePath: string,
requiredRoles: string[]
): PrivilegeChecker => {
return createCombinedPrivilegeChecker(canViewFunction, 'UI', pagePath, requiredRoles);
},
/**
* Create a privilege checker for a data resource
* Checks backend RBAC permissions for DATA context
*/
forData: (resourcePath: string): PrivilegeChecker => {
return createRBACPrivilegeChecker(canViewFunction, 'DATA', resourcePath);
},
/**
* Create a privilege checker for a UI resource
* Checks backend RBAC permissions for UI context
*/
forUI: (resourcePath: string): PrivilegeChecker => {
return createRBACPrivilegeChecker(canViewFunction, 'UI', resourcePath);
}
};
};
// Predefined privilege checkers for common use cases
export const privilegeCheckers = {
// Speech signup checker (existing functionality)