diff --git a/frontend/components/admin/AdminRoleTab.vue b/frontend/components/admin/AdminRoleTab.vue new file mode 100644 index 0000000..c36a706 --- /dev/null +++ b/frontend/components/admin/AdminRoleTab.vue @@ -0,0 +1,116 @@ + + + diff --git a/frontend/components/admin/RoleDrawer.vue b/frontend/components/admin/RoleDrawer.vue new file mode 100644 index 0000000..4db0dc5 --- /dev/null +++ b/frontend/components/admin/RoleDrawer.vue @@ -0,0 +1,186 @@ + + + diff --git a/frontend/i18n/locales/fr.json b/frontend/i18n/locales/fr.json index 5711583..321bfed 100644 --- a/frontend/i18n/locales/fr.json +++ b/frontend/i18n/locales/fr.json @@ -195,6 +195,27 @@ "addUser": "Ajouter un utilisateur", "editUser": "Modifier un utilisateur" }, + "admin": { + "roles": { + "title": "Rôles", + "addRole": "Ajouter un rôle", + "editRole": "Modifier un rôle", + "empty": "Aucun rôle trouvé.", + "system": "Système", + "code": "Code", + "codeHint": "Identifiant technique en snake_case (immuable).", + "codeImmutable": "Le code ne peut pas être modifié après création.", + "codeInvalid": "Code invalide (attendu snake_case : minuscules, chiffres et underscores).", + "label": "Libellé", + "labelRequired": "Le libellé est requis.", + "description": "Description", + "permissions": "Permissions", + "noPermissions": "Aucune permission disponible.", + "created": "Rôle créé avec succès.", + "updated": "Rôle mis à jour avec succès.", + "deleted": "Rôle supprimé avec succès." + } + }, "timeEntries": { "created": "Temps enregistré", "updated": "Temps modifié", diff --git a/frontend/modules/core/composables/usePermissions.ts b/frontend/modules/core/composables/usePermissions.ts new file mode 100644 index 0000000..0a9425a --- /dev/null +++ b/frontend/modules/core/composables/usePermissions.ts @@ -0,0 +1,27 @@ +export function usePermissions() { + const auth = useAuthStore() + + function isAdmin(): boolean { + return auth.user?.roles?.includes('ROLE_ADMIN') ?? false + } + + function can(code: string): boolean { + if (!auth.user) { + return false + } + if (isAdmin()) { + return true + } + return auth.user.effectivePermissions?.includes(code) ?? false + } + + function canAny(codes: string[]): boolean { + return codes.some((c) => can(c)) + } + + function canAll(codes: string[]): boolean { + return codes.every((c) => can(c)) + } + + return { can, canAny, canAll, isAdmin } +} diff --git a/frontend/modules/core/services/permissions.ts b/frontend/modules/core/services/permissions.ts new file mode 100644 index 0000000..bcad3ea --- /dev/null +++ b/frontend/modules/core/services/permissions.ts @@ -0,0 +1,22 @@ +import type { HydraCollection } from '~/utils/api' +import { extractHydraMembers } from '~/utils/api' + +export type Permission = { + id: number + '@id'?: string + code: string + label: string + module: string + orphan?: boolean +} + +export function usePermissionService() { + const api = useApi() + + async function list(): Promise { + const data = await api.get>('/permissions') + return extractHydraMembers(data) + } + + return { list } +} diff --git a/frontend/modules/core/services/roles.ts b/frontend/modules/core/services/roles.ts new file mode 100644 index 0000000..24bcdea --- /dev/null +++ b/frontend/modules/core/services/roles.ts @@ -0,0 +1,50 @@ +import type { Permission } from './permissions' +import type { HydraCollection } from '~/utils/api' +import { extractHydraMembers } from '~/utils/api' + +export type Role = { + id: number + '@id'?: string + code: string + label: string + description?: string | null + isSystem: boolean + permissions: Permission[] +} + +export type RoleWrite = { + code?: string + label: string + description?: string | null + /** IRIs of the granted permissions (e.g. /api/permissions/3). */ + permissions: string[] +} + +export function useRoleService() { + const api = useApi() + + async function list(): Promise { + const data = await api.get>('/roles') + return extractHydraMembers(data) + } + + async function create(payload: RoleWrite): Promise { + return api.post('/roles', payload as Record, { + toastSuccessKey: 'admin.roles.created', + }) + } + + async function update(id: number, payload: Partial): Promise { + return api.patch(`/roles/${id}`, payload as Record, { + toastSuccessKey: 'admin.roles.updated', + }) + } + + async function remove(id: number): Promise { + await api.delete(`/roles/${id}`, {}, { + toastSuccessKey: 'admin.roles.deleted', + }) + } + + return { list, create, update, remove } +} diff --git a/frontend/pages/admin.vue b/frontend/pages/admin.vue index 998deac..f371f09 100644 --- a/frontend/pages/admin.vue +++ b/frontend/pages/admin.vue @@ -6,7 +6,7 @@