feat(permissions) : add role-based UI guards and readonly mode for viewers

- Add usePermissions composable (isAdmin, canEdit, canView)
- Password-protected profile login with modal on profiles page
- Disable all form fields for ROLE_VIEWER across edit/create pages
- Show navigation buttons (Modifier/Consulter) for all roles, hide delete for viewers
- Add readonly prop to ModelTypeForm for category pages
- Disable modal fields (sites, constructeurs) for viewers
- Guard /admin routes in middleware

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-02-26 13:36:42 +01:00
parent 6bed715b7f
commit cc70fe2b29
46 changed files with 946 additions and 423 deletions
+14 -12
View File
@@ -64,7 +64,7 @@
v-model="editionForm.name"
type="text"
class="input input-bordered input-sm md:input-md"
:disabled="saving"
:disabled="!canEdit || saving"
required
>
</div>
@@ -79,7 +79,7 @@
v-model="editionForm.reference"
type="text"
class="input input-bordered input-sm md:input-md"
:disabled="saving"
:disabled="!canEdit || saving"
>
</div>
@@ -90,7 +90,7 @@
<ConstructeurSelect
v-model="editionForm.constructeurIds"
class="w-full"
:disabled="saving"
:disabled="!canEdit || saving"
placeholder="Rechercher un ou plusieurs fournisseurs..."
:initial-options="product?.constructeurs || []"
/>
@@ -108,7 +108,7 @@
step="0.01"
min="0"
class="input input-bordered input-sm md:input-md"
:disabled="saving"
:disabled="!canEdit || saving"
>
</div>
</div>
@@ -148,7 +148,7 @@
type="text"
class="input input-bordered input-sm md:input-md"
:required="field.required"
:disabled="saving"
:disabled="!canEdit || saving"
>
<input
v-else-if="field.type === 'number'"
@@ -157,14 +157,14 @@
step="0.01"
class="input input-bordered input-sm md:input-md"
:required="field.required"
:disabled="saving"
:disabled="!canEdit || saving"
>
<select
v-else-if="field.type === 'select'"
v-model="field.value"
class="select select-bordered select-sm md:select-md"
:required="field.required"
:disabled="saving"
:disabled="!canEdit || saving"
>
<option value="">Sélectionner...</option>
<option
@@ -182,7 +182,7 @@
class="checkbox checkbox-sm"
true-value="true"
false-value="false"
:disabled="saving"
:disabled="!canEdit || saving"
>
<span class="text-sm">{{ field.value === 'true' ? 'Oui' : 'Non' }}</span>
</div>
@@ -192,7 +192,7 @@
type="date"
class="input input-bordered input-sm md:input-md"
:required="field.required"
:disabled="saving"
:disabled="!canEdit || saving"
>
<input
v-else
@@ -200,7 +200,7 @@
type="text"
class="input input-bordered input-sm md:input-md"
:required="field.required"
:disabled="saving"
:disabled="!canEdit || saving"
>
</div>
</div>
@@ -218,7 +218,7 @@
{{ selectedFiles.length }} document{{ selectedFiles.length > 1 ? 's' : '' }} sélectionné{{ selectedFiles.length > 1 ? 's' : '' }}
</span>
</header>
<div :class="{ 'pointer-events-none opacity-60': saving || uploadingDocuments }">
<div :class="{ 'pointer-events-none opacity-60': !canEdit || saving || uploadingDocuments }">
<DocumentUpload
v-model="selectedFiles"
title="Déposer vos fichiers"
@@ -286,6 +286,7 @@
Télécharger
</button>
<button
v-if="canEdit"
type="button"
class="btn btn-error btn-xs"
:disabled="uploadingDocuments || saving"
@@ -424,6 +425,7 @@ import {
historyDiffEntries as _historyDiffEntries,
} from '~/shared/utils/historyDisplayUtils'
const { canEdit } = usePermissions()
const route = useRoute()
const router = useRouter()
const toast = useToast()
@@ -489,7 +491,7 @@ const requiredCustomFieldsFilled = computed(() =>
)
const canSubmit = computed(() =>
Boolean(product.value && editionForm.name.trim().length >= 2 && requiredCustomFieldsFilled.value && !saving.value),
Boolean(canEdit.value && product.value && editionForm.name.trim().length >= 2 && requiredCustomFieldsFilled.value && !saving.value),
)
const structurePreview = computed(() => formatProductStructurePreview(structure.value))