26 KiB
Privilege and Language Flow - Complete Trace (dateien.ts Example)
📋 Overview
This document traces the complete flow of privilege checking and language resolution from PageManager through to rendered content, using dateien.ts as a concrete example.
🔄 Complete Flow Diagram
┌─────────────────────────────────────────────────────────────┐
│ 1. USER NAVIGATES TO /verwaltung/dateien │
└────────────────────┬────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 2. PageManager.tsx - useEffect triggered │
│ Line 67: const pageData = getPageDataByPath(currentPath)│
└────────────────────┬────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 3. data/pages/index.ts - getPageDataByPath() │
│ Line 27-29: Find page by path │
│ Returns: dateienPageData object │
└────────────────────┬────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 4. PageManager.tsx - Check if module enabled │
│ Line 70: if (!pageData.moduleEnabled) return │
│ dateien.ts Line 248: moduleEnabled: true ✅ │
└────────────────────┬────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 5. PageManager.tsx - Check Page Privilege │
│ Line 75: checkPageAccess(pageData) │
│ ↓ │
│ Line 29-40: async checkPageAccess() │
│ if (!pageData.privilegeChecker) return true │
│ else return await pageData.privilegeChecker() │
│ ↓ │
│ dateien.ts Line 243: privilegeChecker: privilegeCheckers.viewerRole │
│ ↓ │
│ privilegeCheckers.ts Line 199-208: │
│ createRolePrivilegeChecker(['viewer', 'user', 'admin', 'sysadmin']) │
│ Reads from localStorage('currentUser').privilege │
│ Returns true if user privilege matches ✅ │
└────────────────────┬────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 6. PageManager.tsx - Get Current Language │
│ Line 20: const { currentLanguage } = useLanguage() │
│ ↓ │
│ LanguageContext reads from: │
│ localStorage('currentUser').language │
│ Current language: 'de' | 'en' | 'fr' │
└────────────────────┬────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 7. PageManager.tsx - Create Page Instance │
│ Line 93-116: Create PageInstance │
│ Line 101-108: Render PageRenderer with: │
│ - pageData (full dateienPageData object) │
│ - language={currentLanguage} ✅ │
└────────────────────┬────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 8. PageRenderer.tsx - Receive Props │
│ Line 13-17: PageRendererProps │
│ - pageData: GenericPageData │
│ - language: 'de' | 'en' | 'fr' ✅ │
└────────────────────┬────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 9. PageRenderer.tsx - Initialize Hook Factory │
│ Line 20-34: Execute hook factory │
│ ↓ │
│ dateien.ts Line 8-62: createFilesHook() │
│ Returns hook function that calls: │
│ - useUserFiles() → fetches files data │
│ - useFileOperations() → handles file operations │
│ Returns: hookData with data, operations, states │
└────────────────────┬────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 10. PageRenderer.tsx - Render Page Header │
│ Line 190-191: Render title │
│ resolveLanguageText(pageData.title, language) │
│ ↓ │
│ dateien.ts Line 141-145: title object │
│ { de: 'Dateien', en: 'Files', fr: 'Fichiers' } │
│ ↓ │
│ pageInterface.ts Line 87-91: resolveLanguageText() │
│ Returns: text[language] → 'Dateien' ✅ │
│ ↓ │
│ Line 192-193: Render subtitle (same process) │
│ Result: 'Verwalten Sie Ihre Dateien...' ✅ │
└────────────────────┬────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 11. PageRenderer.tsx - Render Header Buttons │
│ Line 198-234: Loop through headerButtons │
│ ↓ │
│ dateien.ts Line 153-165: Upload button config │
│ label: { de: 'Datei hochladen', ... } │
│ ↓ │
│ Line 230: resolveLanguageText(button.label, language) │
│ Result: 'Datei hochladen' ✅ │
└────────────────────┬────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 12. PageRenderer.tsx - Render Table Content │
│ Line 115-177: Render table type content │
│ ↓ │
│ dateien.ts Line 169-239: Table configuration │
│ - hookFactory: createFilesHook │
│ - columns: filesColumns (Line 65-124) │
│ Each column has: │
│ label: { de: '...', en: '...', fr: '...' } │
│ - actionButtons: [view, edit, download, delete] │
│ Each button has: │
│ title: { de: '...', en: '...', fr: '...' } │
│ ↓ │
│ Line 140: const columns = hookData.columns || configColumns │
│ columns = filesColumns (LanguageText objects!) │
│ ↓ │
│ Line 142-146: Resolve column labels ✅ │
│ resolvedColumns with label: string │
│ ↓ │
│ Line 150-165: Map action buttons │
│ title: resolveLanguageText(action.title, language) ✅│
│ ↓ │
│ Line 174-181: Pass to FormGenerator │
│ columns={resolvedColumns} ← RESOLVED strings! ✅ │
│ actionButtons={formGeneratorActions} ← RESOLVED! ✅ │
└────────────────────┬────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 13. FormGenerator.tsx - Receive Props │
│ Line 81-104: FormGeneratorProps │
│ columns: ColumnConfig[] with label: string │
│ NOW receiving: label: string (resolved!) ✅ │
│ ↓ │
│ Line 105: const { t } = useLanguage() │
│ Has access to t() and currentLanguage ✅ │
│ ↓ │
│ Line 627, 642: Uses column.label directly │
│ Displays: 'Dateiname' (correct text!) ✅ │
└────────────────────┬────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 14. Action Buttons Rendering │
│ Line 766-785: Map through actionButtons │
│ ↓ │
│ Line 767-769: Get title │
│ actionTitle = actionButton.title (string!) ✅ │
│ ↓ │
│ Passed to EditActionButton, DeleteActionButton, etc. │
│ ↓ │
│ EditActionButton.tsx Line 39: title prop (string) │
│ Receives correct string! ✅ │
└─────────────────────────────────────────────────────────────┘
🎯 Privilege Checking - Detailed
✅ Where Privilege Checks Happen
1. Page Level Check (PageManager.tsx Line 75)
// PageManager.tsx
const checkPageAccess = async (pageData: GenericPageData): Promise<boolean> => {
if (!pageData.privilegeChecker) {
return true; // No checker = accessible to all
}
try {
return await pageData.privilegeChecker();
} catch (error) {
console.error(`Error checking page access for ${pageData.path}:`, error);
return false;
}
};
For dateien.ts:
// Line 243
privilegeChecker: privilegeCheckers.viewerRole
Privilege Checker Implementation:
// privilegeCheckers.ts Lines 199-208
viewerRole: createRolePrivilegeChecker(
['viewer', 'user', 'admin', 'sysadmin'],
() => {
const userPrivilege = getCurrentUserPrivilege(); // Reads from localStorage
return Promise.resolve(userPrivilege ? [userPrivilege] : []);
}
)
Process:
- Read
localStorage.getItem('currentUser') - Parse JSON and extract
user.privilege - Check if privilege is in allowed list:
['viewer', 'user', 'admin', 'sysadmin'] - Return
trueif match,falseotherwise
2. Button Level Check (PageRenderer.tsx Line 40)
const handleButtonClick = async (button: PageButton) => {
try {
// Check privilege if required
if (button.privilegeChecker) {
const hasPrivilege = await button.privilegeChecker();
if (!hasPrivilege) {
console.warn(`Access denied for button: ${button.id}`);
return;
}
}
// Execute onClick...
}
};
Example from example-page.ts:
{
id: 'delete-all',
label: 'Delete All',
onClick: () => { /* ... */ },
privilegeChecker: privilegeCheckers.adminRole // Only admins
}
3. Content Level Check (PageRenderer.tsx Line 245)
{pageData.content?.map((content) => {
// Check privilege for content
if (content.privilegeChecker) {
// Content is rendered only if privilege check passes
return renderContent(content);
}
return renderContent(content);
})}
✅ Timing of Privilege Checks
User navigates → PageManager useEffect triggers
↓
getPageDataByPath(currentPath) - fetches page config
↓
checkPageAccess(pageData) - ASYNC check
↓
If hasAccess = false → Return early (no render)
If hasAccess = true → Create PageInstance → Render PageRenderer
↓
Button clicks → Check button.privilegeChecker before executing
Key Point: Privilege checks are asynchronous and happen before page rendering.
🌍 Language Resolution - Detailed
✅ Where Language IS Resolved Correctly
1. Page Title and Subtitle (PageRenderer.tsx Lines 191-193)
// PageRenderer receives: language = 'de' (from LanguageContext)
<h1>{resolveLanguageText(pageData.title, language)}</h1>
<p>{resolveLanguageText(pageData.subtitle, language)}</p>
Input (dateien.ts):
title: {
de: 'Dateien',
en: 'Files',
fr: 'Fichiers'
}
Process:
// pageInterface.ts Line 87-91
export const resolveLanguageText = (text: string | LanguageText, language: 'de') => {
if (typeof text === 'string') return text;
return text[language] || text.de || '';
};
Result: 'Dateien' ✅
2. Header Button Labels (PageRenderer.tsx Line 230)
{button.icon && <button.icon />}
{resolveLanguageText(button.label, language)}
Input (dateien.ts):
label: {
de: 'Datei hochladen',
en: 'Upload File',
fr: 'Télécharger un fichier'
}
Result: 'Datei hochladen' ✅
3. Simple Content Types (heading, paragraph, list)
All simple content types properly use resolveLanguageText(content.content, language) ✅
❌ Where Language WAS NOT Resolved NOW FIXED ✅
1. Table Column Labels FIXED ✅
Problem:
// PageRenderer.tsx Line 140
const columns = hookData.columns || configColumns;
// Line 169 - Passed directly to FormGenerator
<FormGenerator
columns={columns} // ← LanguageText objects NOT resolved! ❌
...
/>
Input (dateien.ts Lines 68-72):
{
key: 'file_name',
label: {
de: 'Dateiname',
en: 'Filename',
fr: 'Nom de fichier'
},
// ...
}
What happens in FormGenerator:
// FormGenerator.tsx Line 627
<label>{column.label}</label>
// Displays: [object Object] ❌
Expected:
// Should be resolved BEFORE passing to FormGenerator
const resolvedColumns = columns.map(col => ({
...col,
label: resolveLanguageText(col.label, language)
}));
2. Action Button Titles FIXED ✅
Problem:
// PageRenderer.tsx Line 144-158
const formGeneratorActions = actionButtons?.map(action => {
return {
type: action.type,
title: action.title, // ← LanguageText object NOT resolved! ❌
// ...
};
});
Input (dateien.ts Lines 179-183):
{
type: 'view',
title: {
de: 'Datei vorschauen',
en: 'Preview file',
fr: 'Aperçu du fichier'
},
// ...
}
What happens:
- FormGenerator passes raw
titleto action button components - Action buttons expect
title?: stringbut receiveLanguageTextobject - Tooltip/aria-label shows
[object Object]❌
Expected:
const formGeneratorActions = actionButtons?.map(action => {
return {
type: action.type,
title: resolveLanguageText(action.title, language), // ✅ Resolve here!
// ...
};
});
3. Filter Placeholders (FormGenerator.tsx Line 642)
<label>
{t('formgen.filter.placeholder').replace('{column}', column.label)}
</label>
If column.label is a LanguageText object, this breaks! ❌
✅ Issues Fixed
Issue #1: Column Labels Not Resolved FIXED ✅
Location: PageRenderer.tsx Line 142-146
Fixed Code:
const columns = hookData.columns || configColumns;
// CRITICAL: Resolve LanguageText objects in column labels
const resolvedColumns = columns.map(col => ({
...col,
label: resolveLanguageText(col.label, language)
}));
<FormGenerator
columns={resolvedColumns} // ✅ Resolved strings
...
/>
Issue #2: Action Button Titles Not Resolved FIXED ✅
Location: PageRenderer.tsx Line 150-165
Fixed Code:
const formGeneratorActions = actionButtons?.map(action => {
return {
type: action.type,
// CRITICAL: Resolve LanguageText objects in action titles
title: resolveLanguageText(action.title, language), // ✅ Resolved string
isProcessing: action.loading || (() => false),
disabled: action.disabled || (() => false),
// ...
};
});
Result: All LanguageText objects are now properly resolved to strings before being passed to FormGenerator! 🎉
📊 Data Flow Summary
┌────────────────────────────────────────────────────────────┐
│ dateien.ts Configuration │
│ - Page metadata (title, subtitle) → LanguageText │
│ - Header buttons (labels) → LanguageText │
│ - Table columns (labels) → LanguageText ⚠️ │
│ - Action buttons (titles) → LanguageText ⚠️ │
│ - Privilege checker → viewerRole │
└────────────────────┬───────────────────────────────────────┘
↓
┌────────────────────────────────────────────────────────────┐
│ PageManager.tsx │
│ - Fetches page config │
│ - Checks privilege (async) ✅ │
│ - Gets current language from context ✅ │
│ - Passes both to PageRenderer │
└────────────────────┬───────────────────────────────────────┘
↓
┌────────────────────────────────────────────────────────────┐
│ PageRenderer.tsx │
│ - Resolves: title, subtitle, button labels ✅ │
│ - Does NOT resolve: column labels, action titles ❌ │
│ - Passes unresolved objects to FormGenerator │
└────────────────────┬───────────────────────────────────────┘
↓
┌────────────────────────────────────────────────────────────┐
│ FormGenerator.tsx │
│ - Receives columns with LanguageText objects ❌ │
│ - Displays [object Object] for labels │
│ - Has access to useLanguage() but doesn't use it │
└────────────────────────────────────────────────────────────┘
✅ Best Practices
1. Privilege Checks
- ✅ Always check at page level (
pageData.privilegeChecker) - ✅ Check at button level for sensitive actions
- ✅ Checks are async - handled properly
- ✅ Reads from
localStorage('currentUser').privilege
2. Language Resolution
- ✅ Get language from
useLanguage()context - ✅ Resolve ALL LanguageText objects before passing to child components
- ✅ Use
resolveLanguageText()utility function - ❌ DON'T pass raw LanguageText objects to generic components
3. Type Safety
// ❌ Bad - allows LanguageText to leak through
interface ActionButton {
title?: string | LanguageText; // Ambiguous!
}
// ✅ Good - clearly separate config from resolved
interface ActionButtonConfig {
title: string | LanguageText; // Input config
}
interface ActionButtonProps {
title?: string; // Resolved output
}
✅ Completed
Fix PageRenderer to resolve column labels and action titles✅ DONE- Add type checks to ensure LanguageText resolution (optional enhancement)
- Update FormGenerator types to strictly expect
stringfor labels (optional enhancement) - Add console warnings when LanguageText objects are not resolved (optional enhancement)
- Test with all three languages (de, en, fr) - Ready for testing!
📁 Key Files
| File | Role | Line References |
|---|---|---|
src/core/PageManager/data/pages/dateien.ts |
Page configuration | 65-124 (columns), 176-230 (actions), 243 (privilege) |
src/core/PageManager/PageManager.tsx |
Page routing & privilege check | 67-78 (fetch & check), 20 (language), 103 (pass to renderer) |
src/core/PageManager/PageRenderer.tsx |
Page rendering | 140 (columns), 144-158 (actions), 191-230 (header) |
src/components/FormGenerator/FormGenerator.tsx |
Table rendering | 105 (useLanguage), 627, 642 (display labels) |
src/utils/privilegeCheckers.ts |
Privilege checking | 4-21 (getCurrentUserPrivilege), 199-208 (viewerRole) |
src/contexts/LanguageContext.tsx |
Language state | 46-57 (get from currentUser) |
🎯 Conclusion
Privilege checking works perfectly: ✅
- Checks happen at the right time (before rendering)
- Uses cached user data from localStorage
- Async handling is correct
- Multiple levels of checks (page, button, content)
Language resolution now works completely: ✅
- ✅ Page headers, buttons, simple content
- ✅ Table columns labels (FIXED!)
- ✅ Action button titles (FIXED!)
- All LanguageText objects are resolved before passing to FormGenerator