diff --git a/frontend/modules/core/components/RoleDrawer.vue b/frontend/modules/core/components/RoleDrawer.vue index 7933ef8..6b054f6 100644 --- a/frontend/modules/core/components/RoleDrawer.vue +++ b/frontend/modules/core/components/RoleDrawer.vue @@ -19,8 +19,7 @@ :label="t('admin.roles.form.code')" input-class="w-full" required - :readonly="isEditMode && role?.isSystem" - :hint="isEditMode && role?.isSystem ? t('admin.roles.delete.systemTooltip') : ''" + :readonly="isEditMode" /> (() => { async function loadPermissions() { const data = await api.get<{ member: Permission[] }>( '/permissions', - { 'orphan': false, itemsPerPage: 200 }, + { 'orphan': false, itemsPerPage: 999 }, { toast: false }, ) allPermissions.value = data.member @@ -183,19 +182,24 @@ function handleToggleAll(module: string, selected: boolean) { async function handleSave() { saving.value = true try { - const body = { - label: form.value.label, - code: form.value.code, - description: form.value.description || null, - permissions: Array.from(selectedPermissionIds.value).map(id => `/api/permissions/${id}`), - } + const permissions = Array.from(selectedPermissionIds.value).map(id => `/api/permissions/${id}`) if (isEditMode.value && props.role) { - await api.patch(`/roles/${props.role.id}`, body, { + // Le code est immuable apres creation (garde backend RoleProcessor) + await api.patch(`/roles/${props.role.id}`, { + label: form.value.label, + description: form.value.description || null, + permissions, + }, { toastSuccessMessage: t('admin.roles.toast.updated'), }) } else { - await api.post('/roles', body, { + await api.post('/roles', { + label: form.value.label, + code: form.value.code, + description: form.value.description || null, + permissions, + }, { toastSuccessMessage: t('admin.roles.toast.created'), }) } diff --git a/frontend/modules/core/components/UserRbacDrawer.vue b/frontend/modules/core/components/UserRbacDrawer.vue index 2ec4ce1..15d3b4c 100644 --- a/frontend/modules/core/components/UserRbacDrawer.vue +++ b/frontend/modules/core/components/UserRbacDrawer.vue @@ -186,7 +186,7 @@ const effectivePermissions = computed(() => { async function loadData() { const [rolesData, permsData] = await Promise.all([ api.get<{ member: Role[] }>('/roles', {}, { toast: false }), - api.get<{ member: Permission[] }>('/permissions', { orphan: false, itemsPerPage: 200 }, { toast: false }), + api.get<{ member: Permission[] }>('/permissions', { orphan: false, itemsPerPage: 999 }, { toast: false }), ]) allRoles.value = rolesData.member allPermissions.value = permsData.member diff --git a/frontend/modules/core/pages/admin/roles.vue b/frontend/modules/core/pages/admin/roles.vue index a1d3699..505e0da 100644 --- a/frontend/modules/core/pages/admin/roles.vue +++ b/frontend/modules/core/pages/admin/roles.vue @@ -81,7 +81,7 @@ import type { Role } from '~/shared/types/rbac' const { t } = useI18n() const api = useApi() const { can } = usePermissions() -const canManage = can('core.roles.manage') +const canManage = computed(() => can('core.roles.manage')) useHead({ title: t('admin.roles.title') }) diff --git a/frontend/modules/core/pages/admin/users.vue b/frontend/modules/core/pages/admin/users.vue index e0905cd..990aaa2 100644 --- a/frontend/modules/core/pages/admin/users.vue +++ b/frontend/modules/core/pages/admin/users.vue @@ -56,7 +56,7 @@ const { can } = usePermissions() useHead({ title: t('admin.users.title') }) -const canManage = can('core.users.manage') +const canManage = computed(() => can('core.users.manage')) const users = ref([]) const loading = ref(false) diff --git a/src/Module/Core/Domain/Security/AdminHeadcountGuard.php b/src/Module/Core/Domain/Security/AdminHeadcountGuard.php index 8978398..cd244a7 100644 --- a/src/Module/Core/Domain/Security/AdminHeadcountGuard.php +++ b/src/Module/Core/Domain/Security/AdminHeadcountGuard.php @@ -53,6 +53,13 @@ final class AdminHeadcountGuard implements AdminHeadcountGuardInterface * La verification est volontairement conservative (<=1) pour couvrir * le cas defensif ou la base serait deja dans un etat incoherent (0 admin). * + * TOCTOU accepte : la verification n'utilise pas de verrou pessimiste + * (SELECT ... FOR UPDATE). Deux demotions concurrentes pourraient donc + * passer le garde simultanement. Ce risque est accepte dans le contexte + * PME/CRM ou les operations d'administration sont rares et mono-operateur. + * Si la concurrence admin devient un enjeu, ajouter un verrou pessimiste + * sur countAdmins() ou une contrainte CHECK en base. + * * @throws LastAdminProtectionException si le nombre d'admins est inferieur ou egal a 1 */ private function checkAdminHeadcount(): void