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)
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
const loadPluginMenus = async () => {
try {
@ -91,29 +129,7 @@ const loadPluginMenus = async () => {
// Konvertiere zu PrimeVue Menü-Format
const menuGroup = {
label: groupLabel,
items: items.map(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 })
};
})
items: items.map(item => convertMenuItem(item))
};
model.value.push(menuGroup);

8
composer.lock generated
View File

@ -2052,12 +2052,12 @@
"source": {
"type": "git",
"url": "https://git.osdata-home.de/mycrm/mycrm-billing-module",
"reference": "a70043acd597e4ff5064a51e639a5f28227e5cd4"
"reference": "8c2544569381333f45ae013b6d0a4ac61d4eb0e0"
},
"require": {
"doctrine/orm": "^3.0",
"php": ">=8.2",
"symfony/framework-bundle": "^7.1"
"symfony/framework-bundle": "^7.3"
},
"suggest": {
"api-platform/symfony": "Required for API Platform integration (>=4.0)"
@ -2066,7 +2066,7 @@
"type": "symfony-bundle",
"extra": {
"symfony": {
"require": "7.1.*"
"require": "7.3.*"
}
},
"autoload": {
@ -2078,7 +2078,7 @@
"proprietary"
],
"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",

View File

@ -22,21 +22,22 @@ public function getMenuItems(): array
{
return [
[
'label' => 'Rechnungen', // Menü-Titel
'label' => 'Rechnungen', // Menü-Titel (Container)
'icon' => 'pi-file-pdf', // PrimeIcons Icon (ohne pi pi-fw prefix)
'group' => 'Finanzen', // Gruppierung im Menü
// WICHTIG: Container-Items brauchen KEINE permission Property
'items' => [
[
'label' => 'Alle Rechnungen',
'icon' => 'pi-list',
'to' => '/billing/invoices', // Vue Router path
'permission' => 'billing.view' // Optional: Permission-Check
'permission' => 'billing.view' // WICHTIG: Jedes Sub-Item braucht permission
],
[
'label' => 'Neue Rechnung',
'icon' => 'pi-plus',
'to' => '/billing/invoices/create',
'permission' => 'billing.create'
'permission' => 'billing.create' // Explizite Permission
],
[
'separator' => true // Optional: Trennlinie
@ -45,7 +46,7 @@ public function getMenuItems(): array
'label' => 'Einstellungen',
'icon' => 'pi-cog',
'to' => '/billing/settings',
'permission' => 'billing.manage'
'permission' => 'billing.manage' // Explizite Permission
]
]
]
@ -75,11 +76,14 @@ public function getMenuItems(): array
```php
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.create`
- `billing.edit`
@ -276,7 +280,8 @@ class BillingModulePlugin implements ModulePluginInterface
public function getPermissionModules(): array
{
return ['billing', 'invoicing', 'payments'];
// Best Practice: Ein einziges Modul für das gesamte Plugin
return ['billing'];
}
public function getMenuItems(): array
@ -286,12 +291,13 @@ class BillingModulePlugin implements ModulePluginInterface
'label' => 'Rechnungen',
'icon' => 'pi-file-pdf',
'group' => 'Finanzen',
// Wichtig: Container-Item hat KEINE permission Property
'items' => [
[
'label' => 'Dashboard',
'icon' => 'pi-chart-line',
'to' => '/billing/dashboard',
'permission' => 'billing.view'
'permission' => 'billing.view' // Explizite Permission
],
[
'label' => 'Alle Rechnungen',
@ -312,7 +318,7 @@ class BillingModulePlugin implements ModulePluginInterface
'label' => 'Zahlungen',
'icon' => 'pi-money-bill',
'to' => '/billing/payments',
'permission' => 'payments.view'
'permission' => 'billing.view' // Einheitliches Modul!
],
[
'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 tiefen Verschachtelungen** - Max. 2 Ebenen (Gruppe → Items)
- **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": {
"version": "v1.0.0"
"version": "dev-main"
},
"nelmio/cors-bundle": {
"version": "2.6",