Files
Coltura/frontend/shared/components/audit/AuditLogDetail.vue
Matthieu b1255bb57a fix(review) : resout findings 3e passe review (HIGH frontend + MEDIUMs backend/frontend/E2E)
Backend :
- AuditLogWriter::stripSensitive rendu reellement recursif (matche doc).
- Tests GET /api/permissions/{id} non-admin pour chaque branche OR (gap Codex).
- Gardes non-regression UserRbacProcessor : PATCH /rbac sans clef sites ne
  doit ni auto-selectionner currentSite ni exiger sites.manage.

Frontend :
- useAuditLog : renomme export trompeur fetchLogs -> fetchLogsCached, le
  nom reflete desormais le comportement (cache pollue sinon).
- RoleDrawer / UserRbacDrawer : catch explicite + message d'erreur +
  bouton save disabled si le chargement des referentiels a echoue (evite
  un ecrasement silencieux des droits).
- AuditTimeline / AuditLogDetail : `oui`/`non` passent par common.yes/no.
- AuditTimeline : Intl.RelativeTimeFormat et toLocaleString suivent la
  locale i18n courante (plus de hardcode 'fr').

E2E :
- sidebar-visibility.spec : remplace waitForLoadState('networkidle')
  fragile par attente semantique sur accountDashboardLink (stable en CI).

Tests : 237/237 green, eslint clean, php-cs-fixer clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 10:31:03 +02:00

101 lines
4.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<!--
Vue de detail d'une ligne d'audit : tableau field/old/new pour une
update, sinon snapshot complet sous forme de liste { cle: valeur }.
-->
<div class="text-sm">
<p class="text-xs text-gray-500 mb-2">
<span v-if="entry.ipAddress">IP: {{ entry.ipAddress }}</span>
<span v-if="entry.requestId" class="ml-3">Req: {{ entry.requestId }}</span>
</p>
<div v-if="entry.action === 'update'">
<!-- Tableau de comparaison field/old/new. MalioDataTable n'est
pas adapte ici : cas presentationnel non-paginable (cf.
exception documentee dans CLAUDE.md). -->
<table class="min-w-full border border-gray-200 text-xs">
<thead class="bg-gray-100">
<tr>
<th class="px-2 py-1 text-left font-medium">{{ t('audit.detail.field') }}</th>
<th class="px-2 py-1 text-left font-medium">{{ t('audit.detail.old_value') }}</th>
<th class="px-2 py-1 text-left font-medium">{{ t('audit.detail.new_value') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(diff, field) in updateDiff" :key="field" class="border-t border-gray-200">
<td class="px-2 py-1 font-mono">{{ field }}</td>
<td class="px-2 py-1 text-red-700">{{ formatValue(diff.old) }}</td>
<td class="px-2 py-1 text-green-700">{{ formatValue(diff.new) }}</td>
</tr>
<!-- Modifications de collections to-many : shape different
{ added: [ids], removed: [ids] } affiche + et - sur
la meme ligne pour garder une colonne field unique. -->
<tr v-for="(diff, field) in collectionDiff" :key="`col-${field}`" class="border-t border-gray-200">
<td class="px-2 py-1 font-mono">{{ field }}</td>
<td class="px-2 py-1 text-red-700">
<span v-if="diff.removed.length"> {{ diff.removed.join(', ') }}</span>
<span v-else class="text-gray-400"></span>
</td>
<td class="px-2 py-1 text-green-700">
<span v-if="diff.added.length">+ {{ diff.added.join(', ') }}</span>
<span v-else class="text-gray-400"></span>
</td>
</tr>
</tbody>
</table>
</div>
<div v-else class="space-y-1">
<div v-for="(value, key) in entry.changes" :key="key" class="flex gap-2">
<span class="font-mono text-xs text-gray-600">{{ key }}:</span>
<span class="text-xs">{{ formatValue(value) }}</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { AuditLogEntry } from '~/shared/types'
const props = defineProps<{ entry: AuditLogEntry }>()
const { t } = useI18n()
// Extrait les entrees au shape { old, new } pour les updates scalaires.
const updateDiff = computed<Record<string, { old: unknown; new: unknown }>>(() => {
const out: Record<string, { old: unknown; new: unknown }> = {}
for (const [key, value] of Object.entries(props.entry.changes)) {
if (value && typeof value === 'object' && 'old' in value && 'new' in value) {
out[key] = value as { old: unknown; new: unknown }
}
}
return out
})
// Extrait les entrees au shape { added, removed } pour les modifications
// de collections to-many (cf. AuditListener::captureCollectionChange).
const collectionDiff = computed<Record<string, { added: unknown[]; removed: unknown[] }>>(() => {
const out: Record<string, { added: unknown[]; removed: unknown[] }> = {}
for (const [key, value] of Object.entries(props.entry.changes)) {
if (value && typeof value === 'object' && 'added' in value && 'removed' in value) {
const diff = value as { added: unknown; removed: unknown }
out[key] = {
added: Array.isArray(diff.added) ? diff.added : [],
removed: Array.isArray(diff.removed) ? diff.removed : [],
}
}
}
return out
})
function formatValue(value: unknown): string {
if (value === null || value === undefined) return '∅'
// Passe par i18n plutot qu'un hardcode FR : si une autre locale est
// ajoutee, le rendu s'adapte sans nouvelle passe sur ce composant.
if (typeof value === 'boolean') return value ? t('common.yes') : t('common.no')
if (typeof value === 'object') return JSON.stringify(value)
return String(value)
}
</script>