Supprime la colonne actions des tables users et roles (la ligne cliquable ouvre deja le drawer). Deplace la suppression d'un role dans le drawer d'edition (bouton danger avec icone, desactive pour les roles systeme). Harmonise les boutons annuler en variant tertiary et ajoute les icones manquantes (plus pour nouveau role, poubelle pour supprimer). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
260 lines
9.0 KiB
Vue
260 lines
9.0 KiB
Vue
<template>
|
|
<MalioDrawer
|
|
:model-value="modelValue"
|
|
:title="t('admin.users.drawer.title', { username: user?.username ?? '' })"
|
|
drawer-class="w-full max-w-lg"
|
|
@update:model-value="emit('update:modelValue', $event)"
|
|
>
|
|
<div class="flex flex-col gap-6 p-4">
|
|
<!-- Avertissement auto-edition -->
|
|
<div
|
|
v-if="isSelfEdit"
|
|
class="flex items-center gap-2 rounded-lg border border-yellow-300 bg-yellow-50 px-4 py-3 text-sm text-yellow-800"
|
|
>
|
|
<Icon name="mdi:alert-outline" class="size-5 shrink-0" />
|
|
{{ t('admin.users.drawer.selfWarning') }}
|
|
</div>
|
|
|
|
<!-- Toggle Administrateur -->
|
|
<MalioCheckbox
|
|
id="admin-toggle"
|
|
:label="t('admin.users.drawer.adminToggle')"
|
|
:model-value="form.isAdmin"
|
|
label-class="font-semibold text-sm text-neutral-700"
|
|
@update:model-value="form.isAdmin = $event"
|
|
/>
|
|
|
|
<!-- Section Roles -->
|
|
<div>
|
|
<h4 class="mb-3 text-sm font-semibold text-neutral-700">
|
|
{{ t('admin.users.drawer.rolesSection') }}
|
|
</h4>
|
|
<div class="flex flex-col gap-2">
|
|
<MalioCheckbox
|
|
v-for="role in allRoles"
|
|
:key="role.id"
|
|
:id="`role-${role.id}`"
|
|
:label="role.label"
|
|
:model-value="selectedRoleIds.has(role.id)"
|
|
label-class="text-sm text-neutral-600"
|
|
@update:model-value="(val: boolean) => toggleRole(role.id, val)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Section Permissions directes -->
|
|
<div>
|
|
<h4 class="mb-3 text-sm font-semibold text-neutral-700">
|
|
{{ t('admin.users.drawer.directPermissionsSection') }}
|
|
</h4>
|
|
<div v-if="permissionsByModule.length === 0" class="text-sm text-neutral-400">
|
|
{{ t('admin.roles.permissions.noPermissions') }}
|
|
</div>
|
|
<div class="flex flex-col gap-4">
|
|
<PermissionGroup
|
|
v-for="group in permissionsByModule"
|
|
:key="group.module"
|
|
:module="group.module"
|
|
:module-label="group.module"
|
|
:permissions="group.permissions"
|
|
:selected-ids="selectedDirectPermissionIds"
|
|
@toggle="handleTogglePermission"
|
|
@toggle-all="handleToggleAll"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Section Resume permissions effectives -->
|
|
<div>
|
|
<h4 class="mb-3 text-sm font-semibold text-neutral-700">
|
|
{{ t('admin.users.drawer.summarySection') }}
|
|
</h4>
|
|
<EffectivePermissions :permissions="effectivePermissions" />
|
|
</div>
|
|
|
|
<!-- Boutons -->
|
|
<div class="flex justify-end gap-3 border-t border-neutral-200 pt-4">
|
|
<MalioButton
|
|
:label="t('common.cancel')"
|
|
variant="tertiary"
|
|
@click="emit('update:modelValue', false)"
|
|
/>
|
|
<MalioButton
|
|
:label="t('common.save')"
|
|
variant="primary"
|
|
:disabled="saving"
|
|
@click="handleSave"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</MalioDrawer>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { Permission, Role, UserListItem, EffectivePermission } from '~/shared/types/rbac'
|
|
|
|
interface PermissionModule {
|
|
module: string
|
|
permissions: Permission[]
|
|
}
|
|
|
|
const { t } = useI18n()
|
|
const api = useApi()
|
|
const auth = useAuthStore()
|
|
|
|
const props = defineProps<{
|
|
modelValue: boolean
|
|
user: UserListItem | null
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [value: boolean]
|
|
saved: []
|
|
}>()
|
|
|
|
const saving = ref(false)
|
|
const allRoles = ref<Role[]>([])
|
|
const allPermissions = ref<Permission[]>([])
|
|
|
|
const form = ref({ isAdmin: false })
|
|
const selectedRoleIds = ref(new Set<number>())
|
|
const selectedDirectPermissionIds = ref(new Set<number>())
|
|
|
|
// Detecter l'auto-edition
|
|
const isSelfEdit = computed(() => props.user?.id === auth.user?.id)
|
|
|
|
// Extraire un ID depuis une IRI API Platform
|
|
function iriToId(iri: string): number {
|
|
return Number(iri.split('/').pop())
|
|
}
|
|
|
|
// Grouper les permissions par module (pour les checkboxes)
|
|
const permissionsByModule = computed<PermissionModule[]>(() => {
|
|
const groups = new Map<string, Permission[]>()
|
|
for (const perm of allPermissions.value) {
|
|
if (perm.orphan) continue
|
|
const list = groups.get(perm.module) || []
|
|
list.push(perm)
|
|
groups.set(perm.module, list)
|
|
}
|
|
return Array.from(groups.entries())
|
|
.map(([module, permissions]) => ({ module, permissions }))
|
|
.sort((a, b) => a.module.localeCompare(b.module))
|
|
})
|
|
|
|
// Calculer les permissions effectives avec leurs sources
|
|
const effectivePermissions = computed<EffectivePermission[]>(() => {
|
|
const permMap = new Map<number, Permission>()
|
|
for (const p of allPermissions.value) {
|
|
if (!p.orphan) permMap.set(p.id, p)
|
|
}
|
|
|
|
// Construire la map permissionId -> sources[]
|
|
const result = new Map<number, string[]>()
|
|
|
|
// Permissions heritees des roles
|
|
for (const roleId of selectedRoleIds.value) {
|
|
const role = allRoles.value.find(r => r.id === roleId)
|
|
if (!role) continue
|
|
for (const p of role.permissions) {
|
|
const pid = typeof p === 'string' ? iriToId(p) : p.id
|
|
const sources = result.get(pid) || []
|
|
sources.push(t('admin.users.drawer.sourceRole', { role: role.label }))
|
|
result.set(pid, sources)
|
|
}
|
|
}
|
|
|
|
// Permissions directes
|
|
for (const pid of selectedDirectPermissionIds.value) {
|
|
const sources = result.get(pid) || []
|
|
sources.push(t('admin.users.drawer.sourceDirect'))
|
|
result.set(pid, sources)
|
|
}
|
|
|
|
// Construire la liste finale
|
|
return Array.from(result.entries())
|
|
.map(([pid, sources]) => {
|
|
const perm = permMap.get(pid)
|
|
if (!perm) return null
|
|
return { code: perm.code, label: perm.label, module: perm.module, sources }
|
|
})
|
|
.filter((p): p is EffectivePermission => p !== null)
|
|
.sort((a, b) => a.code.localeCompare(b.code))
|
|
})
|
|
|
|
// Charger roles et permissions
|
|
async function loadData() {
|
|
const [rolesData, permsData] = await Promise.all([
|
|
api.get<{ member: Role[] }>('/roles', {}, { toast: false }),
|
|
api.get<{ member: Permission[] }>('/permissions', { orphan: false, itemsPerPage: 999 }, { toast: false }),
|
|
])
|
|
allRoles.value = rolesData.member
|
|
allPermissions.value = permsData.member
|
|
}
|
|
|
|
// Remplir le formulaire quand le user change
|
|
watch(() => props.user, (user) => {
|
|
if (user) {
|
|
form.value.isAdmin = user.isAdmin
|
|
selectedRoleIds.value = new Set(user.roles.map(iriToId))
|
|
selectedDirectPermissionIds.value = new Set(user.directPermissions.map(iriToId))
|
|
} else {
|
|
form.value.isAdmin = false
|
|
selectedRoleIds.value = new Set()
|
|
selectedDirectPermissionIds.value = new Set()
|
|
}
|
|
}, { immediate: true })
|
|
|
|
// Charger les donnees quand le drawer s'ouvre
|
|
watch(() => props.modelValue, (open) => {
|
|
if (open) loadData()
|
|
})
|
|
|
|
function toggleRole(id: number, selected: boolean) {
|
|
const ids = new Set(selectedRoleIds.value)
|
|
if (selected) ids.add(id)
|
|
else ids.delete(id)
|
|
selectedRoleIds.value = ids
|
|
}
|
|
|
|
function handleTogglePermission(id: number, selected: boolean) {
|
|
const ids = new Set(selectedDirectPermissionIds.value)
|
|
if (selected) ids.add(id)
|
|
else ids.delete(id)
|
|
selectedDirectPermissionIds.value = ids
|
|
}
|
|
|
|
function handleToggleAll(module: string, selected: boolean) {
|
|
const ids = new Set(selectedDirectPermissionIds.value)
|
|
const group = permissionsByModule.value.find(g => g.module === module)
|
|
if (!group) return
|
|
for (const perm of group.permissions) {
|
|
if (selected) ids.add(perm.id)
|
|
else ids.delete(perm.id)
|
|
}
|
|
selectedDirectPermissionIds.value = ids
|
|
}
|
|
|
|
async function handleSave() {
|
|
if (!props.user) return
|
|
saving.value = true
|
|
try {
|
|
await api.patch(`/users/${props.user.id}/rbac`, {
|
|
isAdmin: form.value.isAdmin,
|
|
roles: Array.from(selectedRoleIds.value).map(id => `/api/roles/${id}`),
|
|
directPermissions: Array.from(selectedDirectPermissionIds.value).map(id => `/api/permissions/${id}`),
|
|
}, {
|
|
toastSuccessMessage: t('admin.users.toast.updated'),
|
|
})
|
|
// Rafraichir les donnees du user courant si auto-edition
|
|
if (isSelfEdit.value) {
|
|
await auth.refreshUser()
|
|
}
|
|
emit('saved')
|
|
emit('update:modelValue', false)
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
</script>
|