refactor(users)!: merge users.edit and users.delete into users.manage permission

BREAKING CHANGE: permissions `users.edit` and `users.delete` have been replaced by a single `users.manage` permission; any role or code referencing the old keys must be updated

- remove `USERS_EDIT` and `USERS_DELETE` from `PERMISSIONS` and `PERMISSION_DEFINITIONS`
- add `USERS_MANAGE` permission covering create, edit and delete actions
- update `db.js` to use `users.manage` in permission checks
- update `auth/api.js` to reference the new permission key
- update `UsersPage.client.js` to check `users.manage` instead of old keys
- update `api/define.js` and all README examples to reflect the new key
This commit is contained in:
2026-04-25 09:47:34 -04:00
parent 27ebc91d31
commit 2360021376
7 changed files with 34 additions and 20 deletions
+1 -1
View File
@@ -181,7 +181,7 @@ Champs optionnels :
| Champ | Type | Description | | Champ | Type | Description |
|-------|------|-------------| |-------|------|-------------|
| `skipRateLimit` | `boolean` | Exempte la route du rate limiting par IP (ex. health checks) | | `skipRateLimit` | `boolean` | Exempte la route du rate limiting par IP (ex. health checks) |
| `permission` | `string` | Sur une route `auth: 'admin'`, exige en plus cette clé de permission granulaire (ex. `'users.edit'`). Retourne 403 si l'utilisateur ne la possède pas. Voir `PERMISSIONS` dans `src/core/users/constants.js` | | `permission` | `string` | Sur une route `auth: 'admin'`, exige en plus cette clé de permission granulaire (ex. `'users.manage'`). Retourne 403 si l'utilisateur ne la possède pas. Voir `PERMISSIONS` dans `src/core/users/constants.js` |
--- ---
+1 -1
View File
@@ -25,7 +25,7 @@
* (e.g. health checks from monitoring systems). * (e.g. health checks from monitoring systems).
* permission {string} When set on an 'admin' route, the router additionally * permission {string} When set on an 'admin' route, the router additionally
* verifies that the authenticated user holds this granular * verifies that the authenticated user holds this granular
* permission key (e.g. 'users.edit'). If the user lacks * permission key (e.g. 'users.manage'). If the user lacks
* the permission, the request is rejected with 403 Forbidden. * the permission, the request is rejected with 403 Forbidden.
* *
* Auth levels: * Auth levels:
+2 -2
View File
@@ -204,7 +204,7 @@ const role = await createRole({ name: 'Modérateur', description: 'Peut gérer l
await updateRole(roleId, { await updateRole(roleId, {
name: 'Modérateur', name: 'Modérateur',
permissionKeys: [PERMISSIONS.USERS_VIEW, PERMISSIONS.USERS_EDIT], permissionKeys: [PERMISSIONS.USERS_VIEW, PERMISSIONS.USERS_MANAGE],
}); });
await deleteRole(roleId); // impossible sur les rôles système await deleteRole(roleId); // impossible sur les rôles système
@@ -230,7 +230,7 @@ const keys = await getUserPermissions(userId);
| Groupe | Clés | | Groupe | Clés |
|--------|------| |--------|------|
| Administration | `admin.access` | | Administration | `admin.access` |
| Utilisateurs | `users.view`, `users.edit`, `users.delete` | | Utilisateurs | `users.view`, `users.manage` |
| Rôles | `roles.view`, `roles.manage` | | Rôles | `roles.view`, `roles.manage` |
--- ---
+2 -4
View File
@@ -6,8 +6,7 @@
export const PERMISSIONS = { export const PERMISSIONS = {
ADMIN_ACCESS: 'admin.access', ADMIN_ACCESS: 'admin.access',
USERS_VIEW: 'users.view', USERS_VIEW: 'users.view',
USERS_EDIT: 'users.edit', USERS_MANAGE: 'users.manage',
USERS_DELETE: 'users.delete',
ROLES_VIEW: 'roles.view', ROLES_VIEW: 'roles.view',
ROLES_MANAGE: 'roles.manage', ROLES_MANAGE: 'roles.manage',
}; };
@@ -15,8 +14,7 @@ export const PERMISSIONS = {
export const PERMISSION_DEFINITIONS = [ export const PERMISSION_DEFINITIONS = [
{ key: 'admin.access', name: 'Accès au panneau admin', description: "Permet d'accéder à l'interface d'administration.", group_name: 'Administration' }, { key: 'admin.access', name: 'Accès au panneau admin', description: "Permet d'accéder à l'interface d'administration.", group_name: 'Administration' },
{ key: 'users.view', name: 'Voir les utilisateurs', description: 'Permet de consulter la liste des membres et leurs profils.', group_name: 'Utilisateurs' }, { key: 'users.view', name: 'Voir les utilisateurs', description: 'Permet de consulter la liste des membres et leurs profils.', group_name: 'Utilisateurs' },
{ key: 'users.edit', name: 'Modifier les utilisateurs', description: 'Permet de changer les informations et les rôles des membres.', group_name: 'Utilisateurs' }, { key: 'users.manage', name: 'Gérer les utilisateurs', description: 'Permet de créer, modifier et supprimer des comptes membres.', group_name: 'Utilisateurs' },
{ key: 'users.delete', name: 'Supprimer des utilisateurs', description: 'Permet de supprimer des comptes membres.', group_name: 'Utilisateurs' },
{ key: 'roles.view', name: 'Voir les rôles', description: 'Permet de consulter la liste des rôles et leurs permissions.', group_name: 'Rôles' }, { key: 'roles.view', name: 'Voir les rôles', description: 'Permet de consulter la liste des rôles et leurs permissions.', group_name: 'Rôles' },
{ key: 'roles.manage', name: 'Gérer les rôles', description: 'Permet de créer, modifier et supprimer des rôles.', group_name: 'Rôles' }, { key: 'roles.manage', name: 'Gérer les rôles', description: 'Permet de créer, modifier et supprimer des rôles.', group_name: 'Rôles' },
]; ];
+16
View File
@@ -66,6 +66,20 @@ async function dropRoleCheckConstraint() {
`); `);
} }
async function migratePermissions() {
// Migrate users.edit / users.delete → users.manage
await query(`
INSERT INTO zen_auth_role_permissions (role_id, permission_key)
SELECT DISTINCT role_id, 'users.manage'
FROM zen_auth_role_permissions
WHERE permission_key IN ('users.edit', 'users.delete')
AND EXISTS (SELECT 1 FROM zen_auth_permissions WHERE key = 'users.manage')
ON CONFLICT DO NOTHING
`);
await query(`DELETE FROM zen_auth_role_permissions WHERE permission_key IN ('users.edit', 'users.delete')`);
await query(`DELETE FROM zen_auth_permissions WHERE key IN ('users.edit', 'users.delete')`);
}
async function seedDefaultRolesAndPermissions() { async function seedDefaultRolesAndPermissions() {
// Permissions // Permissions
for (const perm of PERMISSION_DEFINITIONS) { for (const perm of PERMISSION_DEFINITIONS) {
@@ -75,6 +89,8 @@ async function seedDefaultRolesAndPermissions() {
); );
} }
await migratePermissions();
// Admin role // Admin role
const adminRoleId = generateId(); const adminRoleId = generateId();
await query( await query(
+1 -1
View File
@@ -174,7 +174,7 @@ const UsersPageClient = ({ currentUserId, refreshKey, canEdit }) => {
const UsersPage = ({ user }) => { const UsersPage = ({ user }) => {
const [createModalOpen, setCreateModalOpen] = useState(false); const [createModalOpen, setCreateModalOpen] = useState(false);
const [refreshKey, setRefreshKey] = useState(0); const [refreshKey, setRefreshKey] = useState(0);
const canEdit = user?.permissions?.includes('users.edit'); const canEdit = user?.permissions?.includes('users.manage');
return ( return (
<div className="flex flex-col gap-4 sm:gap-6 lg:gap-8"> <div className="flex flex-col gap-4 sm:gap-6 lg:gap-8">
+7 -7
View File
@@ -897,7 +897,7 @@ async function handleAdminCreateUser(request) {
export const routes = defineApiRoutes([ export const routes = defineApiRoutes([
{ path: '/users', method: 'GET', handler: handleListUsers, auth: 'admin', permission: PERMISSIONS.USERS_VIEW }, { path: '/users', method: 'GET', handler: handleListUsers, auth: 'admin', permission: PERMISSIONS.USERS_VIEW },
{ path: '/users', method: 'POST', handler: handleAdminCreateUser, auth: 'admin', permission: PERMISSIONS.USERS_EDIT }, { path: '/users', method: 'POST', handler: handleAdminCreateUser, auth: 'admin', permission: PERMISSIONS.USERS_MANAGE },
{ path: '/users/profile', method: 'PUT', handler: handleUpdateProfile, auth: 'user' }, { path: '/users/profile', method: 'PUT', handler: handleUpdateProfile, auth: 'user' },
{ path: '/users/profile/email', method: 'POST', handler: handleInitiateEmailChange, auth: 'user' }, { path: '/users/profile/email', method: 'POST', handler: handleInitiateEmailChange, auth: 'user' },
{ path: '/users/profile/password', method: 'POST', handler: handleChangeOwnPassword, auth: 'user' }, { path: '/users/profile/password', method: 'POST', handler: handleChangeOwnPassword, auth: 'user' },
@@ -908,13 +908,13 @@ export const routes = defineApiRoutes([
{ path: '/users/profile/sessions/:sessionId', method: 'DELETE', handler: handleDeleteSession, auth: 'user' }, { path: '/users/profile/sessions/:sessionId', method: 'DELETE', handler: handleDeleteSession, auth: 'user' },
{ path: '/users/email/confirm', method: 'GET', handler: handleConfirmEmailChange, auth: 'user' }, { path: '/users/email/confirm', method: 'GET', handler: handleConfirmEmailChange, auth: 'user' },
{ path: '/users/:id/roles', method: 'GET', handler: handleGetUserRoles, auth: 'admin', permission: PERMISSIONS.USERS_VIEW }, { path: '/users/:id/roles', method: 'GET', handler: handleGetUserRoles, auth: 'admin', permission: PERMISSIONS.USERS_VIEW },
{ path: '/users/:id/roles', method: 'POST', handler: handleAssignUserRole, auth: 'admin', permission: PERMISSIONS.USERS_EDIT }, { path: '/users/:id/roles', method: 'POST', handler: handleAssignUserRole, auth: 'admin', permission: PERMISSIONS.USERS_MANAGE },
{ path: '/users/:id/roles/:roleId', method: 'DELETE', handler: handleRevokeUserRole, auth: 'admin', permission: PERMISSIONS.USERS_EDIT }, { path: '/users/:id/roles/:roleId', method: 'DELETE', handler: handleRevokeUserRole, auth: 'admin', permission: PERMISSIONS.USERS_MANAGE },
{ path: '/users/:id', method: 'GET', handler: handleGetUserById, auth: 'admin', permission: PERMISSIONS.USERS_VIEW }, { path: '/users/:id', method: 'GET', handler: handleGetUserById, auth: 'admin', permission: PERMISSIONS.USERS_VIEW },
{ path: '/users/:id', method: 'PUT', handler: handleUpdateUserById, auth: 'admin', permission: PERMISSIONS.USERS_EDIT }, { path: '/users/:id', method: 'PUT', handler: handleUpdateUserById, auth: 'admin', permission: PERMISSIONS.USERS_MANAGE },
{ path: '/users/:id/email', method: 'PUT', handler: handleAdminUpdateUserEmail, auth: 'admin', permission: PERMISSIONS.USERS_EDIT }, { path: '/users/:id/email', method: 'PUT', handler: handleAdminUpdateUserEmail, auth: 'admin', permission: PERMISSIONS.USERS_MANAGE },
{ path: '/users/:id/password', method: 'PUT', handler: handleAdminSetUserPassword, auth: 'admin', permission: PERMISSIONS.USERS_EDIT }, { path: '/users/:id/password', method: 'PUT', handler: handleAdminSetUserPassword, auth: 'admin', permission: PERMISSIONS.USERS_MANAGE },
{ path: '/users/:id/send-password-reset', method: 'POST', handler: handleAdminSendPasswordReset, auth: 'admin', permission: PERMISSIONS.USERS_EDIT }, { path: '/users/:id/send-password-reset', method: 'POST', handler: handleAdminSendPasswordReset, auth: 'admin', permission: PERMISSIONS.USERS_MANAGE },
{ path: '/roles', method: 'GET', handler: handleListRoles, auth: 'admin', permission: PERMISSIONS.ROLES_VIEW }, { path: '/roles', method: 'GET', handler: handleListRoles, auth: 'admin', permission: PERMISSIONS.ROLES_VIEW },
{ path: '/roles', method: 'POST', handler: handleCreateRole, auth: 'admin', permission: PERMISSIONS.ROLES_MANAGE }, { path: '/roles', method: 'POST', handler: handleCreateRole, auth: 'admin', permission: PERMISSIONS.ROLES_MANAGE },
{ path: '/roles/:id', method: 'GET', handler: handleGetRole, auth: 'admin', permission: PERMISSIONS.ROLES_VIEW }, { path: '/roles/:id', method: 'GET', handler: handleGetRole, auth: 'admin', permission: PERMISSIONS.ROLES_VIEW },