feat: implement recursive menu item conversion with permission checks

This commit is contained in:
olli 2025-12-28 10:52:53 +01:00
parent b4974b93ef
commit ed2199096d
4 changed files with 131 additions and 37 deletions

View File

@ -79,6 +79,44 @@ const adminMenu = {
// Dynamisches Menü (wird geladen) // Dynamisches Menü (wird geladen)
const model = ref([...coreMenu]); const model = ref([...coreMenu]);
// Hilfsfunktion: Konvertiert ein Menü-Item und alle Sub-Items
const convertMenuItem = (item) => {
// Erstelle Permission-Prüfung
let visibleFn;
if (item.permission) {
// Explizite Permission aus Plugin (z.B. "billing.view")
visibleFn = () => authStore.hasPermission(item.permission);
} else if (item.module) {
// Fallback: Modulname + ".view" (z.B. "billing" -> "billing.view")
visibleFn = () => authStore.hasPermission(`${item.module}.view`);
} else if (item.source) {
// Fallback: Plugin-Identifier + ".view" (z.B. "billing" -> "billing.view")
visibleFn = () => authStore.hasPermission(`${item.source}.view`);
}
const converted = {
label: item.label,
icon: item.icon ? `pi pi-fw ${item.icon}` : 'pi pi-fw pi-circle',
to: item.to,
...(visibleFn && { visible: visibleFn })
};
// Rekursiv Sub-Items verarbeiten
if (item.items && Array.isArray(item.items)) {
converted.items = item.items
.filter(subItem => !subItem.separator) // Separators filtern
.map(subItem => convertMenuItem(subItem));
}
// Separators beibehalten
if (item.separator) {
return { separator: true };
}
return converted;
};
// Plugin-Menüs laden // Plugin-Menüs laden
const loadPluginMenus = async () => { const loadPluginMenus = async () => {
try { try {
@ -91,29 +129,7 @@ const loadPluginMenus = async () => {
// Konvertiere zu PrimeVue Menü-Format // Konvertiere zu PrimeVue Menü-Format
const menuGroup = { const menuGroup = {
label: groupLabel, label: groupLabel,
items: items.map(item => { items: items.map(item => convertMenuItem(item))
// Erstelle Permission-Prüfung
let visibleFn;
if (item.permission) {
// Explizite Permission aus Plugin
visibleFn = () => authStore.hasPermission(item.permission);
} else if (item.module) {
// Fallback: Leite von Modulname ab (z.B. 'billing')
visibleFn = () => permissionStore.canView(item.module);
} else if (item.source) {
// Fallback: Nutze Plugin-Identifier als Modulname
visibleFn = () => permissionStore.canView(item.source);
}
return {
label: item.label,
icon: item.icon ? `pi pi-fw ${item.icon}` : 'pi pi-fw pi-circle',
to: item.to,
...(item.items && { items: item.items }),
...(visibleFn && { visible: visibleFn })
};
})
}; };
model.value.push(menuGroup); model.value.push(menuGroup);

8
composer.lock generated
View File

@ -2052,12 +2052,12 @@
"source": { "source": {
"type": "git", "type": "git",
"url": "https://git.osdata-home.de/mycrm/mycrm-billing-module", "url": "https://git.osdata-home.de/mycrm/mycrm-billing-module",
"reference": "a70043acd597e4ff5064a51e639a5f28227e5cd4" "reference": "8c2544569381333f45ae013b6d0a4ac61d4eb0e0"
}, },
"require": { "require": {
"doctrine/orm": "^3.0", "doctrine/orm": "^3.0",
"php": ">=8.2", "php": ">=8.2",
"symfony/framework-bundle": "^7.1" "symfony/framework-bundle": "^7.3"
}, },
"suggest": { "suggest": {
"api-platform/symfony": "Required for API Platform integration (>=4.0)" "api-platform/symfony": "Required for API Platform integration (>=4.0)"
@ -2066,7 +2066,7 @@
"type": "symfony-bundle", "type": "symfony-bundle",
"extra": { "extra": {
"symfony": { "symfony": {
"require": "7.1.*" "require": "7.3.*"
} }
}, },
"autoload": { "autoload": {
@ -2078,7 +2078,7 @@
"proprietary" "proprietary"
], ],
"description": "Ausgangsrechnungsverwaltung für myCRM", "description": "Ausgangsrechnungsverwaltung für myCRM",
"time": "2025-12-22T08:27:54+00:00" "time": "2025-12-28T09:14:04+00:00"
}, },
{ {
"name": "nelmio/cors-bundle", "name": "nelmio/cors-bundle",

View File

@ -22,21 +22,22 @@ public function getMenuItems(): array
{ {
return [ return [
[ [
'label' => 'Rechnungen', // Menü-Titel 'label' => 'Rechnungen', // Menü-Titel (Container)
'icon' => 'pi-file-pdf', // PrimeIcons Icon (ohne pi pi-fw prefix) 'icon' => 'pi-file-pdf', // PrimeIcons Icon (ohne pi pi-fw prefix)
'group' => 'Finanzen', // Gruppierung im Menü 'group' => 'Finanzen', // Gruppierung im Menü
// WICHTIG: Container-Items brauchen KEINE permission Property
'items' => [ 'items' => [
[ [
'label' => 'Alle Rechnungen', 'label' => 'Alle Rechnungen',
'icon' => 'pi-list', 'icon' => 'pi-list',
'to' => '/billing/invoices', // Vue Router path 'to' => '/billing/invoices', // Vue Router path
'permission' => 'billing.view' // Optional: Permission-Check 'permission' => 'billing.view' // WICHTIG: Jedes Sub-Item braucht permission
], ],
[ [
'label' => 'Neue Rechnung', 'label' => 'Neue Rechnung',
'icon' => 'pi-plus', 'icon' => 'pi-plus',
'to' => '/billing/invoices/create', 'to' => '/billing/invoices/create',
'permission' => 'billing.create' 'permission' => 'billing.create' // Explizite Permission
], ],
[ [
'separator' => true // Optional: Trennlinie 'separator' => true // Optional: Trennlinie
@ -45,7 +46,7 @@ public function getMenuItems(): array
'label' => 'Einstellungen', 'label' => 'Einstellungen',
'icon' => 'pi-cog', 'icon' => 'pi-cog',
'to' => '/billing/settings', 'to' => '/billing/settings',
'permission' => 'billing.manage' 'permission' => 'billing.manage' // Explizite Permission
] ]
] ]
] ]
@ -75,11 +76,14 @@ public function getMenuItems(): array
```php ```php
public function getPermissionModules(): array public function getPermissionModules(): array
{ {
return ['billing', 'invoicing']; // Best Practice: Ein einziges Modul pro Plugin
return ['billing'];
} }
``` ```
Das System erstellt automatisch folgende Permissions: **WICHTIG:** Verwende nur **ein** Permission-Modul pro Plugin, nicht mehrere! Dies vereinfacht die Administration massiv.
Das System erstellt automatisch folgende Permissions für das Modul `billing`:
- `billing.view` - `billing.view`
- `billing.create` - `billing.create`
- `billing.edit` - `billing.edit`
@ -276,7 +280,8 @@ class BillingModulePlugin implements ModulePluginInterface
public function getPermissionModules(): array public function getPermissionModules(): array
{ {
return ['billing', 'invoicing', 'payments']; // Best Practice: Ein einziges Modul für das gesamte Plugin
return ['billing'];
} }
public function getMenuItems(): array public function getMenuItems(): array
@ -286,12 +291,13 @@ class BillingModulePlugin implements ModulePluginInterface
'label' => 'Rechnungen', 'label' => 'Rechnungen',
'icon' => 'pi-file-pdf', 'icon' => 'pi-file-pdf',
'group' => 'Finanzen', 'group' => 'Finanzen',
// Wichtig: Container-Item hat KEINE permission Property
'items' => [ 'items' => [
[ [
'label' => 'Dashboard', 'label' => 'Dashboard',
'icon' => 'pi-chart-line', 'icon' => 'pi-chart-line',
'to' => '/billing/dashboard', 'to' => '/billing/dashboard',
'permission' => 'billing.view' 'permission' => 'billing.view' // Explizite Permission
], ],
[ [
'label' => 'Alle Rechnungen', 'label' => 'Alle Rechnungen',
@ -312,7 +318,7 @@ class BillingModulePlugin implements ModulePluginInterface
'label' => 'Zahlungen', 'label' => 'Zahlungen',
'icon' => 'pi-money-bill', 'icon' => 'pi-money-bill',
'to' => '/billing/payments', 'to' => '/billing/payments',
'permission' => 'payments.view' 'permission' => 'billing.view' // Einheitliches Modul!
], ],
[ [
'label' => 'Einstellungen', 'label' => 'Einstellungen',
@ -438,6 +444,78 @@ tail -f var/log/dev.log | grep -E "(Plugin|Menu|Permission)"
- **Keine Core-Gruppen überschreiben** - "Home", "CRM", "Administration" sind reserviert - **Keine Core-Gruppen überschreiben** - "Home", "CRM", "Administration" sind reserviert
- **Keine tiefen Verschachtelungen** - Max. 2 Ebenen (Gruppe → Items) - **Keine tiefen Verschachtelungen** - Max. 2 Ebenen (Gruppe → Items)
- **Keine externen URLs ohne Warnung** - User erwarten interne Navigation - **Keine externen URLs ohne Warnung** - User erwarten interne Navigation
- **Nicht mehrere Permission-Module für ein Plugin** - Erschwert die Administration unnötig
### 🎯 WICHTIG: Permission-Format und Sub-Items (Stand: 2025-12-28)
**Ein Permission-Modul pro Plugin:**
```php
// ✅ RICHTIG: Ein einziges Modul
public function getPermissionModules(): array
{
return ['billing'];
}
// ❌ FALSCH: Unnötig aufgeteilt
public function getPermissionModules(): array
{
return ['billing', 'invoices', 'payments'];
}
```
**Explizite Permissions für ALLE Sub-Items:**
```php
// ✅ RICHTIG: Jedes anklickbare Item hat permission
public function getMenuItems(): array
{
return [
[
'label' => 'Rechnungen', // Container - KEINE Permission nötig
'icon' => 'pi-file-pdf',
'group' => 'Finanzen',
'items' => [
[
'label' => 'Alle Rechnungen',
'to' => '/billing/invoices',
'permission' => 'billing.view' // ✅ Explizite Permission
],
[
'label' => 'Neue Rechnung',
'to' => '/billing/invoices/create',
'permission' => 'billing.create' // ✅ Explizite Permission
]
]
]
];
}
// ❌ FALSCH: Sub-Items ohne Permission
public function getMenuItems(): array
{
return [
[
'label' => 'Rechnungen',
'permission' => 'billing.view', // ❌ Container braucht keine Permission
'items' => [
[
'label' => 'Alle Rechnungen',
'to' => '/billing/invoices'
// ❌ FEHLT: permission Property!
]
]
]
];
}
```
**Warum ist das wichtig?**
Das Frontend (`AppMenu.vue`) verwendet eine **rekursive `convertMenuItem()` Funktion**, die:
1. Alle Sub-Items durchläuft und deren `permission` Properties in `visible` Funktionen umwandelt
2. Items mit `source` Property automatisch zu `${source}.view` Permission konvertiert
3. Nur Items mit erfüllter Permission anzeigt
**Container-Items** (mit `items` Array) brauchen **KEINE eigene Permission**, da sie nur dann angezeigt werden, wenn mindestens ein Sub-Item sichtbar ist.
--- ---

View File

@ -74,7 +74,7 @@
] ]
}, },
"mycrm/billing-module": { "mycrm/billing-module": {
"version": "v1.0.0" "version": "dev-main"
}, },
"nelmio/cors-bundle": { "nelmio/cors-bundle": {
"version": "2.6", "version": "2.6",