diff --git a/docs/DEV.md b/docs/DEV.md index 0f5f777..0e0a1f7 100644 --- a/docs/DEV.md +++ b/docs/DEV.md @@ -88,7 +88,7 @@ registerWidgetFetcher('orders', async () => ({ total: await countOrders() })); registerWidget({ id: 'orders', Component: OrdersWidget, order: 20 }); registerNavSection({ id: 'commerce', title: 'Commerce', icon: 'ShoppingBag03Icon', order: 30 }); -registerNavItem({ id: 'orders', label: 'Commandes', icon: 'ShoppingBag03Icon', href: '/admin/orders', sectionId: 'commerce' }); +registerNavItem({ id: 'orders', label: 'Commandes', icon: 'ShoppingBag03Icon', href: '/admin/orders', sectionId: 'commerce', permission: 'orders.view' }); registerPage({ slug: 'orders', Component: OrdersPage, title: 'Commandes' }); ``` diff --git a/src/features/admin/AdminLayout.server.js b/src/features/admin/AdminLayout.server.js index e544160..b566570 100644 --- a/src/features/admin/AdminLayout.server.js +++ b/src/features/admin/AdminLayout.server.js @@ -3,12 +3,14 @@ import { protectAdmin } from './protect.js'; import { buildNavigationSections, buildBottomNavItems } from './navigation.js'; import { logoutAction } from '@zen/core/features/auth/actions'; import { getAppName } from '@zen/core'; +import { getUserPermissions } from '@zen/core/users'; import './widgets/index.server.js'; export default async function AdminLayout({ children }) { const session = await protectAdmin(); const appName = getAppName(); - const navigationSections = buildNavigationSections('/'); + const permissions = await getUserPermissions(session.user.id); + const navigationSections = buildNavigationSections('/', permissions); const bottomNavItems = buildBottomNavItems('/'); return ( diff --git a/src/features/admin/README.md b/src/features/admin/README.md index cc83c95..778642c 100644 --- a/src/features/admin/README.md +++ b/src/features/admin/README.md @@ -102,12 +102,13 @@ if (!admin) return null; --- -### `buildNavigationSections(pathname)` +### `buildNavigationSections(pathname, userPermissions?)` -Construit les sections de navigation pour la sidebar à partir du registre. Marque l'entrée active selon `pathname`. +Construit les sections de navigation pour la sidebar à partir du registre. Marque l'entrée active selon `pathname`. Les items dont le champ `permission` n'est pas présent dans `userPermissions` sont automatiquement exclus ; si tous les items d'une section sont exclus, la section disparaît également. ```js -const sections = buildNavigationSections('/admin/users'); +const permissions = await getUserPermissions(session.user.id); +const sections = buildNavigationSections('/admin/users', permissions); // [{ id, title, icon, items: [{ name, href, icon, current }] }] ``` @@ -183,6 +184,7 @@ registerNavItem({ href: '/admin/orders', sectionId: 'commerce', order: 10, + permission: 'orders.view', // optionnel — masqué si l'utilisateur n'a pas cette permission }); ``` @@ -195,7 +197,7 @@ registerNavItem({ | `icon` | `string` | Nom d'icône Hugeicons | | `order` | `number` | Ordre d'affichage (défaut : `0`) | -**`registerNavItem({ id, label, icon, href, sectionId?, order?, position? })`** +**`registerNavItem({ id, label, icon, href, sectionId?, order?, position?, permission? })`** | Paramètre | Type | Description | |-----------|------|-------------| @@ -206,6 +208,7 @@ registerNavItem({ | `sectionId` | `string` | Section parente (défaut : `'main'`) | | `order` | `number` | Ordre d'affichage (défaut : `0`) | | `position` | `string` | `'bottom'` pour épingler en bas de la sidebar | +| `permission` | `string` | Clé de permission requise pour voir cette entrée (ex. `'orders.view'`). L'entrée est masquée si l'utilisateur ne possède pas cette permission. | --- @@ -247,7 +250,7 @@ import { registerNavSection, registerNavItem, registerPage } from '@zen/core/fea import OrdersPage from './admin/orders/OrdersPage.js'; registerNavSection({ id: 'commerce', title: 'Commerce', icon: 'ShoppingBag03Icon', order: 30 }); -registerNavItem({ id: 'orders', label: 'Commandes', icon: 'ShoppingBag03Icon', href: '/admin/orders', sectionId: 'commerce' }); +registerNavItem({ id: 'orders', label: 'Commandes', icon: 'ShoppingBag03Icon', href: '/admin/orders', sectionId: 'commerce', permission: 'orders.view' }); registerPage({ slug: 'orders', Component: OrdersPage, title: 'Commandes' }); ``` diff --git a/src/features/admin/navigation.js b/src/features/admin/navigation.js index 5a97eb3..784b20e 100644 --- a/src/features/admin/navigation.js +++ b/src/features/admin/navigation.js @@ -5,14 +5,15 @@ import { getNavItems, } from './registry.js'; import { isDevkitEnabled } from '../../shared/lib/appConfig.js'; +import { PERMISSIONS } from '@zen/core/users'; // Sections et items core — enregistrés à l'import de ce module. registerNavSection({ id: 'dashboard', title: 'Tableau de bord', icon: 'DashboardSquare03Icon', order: 10 }); registerNavSection({ id: 'system', title: 'Utilisateurs', icon: 'UserMultiple02Icon', order: 20 }); registerNavItem({ id: 'dashboard', label: 'Tableau de bord', icon: 'DashboardSquare03Icon', href: '/admin/dashboard', sectionId: 'dashboard', order: 10 }); -registerNavItem({ id: 'users', label: 'Utilisateurs', icon: 'UserMultiple02Icon', href: '/admin/users', sectionId: 'system', order: 10 }); -registerNavItem({ id: 'roles', label: 'Rôles', icon: 'Crown03Icon', href: '/admin/roles', sectionId: 'system', order: 20 }); +registerNavItem({ id: 'users', label: 'Utilisateurs', icon: 'UserMultiple02Icon', href: '/admin/users', sectionId: 'system', order: 10, permission: PERMISSIONS.USERS_VIEW }); +registerNavItem({ id: 'roles', label: 'Rôles', icon: 'Crown03Icon', href: '/admin/roles', sectionId: 'system', order: 20, permission: PERMISSIONS.ROLES_VIEW }); registerNavItem({ id: 'settings', label: 'Paramètres', icon: 'Settings02Icon', href: '/admin/settings', position: 'bottom', order: 10 }); if (isDevkitEnabled()) { @@ -24,10 +25,17 @@ if (isDevkitEnabled()) { /** * Build sections for AdminSidebar. Items are sérialisables (pas de composants), * icônes en chaînes résolues côté client. + * @param {string} pathname + * @param {string[]} [userPermissions] - Permissions de l'utilisateur connecté ; les items + * avec un champ `permission` sont masqués si la permission n'est pas présente. */ -export function buildNavigationSections(pathname) { +export function buildNavigationSections(pathname, userPermissions = []) { const sections = getNavSections(); - const items = getNavItems().filter(item => item.position !== 'bottom'); + const items = getNavItems().filter(item => { + if (item.position === 'bottom') return false; + if (item.permission && !userPermissions.includes(item.permission)) return false; + return true; + }); const bySection = new Map(); for (const item of items) { diff --git a/src/features/admin/registry.js b/src/features/admin/registry.js index 035fdb0..5e6e784 100644 --- a/src/features/admin/registry.js +++ b/src/features/admin/registry.js @@ -57,8 +57,8 @@ export function registerNavSection({ id, title, icon, order = 0 }) { navSections.set(id, { id, title, icon, order }); } -export function registerNavItem({ id, label, icon, href, order = 0, sectionId = 'main', position }) { - navItems.set(id, { id, label, icon, href, order, sectionId, position }); +export function registerNavItem({ id, label, icon, href, order = 0, sectionId = 'main', position, permission }) { + navItems.set(id, { id, label, icon, href, order, sectionId, position, permission }); } export function getNavSections() {