581 lines
26 KiB
Markdown
581 lines
26 KiB
Markdown
# 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)
|
|
|
|
```typescript
|
|
// 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:**
|
|
```typescript
|
|
// Line 243
|
|
privilegeChecker: privilegeCheckers.viewerRole
|
|
```
|
|
|
|
**Privilege Checker Implementation:**
|
|
```typescript
|
|
// privilegeCheckers.ts Lines 199-208
|
|
viewerRole: createRolePrivilegeChecker(
|
|
['viewer', 'user', 'admin', 'sysadmin'],
|
|
() => {
|
|
const userPrivilege = getCurrentUserPrivilege(); // Reads from localStorage
|
|
return Promise.resolve(userPrivilege ? [userPrivilege] : []);
|
|
}
|
|
)
|
|
```
|
|
|
|
**Process:**
|
|
1. Read `localStorage.getItem('currentUser')`
|
|
2. Parse JSON and extract `user.privilege`
|
|
3. Check if privilege is in allowed list: `['viewer', 'user', 'admin', 'sysadmin']`
|
|
4. Return `true` if match, `false` otherwise
|
|
|
|
#### 2. **Button Level Check** (`PageRenderer.tsx` Line 40)
|
|
|
|
```typescript
|
|
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:**
|
|
```typescript
|
|
{
|
|
id: 'delete-all',
|
|
label: 'Delete All',
|
|
onClick: () => { /* ... */ },
|
|
privilegeChecker: privilegeCheckers.adminRole // Only admins
|
|
}
|
|
```
|
|
|
|
#### 3. **Content Level Check** (`PageRenderer.tsx` Line 245)
|
|
|
|
```typescript
|
|
{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)
|
|
|
|
```typescript
|
|
// PageRenderer receives: language = 'de' (from LanguageContext)
|
|
|
|
<h1>{resolveLanguageText(pageData.title, language)}</h1>
|
|
<p>{resolveLanguageText(pageData.subtitle, language)}</p>
|
|
```
|
|
|
|
**Input (dateien.ts):**
|
|
```typescript
|
|
title: {
|
|
de: 'Dateien',
|
|
en: 'Files',
|
|
fr: 'Fichiers'
|
|
}
|
|
```
|
|
|
|
**Process:**
|
|
```typescript
|
|
// 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)
|
|
|
|
```typescript
|
|
{button.icon && <button.icon />}
|
|
{resolveLanguageText(button.label, language)}
|
|
```
|
|
|
|
**Input (dateien.ts):**
|
|
```typescript
|
|
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:**
|
|
```typescript
|
|
// 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):**
|
|
```typescript
|
|
{
|
|
key: 'file_name',
|
|
label: {
|
|
de: 'Dateiname',
|
|
en: 'Filename',
|
|
fr: 'Nom de fichier'
|
|
},
|
|
// ...
|
|
}
|
|
```
|
|
|
|
**What happens in FormGenerator:**
|
|
```typescript
|
|
// FormGenerator.tsx Line 627
|
|
<label>{column.label}</label>
|
|
// Displays: [object Object] ❌
|
|
```
|
|
|
|
**Expected:**
|
|
```typescript
|
|
// Should be resolved BEFORE passing to FormGenerator
|
|
const resolvedColumns = columns.map(col => ({
|
|
...col,
|
|
label: resolveLanguageText(col.label, language)
|
|
}));
|
|
```
|
|
|
|
#### ~~2. **Action Button Titles**~~ FIXED ✅
|
|
|
|
**Problem:**
|
|
```typescript
|
|
// 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):**
|
|
```typescript
|
|
{
|
|
type: 'view',
|
|
title: {
|
|
de: 'Datei vorschauen',
|
|
en: 'Preview file',
|
|
fr: 'Aperçu du fichier'
|
|
},
|
|
// ...
|
|
}
|
|
```
|
|
|
|
**What happens:**
|
|
- FormGenerator passes raw `title` to action button components
|
|
- Action buttons expect `title?: string` but receive `LanguageText` object
|
|
- Tooltip/aria-label shows `[object Object]` ❌
|
|
|
|
**Expected:**
|
|
```typescript
|
|
const formGeneratorActions = actionButtons?.map(action => {
|
|
return {
|
|
type: action.type,
|
|
title: resolveLanguageText(action.title, language), // ✅ Resolve here!
|
|
// ...
|
|
};
|
|
});
|
|
```
|
|
|
|
#### 3. **Filter Placeholders** (`FormGenerator.tsx` Line 642)
|
|
|
|
```typescript
|
|
<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:**
|
|
```typescript
|
|
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:**
|
|
```typescript
|
|
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**
|
|
|
|
```typescript
|
|
// ❌ 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
|
|
|
|
1. ~~**Fix PageRenderer** to resolve column labels and action titles~~ ✅ DONE
|
|
2. **Add type checks** to ensure LanguageText resolution (optional enhancement)
|
|
3. **Update FormGenerator types** to strictly expect `string` for labels (optional enhancement)
|
|
4. **Add console warnings** when LanguageText objects are not resolved (optional enhancement)
|
|
5. **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
|
|
|