frontend_nyla/docs/PRIVILEGE_AND_LANGUAGE_FLOW_DETAILED.md

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:

  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)

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 title to action button components
  • Action buttons expect title?: string but receive LanguageText object
  • 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

  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