feat(admin): add permission-based filtering to admin navigation

- add optional `permission` field to nav items in registry
- filter nav items by user permissions in `buildNavigationSections`
- auto-hide sections when all their items are filtered out
- fetch user permissions in `AdminLayout.server.js` and pass to navigation builder
- update docs and README to document `permission` param and new signature
This commit is contained in:
2026-04-25 09:27:07 -04:00
parent cb8266d9a9
commit 97f8baf502
5 changed files with 26 additions and 13 deletions
+1 -1
View File
@@ -88,7 +88,7 @@ registerWidgetFetcher('orders', async () => ({ total: await countOrders() }));
registerWidget({ id: 'orders', Component: OrdersWidget, order: 20 }); registerWidget({ id: 'orders', Component: OrdersWidget, order: 20 });
registerNavSection({ id: 'commerce', title: 'Commerce', icon: 'ShoppingBag03Icon', order: 30 }); 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' }); registerPage({ slug: 'orders', Component: OrdersPage, title: 'Commandes' });
``` ```
+3 -1
View File
@@ -3,12 +3,14 @@ import { protectAdmin } from './protect.js';
import { buildNavigationSections, buildBottomNavItems } from './navigation.js'; import { buildNavigationSections, buildBottomNavItems } from './navigation.js';
import { logoutAction } from '@zen/core/features/auth/actions'; import { logoutAction } from '@zen/core/features/auth/actions';
import { getAppName } from '@zen/core'; import { getAppName } from '@zen/core';
import { getUserPermissions } from '@zen/core/users';
import './widgets/index.server.js'; import './widgets/index.server.js';
export default async function AdminLayout({ children }) { export default async function AdminLayout({ children }) {
const session = await protectAdmin(); const session = await protectAdmin();
const appName = getAppName(); const appName = getAppName();
const navigationSections = buildNavigationSections('/'); const permissions = await getUserPermissions(session.user.id);
const navigationSections = buildNavigationSections('/', permissions);
const bottomNavItems = buildBottomNavItems('/'); const bottomNavItems = buildBottomNavItems('/');
return ( return (
+8 -5
View File
@@ -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 ```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 }] }] // [{ id, title, icon, items: [{ name, href, icon, current }] }]
``` ```
@@ -183,6 +184,7 @@ registerNavItem({
href: '/admin/orders', href: '/admin/orders',
sectionId: 'commerce', sectionId: 'commerce',
order: 10, 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 | | `icon` | `string` | Nom d'icône Hugeicons |
| `order` | `number` | Ordre d'affichage (défaut : `0`) | | `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 | | Paramètre | Type | Description |
|-----------|------|-------------| |-----------|------|-------------|
@@ -206,6 +208,7 @@ registerNavItem({
| `sectionId` | `string` | Section parente (défaut : `'main'`) | | `sectionId` | `string` | Section parente (défaut : `'main'`) |
| `order` | `number` | Ordre d'affichage (défaut : `0`) | | `order` | `number` | Ordre d'affichage (défaut : `0`) |
| `position` | `string` | `'bottom'` pour épingler en bas de la sidebar | | `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'; import OrdersPage from './admin/orders/OrdersPage.js';
registerNavSection({ id: 'commerce', title: 'Commerce', icon: 'ShoppingBag03Icon', order: 30 }); 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' }); registerPage({ slug: 'orders', Component: OrdersPage, title: 'Commandes' });
``` ```
+12 -4
View File
@@ -5,14 +5,15 @@ import {
getNavItems, getNavItems,
} from './registry.js'; } from './registry.js';
import { isDevkitEnabled } from '../../shared/lib/appConfig.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. // Sections et items core — enregistrés à l'import de ce module.
registerNavSection({ id: 'dashboard', title: 'Tableau de bord', icon: 'DashboardSquare03Icon', order: 10 }); registerNavSection({ id: 'dashboard', title: 'Tableau de bord', icon: 'DashboardSquare03Icon', order: 10 });
registerNavSection({ id: 'system', title: 'Utilisateurs', icon: 'UserMultiple02Icon', order: 20 }); 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: '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: '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 }); 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 }); registerNavItem({ id: 'settings', label: 'Paramètres', icon: 'Settings02Icon', href: '/admin/settings', position: 'bottom', order: 10 });
if (isDevkitEnabled()) { if (isDevkitEnabled()) {
@@ -24,10 +25,17 @@ if (isDevkitEnabled()) {
/** /**
* Build sections for AdminSidebar. Items are sérialisables (pas de composants), * Build sections for AdminSidebar. Items are sérialisables (pas de composants),
* icônes en chaînes résolues côté client. * 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 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(); const bySection = new Map();
for (const item of items) { for (const item of items) {
+2 -2
View File
@@ -57,8 +57,8 @@ export function registerNavSection({ id, title, icon, order = 0 }) {
navSections.set(id, { id, title, icon, order }); navSections.set(id, { id, title, icon, order });
} }
export function registerNavItem({ id, label, icon, href, order = 0, sectionId = 'main', position }) { export function registerNavItem({ id, label, icon, href, order = 0, sectionId = 'main', position, permission }) {
navItems.set(id, { id, label, icon, href, order, sectionId, position }); navItems.set(id, { id, label, icon, href, order, sectionId, position, permission });
} }
export function getNavSections() { export function getNavSections() {