feat(core) : add usePermissions composable and rbac roles admin front

This commit is contained in:
Matthieu
2026-06-19 17:35:51 +02:00
parent 544d4cf44f
commit 511353c3f5
10 changed files with 437 additions and 3 deletions
+116
View File
@@ -0,0 +1,116 @@
<template>
<div>
<div class="flex items-center justify-between">
<h2 class="text-lg font-bold text-neutral-900">{{ $t('admin.roles.title') }}</h2>
<MalioButton
icon-name="mdi:plus"
icon-position="left"
button-class="w-auto px-4"
:label="$t('admin.roles.addRole')"
@click="openCreate"
/>
</div>
<DataTable
:columns="columns"
:items="items"
:loading="isLoading"
:empty-message="$t('admin.roles.empty')"
@row-click="openEdit"
>
<template #cell-isSystem="{ item }">
<span
v-if="item.isSystem"
class="rounded-full bg-primary-100 px-2 py-0.5 text-xs font-semibold text-primary-600"
>
{{ $t('admin.roles.system') }}
</span>
</template>
<template #cell-permissions="{ item }">
<span class="text-neutral-600">{{ item.permissions.length }}</span>
</template>
<template #actions="{ item }">
<MalioButtonIcon
v-if="!item.isSystem"
icon="mdi:delete-outline"
:aria-label="$t('common.delete')"
variant="ghost"
icon-size="20"
button-class="text-neutral-400 hover:text-red-500"
@click.stop="handleDelete(item.id)"
/>
</template>
</DataTable>
<RoleDrawer
v-model="drawerOpen"
:item="selectedItem"
:permissions="permissions"
@saved="onSaved"
/>
</div>
</template>
<script setup lang="ts">
import type { Role } from '~/modules/core/services/roles'
import { useRoleService } from '~/modules/core/services/roles'
import type { Permission } from '~/modules/core/services/permissions'
import { usePermissionService } from '~/modules/core/services/permissions'
import type { DataTableColumn } from '~/components/ui/DataTable.vue'
const { t } = useI18n()
const columns = computed<DataTableColumn[]>(() => [
{ key: 'label', label: t('admin.roles.label'), primary: true },
{ key: 'code', label: t('admin.roles.code') },
{ key: 'permissions', label: t('admin.roles.permissions') },
{ key: 'isSystem', label: '' },
])
const roleService = useRoleService()
const permissionService = usePermissionService()
const items = ref<Role[]>([])
const permissions = ref<Permission[]>([])
const isLoading = ref(true)
const drawerOpen = ref(false)
const selectedItem = ref<Role | null>(null)
async function loadItems() {
isLoading.value = true
try {
items.value = await roleService.list()
} finally {
isLoading.value = false
}
}
async function loadPermissions() {
permissions.value = await permissionService.list()
}
function openCreate() {
selectedItem.value = null
drawerOpen.value = true
}
function openEdit(item: Role) {
selectedItem.value = item
drawerOpen.value = true
}
async function handleDelete(id: number) {
await roleService.remove(id)
await loadItems()
}
async function onSaved() {
await loadItems()
}
onMounted(() => {
loadItems()
loadPermissions()
})
</script>
+186
View File
@@ -0,0 +1,186 @@
<template>
<MalioDrawer v-model="isOpen">
<template #header>
<h2 class="text-xl font-bold">
{{ isEditing ? $t('admin.roles.editRole') : $t('admin.roles.addRole') }}
</h2>
</template>
<form class="flex flex-col gap-3" @submit.prevent="handleSubmit">
<MalioInputText
v-model="form.code"
:label="$t('admin.roles.code')"
input-class="w-full"
:disabled="isEditing"
:hint="isEditing ? $t('admin.roles.codeImmutable') : $t('admin.roles.codeHint')"
:error="touched.code && !codeValid ? $t('admin.roles.codeInvalid') : ''"
@blur="touched.code = true"
/>
<MalioInputText
v-model="form.label"
:label="$t('admin.roles.label')"
input-class="w-full"
:error="touched.label && !form.label.trim() ? $t('admin.roles.labelRequired') : ''"
@blur="touched.label = true"
/>
<MalioInputTextArea
v-model="form.description"
:label="$t('admin.roles.description')"
input-class="w-full"
/>
<div class="mt-2">
<label class="text-sm font-semibold text-neutral-700">
{{ $t('admin.roles.permissions') }}
</label>
<p v-if="permissions.length === 0" class="mt-2 text-xs text-neutral-400">
{{ $t('admin.roles.noPermissions') }}
</p>
<div
v-for="group in groupedPermissions"
:key="group.module"
class="mt-3 rounded-lg border border-neutral-200 p-3"
>
<p class="mb-2 text-xs font-bold uppercase tracking-wide text-neutral-500">
{{ group.module }}
</p>
<div class="flex flex-col gap-2">
<label
v-for="perm in group.permissions"
:key="perm.id"
class="flex items-start gap-2 text-sm text-neutral-700"
>
<input
v-model="form.permissions"
type="checkbox"
:value="perm['@id']"
class="mt-0.5 rounded border-neutral-300"
/>
<span>
{{ perm.label }}
<span class="block text-xs text-neutral-400">{{ perm.code }}</span>
</span>
</label>
</div>
</div>
</div>
<div class="mt-4 flex justify-end">
<MalioButton
:label="$t('common.save')"
button-class="w-auto px-6"
:disabled="isSubmitting"
@click="handleSubmit"
/>
</div>
</form>
</MalioDrawer>
</template>
<script setup lang="ts">
import type { Role, RoleWrite } from '~/modules/core/services/roles'
import { useRoleService } from '~/modules/core/services/roles'
import type { Permission } from '~/modules/core/services/permissions'
const props = defineProps<{
modelValue: boolean
item: Role | null
permissions: Permission[]
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
(e: 'saved'): void
}>()
const isOpen = computed({
get: () => props.modelValue,
set: (v) => emit('update:modelValue', v),
})
const isEditing = computed(() => !!props.item)
const isSubmitting = ref(false)
const form = reactive({
code: '',
label: '',
description: '',
permissions: [] as string[],
})
const touched = reactive({
code: false,
label: false,
})
const codeValid = computed(() => /^[a-z][a-z0-9_]*$/.test(form.code))
const groupedPermissions = computed(() => {
const byModule = new Map<string, Permission[]>()
for (const perm of props.permissions) {
const list = byModule.get(perm.module) ?? []
list.push(perm)
byModule.set(perm.module, list)
}
return [...byModule.entries()]
.map(([module, permissions]) => ({ module, permissions }))
.sort((a, b) => a.module.localeCompare(b.module))
})
watch(() => props.modelValue, (open) => {
if (open) {
if (props.item) {
form.code = props.item.code
form.label = props.item.label
form.description = props.item.description ?? ''
form.permissions = props.item.permissions
.map((p) => p['@id'])
.filter((iri): iri is string => !!iri)
} else {
form.code = ''
form.label = ''
form.description = ''
form.permissions = []
}
touched.code = false
touched.label = false
}
})
const { create, update } = useRoleService()
async function handleSubmit() {
touched.code = true
touched.label = true
if (!form.label.trim()) {
return
}
if (!isEditing.value && !codeValid.value) {
return
}
isSubmitting.value = true
try {
if (isEditing.value && props.item) {
const payload: Partial<RoleWrite> = {
label: form.label.trim(),
description: form.description.trim() || null,
permissions: form.permissions,
}
await update(props.item.id, payload)
} else {
const payload: RoleWrite = {
code: form.code.trim(),
label: form.label.trim(),
description: form.description.trim() || null,
permissions: form.permissions,
}
await create(payload)
}
emit('saved')
isOpen.value = false
} finally {
isSubmitting.value = false
}
}
</script>