✨ feat(security): Ajoute l'authentification à deux facteurs (2FA) avec Google Authenticator.
```
182 lines
14 KiB
Twig
182 lines
14 KiB
Twig
{% extends 'dashboard/base.twig' %}
|
|
|
|
{% block title %}Traçabilité des actions{% endblock %}
|
|
|
|
{# HEADER : ACTIONS GLOBALES #}
|
|
{% block actions %}
|
|
<div class="flex items-center space-x-3">
|
|
{# Bouton Exporter XLSX #}
|
|
<a href="{{ path('app_crm_audit_logs', {extract: true}) }}" target="_blank" class="flex items-center space-x-2 px-5 py-2.5 bg-emerald-600 hover:bg-emerald-700 text-white text-sm font-bold rounded-xl transition-all shadow-lg shadow-emerald-500/20 group">
|
|
<svg class="w-5 h-5 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
<span>Exporter XLSX</span>
|
|
</a>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block body %}
|
|
<div class="w-full bg-white dark:bg-[#1e293b] rounded-2xl border border-slate-200 dark:border-slate-800 shadow-sm overflow-hidden">
|
|
|
|
{# STATISTIQUES #}
|
|
<div class="px-8 py-6 border-b border-slate-100 dark:border-slate-800 flex items-center justify-between bg-slate-50/30 dark:bg-slate-800/30">
|
|
<div>
|
|
<h2 class="text-xl font-bold text-slate-800 dark:text-white tracking-tight">Journal d'Audit</h2>
|
|
<p class="text-xs text-slate-500 dark:text-slate-400 mt-1">Historique sécurisé par signature cryptographique</p>
|
|
</div>
|
|
<div class="flex items-center">
|
|
<span class="px-4 py-1.5 bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 text-[10px] font-black uppercase rounded-lg border border-blue-100 dark:border-blue-800/50">
|
|
{{ logs.getTotalItemCount }} ENREGISTREMENTS
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="overflow-x-auto custom-scrollbar">
|
|
<table class="w-full text-left border-collapse">
|
|
<thead>
|
|
<tr class="bg-slate-50/50 dark:bg-slate-900/40 border-b border-slate-100 dark:border-slate-800">
|
|
<th class="px-8 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-[0.2em]">Horodatage</th>
|
|
<th class="px-8 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-[0.2em]">Administrateur & Appareil</th>
|
|
<th class="px-8 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-[0.2em] text-center">Action</th>
|
|
<th class="px-8 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-[0.2em] text-center">Intégrité</th>
|
|
<th class="px-8 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-[0.2em]">Détails</th>
|
|
|
|
{# COLONNE ACTIONS VISIBLE UNIQUEMENT POUR ROOT #}
|
|
{% if is_granted('ROLE_ROOT') %}
|
|
<th class="px-8 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-[0.2em] text-right">Actions</th>
|
|
{% endif %}
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-100 dark:divide-slate-800">
|
|
{% for log in logs %}
|
|
<tr class="hover:bg-slate-50/50 dark:hover:bg-slate-800/30 transition-colors duration-150">
|
|
|
|
{# 1. DATE #}
|
|
<td class="px-8 py-4 whitespace-nowrap align-top">
|
|
<div class="text-sm font-bold text-slate-700 dark:text-slate-200">{{ log.actionAt|date('d/m/Y') }}</div>
|
|
<div class="text-[10px] text-slate-400 font-mono mt-0.5 tracking-wider">{{ log.actionAt|date('H:i:s') }}</div>
|
|
</td>
|
|
|
|
{# 2. ADMIN & UA #}
|
|
<td class="px-8 py-4 whitespace-nowrap align-top">
|
|
<div class="flex items-start">
|
|
<div class="h-10 w-10 rounded-xl bg-gradient-to-br from-blue-500 to-indigo-600 flex flex-shrink-0 items-center justify-center text-white font-bold text-xs shadow-md mt-0.5">
|
|
{{ log.account.firstName|first|upper }}{{ log.account.name|first|upper }}
|
|
</div>
|
|
<div class="ml-4">
|
|
<div class="text-sm font-bold text-slate-800 dark:text-white flex items-center mb-0.5">
|
|
{{ log.account.firstName }} {{ log.account.name }}
|
|
{% if 'ROLE_ROOT' in log.account.roles %}
|
|
<span class="ml-2 px-1.5 py-0.5 rounded text-[8px] bg-red-600/10 text-red-600 dark:bg-red-500/20 dark:text-red-400 font-black uppercase border border-red-200 dark:border-red-900/50">Root</span>
|
|
{% endif %}
|
|
</div>
|
|
<div class="text-[11px] text-slate-400 dark:text-slate-500 mb-2">{{ log.account.email }}</div>
|
|
|
|
{% if log.userAgent %}
|
|
{% set ua = log.userAgent|lower %}
|
|
<div class="flex items-center text-[10px] text-slate-500 bg-slate-100/50 dark:bg-slate-900/50 px-2 py-1.5 rounded-lg border border-slate-200/50 max-w-[260px]">
|
|
<span class="mr-2">
|
|
{% if 'firefox' in ua %}
|
|
<svg class="w-3.5 h-3.5 text-orange-500" fill="currentColor" viewBox="0 0 24 24"><path d="M23.9 12c0 6.6-5.4 12-11.9 12C5.4 24 0 18.6 0 12S5.4 0 12 0c6.5 0 11.9 5.4 11.9 12zM10.8 4.7c-.5.1-1.3.4-1.3.4s.8-.2 1.3-.3c1.5-.4 3.1-.2 4.4.6 1.3.7 2.2 2 2.5 3.4.1.7.1 1.5-.1 2.2-.2 1-1.2 2.2-1.2 2.2s.8-.9 1-1.8c.3-1.4-.1-2.9-1.2-4-1.1-1.1-2.6-1.6-4.2-1.4-1.4.1-2.1.8-2.6 1.4-.5.6-.7 1.4-.6 2.1.1.7.5 1.4 1.1 1.8.6.4 1.3.5 2 .4.7-.1 1.3-.5 1.7-1.1.4-.6.5-1.3.3-2-.1-.7-.5-1.3-1-1.7s-1.2-.5-1.9-.4c-.7.1-1.3.5-1.7 1.1-.3.5-.4 1.1-.3 1.7.1.5.3.9.7 1.2.4.3.8.4 1.3.3.5-.1.9-.3 1.2-.7.2-.4.3-.8.2-1.3-.1-.4-.3-.7-.6-.9-.3-.2-.6-.3-1-.2-.3 0-.6.1-.8.4-.2.2-.3.5-.2.8.1.3.3.5.6.6.3.1.6 0 .8-.2.2-.2.3-.4.2-.7 0-.3-.2-.5-.5-.6-.2-.1-.5 0-.7.2s-.3.4-.2.7z"/></svg>
|
|
{% elseif 'chrome' in ua %}
|
|
<svg class="w-3.5 h-3.5 text-blue-400" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0C8.21 0 4.83 1.75 2.64 4.5l3.96 6.84A6.004 6.004 0 0 1 12 6h10.36A12.012 12.012 0 0 0 12 0zm-1.04 13.5l-5.12-8.88A11.936 11.936 0 0 0 0 12c0 6.07 4.51 11.08 10.36 11.92l3.96-6.84a6.012 6.012 0 0 1-3.36-3.58zm12.4-7.5H12a6.002 6.002 0 0 1 3.36 9.42l-5.12 8.88C10.74 23.9 11.36 24 12 24c6.63 0 12-5.37 12-12 0-2.12-.55-4.12-1.52-5.88z"/></svg>
|
|
{% else %}
|
|
<svg class="w-3.5 h-3.5 text-slate-400" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/></svg>
|
|
{% endif %}
|
|
</span>
|
|
<span class="truncate opacity-80" title="{{ log.userAgent }}">{{ log.userAgent }}</span>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
|
|
{# 3. TYPE D'ACTION TRADUIT #}
|
|
{# 3. TYPE D'ACTION TRADUIT ET STYLISÉ #}
|
|
<td class="px-8 py-4 whitespace-nowrap text-center align-top">
|
|
{% set typeMapping = {
|
|
'CREATE': { 'label': 'CRÉATION', 'style': 'bg-emerald-500/10 text-emerald-600 border-emerald-500/20' },
|
|
'DELETE': { 'label': 'SUPPRESSION', 'style': 'bg-rose-500/10 text-rose-600 border-rose-500/20' },
|
|
'UPDATE': { 'label': 'MODIFICATION', 'style': 'bg-blue-500/10 text-blue-600 border-blue-500/20' },
|
|
'AUTH': { 'label': 'CONNEXION', 'style': 'bg-amber-500/10 text-amber-600 border-amber-500/20' },
|
|
'VIEW': { 'label': 'CONSULTATION', 'style': 'bg-sky-500/10 text-sky-600 border-sky-500/20' },
|
|
'SECURITY_ALERT': { 'label': 'ALERTE SÉCURITÉ', 'style': 'bg-orange-500/10 text-orange-600 border-orange-500/20' },
|
|
'SECURITY_CRITICAL': { 'label': 'CRITIQUE', 'style': 'bg-red-600 text-white border-red-700 animate-pulse font-black' },
|
|
'2FA_INVITE': { 'label': 'INVITATION 2FA', 'style': 'bg-indigo-500/10 text-indigo-500 border-indigo-500/20' },
|
|
'2FA_DISABLED': { 'label': '2FA DÉSACTIVÉE', 'style': 'bg-rose-500/10 text-rose-600 border-rose-500/20 font-bold' },
|
|
'UPDATE_STATUS': { 'label': 'STATUT COMPTE', 'style': 'bg-slate-500/10 text-slate-500 border-slate-500/20' },
|
|
'UPDATE_ROLES': { 'label': 'DROITS ACCÈS', 'style': 'bg-purple-500/10 text-purple-600 border-purple-500/20' }
|
|
} %}
|
|
|
|
{# On récupère la configuration ou on utilise une valeur par défaut #}
|
|
{% set config = typeMapping[log.type] ?? { 'label': log.type|replace({'_': ' '}), 'style': 'bg-slate-500/10 text-slate-500 border-slate-500/20' } %}
|
|
|
|
<span class="px-3 py-1.5 rounded-lg text-[10px] font-black border uppercase tracking-widest {{ config.style }}">
|
|
{{ config.label }}
|
|
</span>
|
|
</td>
|
|
{# 4. INTÉGRITÉ (HASH CHECK) #}
|
|
<td class="px-8 py-4 whitespace-nowrap align-top text-center">
|
|
{% if log.hashCode == log.generateSignature %}
|
|
<div class="inline-flex flex-col items-center group cursor-help" title="Signature valide">
|
|
<div class="h-8 w-8 rounded-lg bg-emerald-500/10 flex items-center justify-center text-emerald-600 border border-emerald-500/20">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path d="M5 13l4 4L19 7"/></svg>
|
|
</div>
|
|
<span class="text-[8px] font-black uppercase text-emerald-600 mt-1">Valide</span>
|
|
</div>
|
|
{% else %}
|
|
<div class="inline-flex flex-col items-center animate-pulse cursor-help" title="Données altérées !">
|
|
<div class="h-8 w-8 rounded-lg bg-rose-500/10 flex items-center justify-center text-rose-600 border border-rose-500/20 shadow-lg shadow-rose-500/10">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
|
|
</div>
|
|
<span class="text-[8px] font-black uppercase text-rose-600 mt-1">Altéré</span>
|
|
</div>
|
|
{% endif %}
|
|
</td>
|
|
|
|
{# 5. DÉTAILS #}
|
|
<td class="px-8 py-4 align-top">
|
|
<div class="text-sm text-slate-600 dark:text-slate-300 font-medium leading-relaxed max-w-xl">{{ log.message }}</div>
|
|
<div class="mt-2 flex items-center">
|
|
<code class="text-[10px] bg-slate-100 dark:bg-slate-900 px-2 py-0.5 rounded text-blue-500 font-mono">{{ log.path }}</code>
|
|
</div>
|
|
</td>
|
|
|
|
{# 6. CELLULE ACTIONS VISIBLE UNIQUEMENT POUR ROOT #}
|
|
{% if is_granted('ROLE_ROOT') %}
|
|
<td class="px-8 py-4 whitespace-nowrap text-right align-top">
|
|
<form action="{{ path('app_crm_audit_logs_delete', {id: log.id}) }}" method="POST" class="inline-block" onsubmit="return confirm('Supprimer ce log précis ?');">
|
|
<input type="hidden" name="_token" value="{{ csrf_token('delete' ~ log.id) }}">
|
|
<button type="submit" class="p-2 text-slate-400 hover:text-rose-600 hover:bg-rose-50 dark:hover:bg-rose-900/10 rounded-lg transition-all">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
|
</button>
|
|
</form>
|
|
</td>
|
|
{% endif %}
|
|
</tr>
|
|
{% else %}
|
|
<tr>
|
|
{# On ajuste le colspan dynamiquement #}
|
|
<td colspan="{{ is_granted('ROLE_ROOT') ? 6 : 5 }}" class="px-8 py-12 text-center italic text-slate-400">
|
|
Aucun enregistrement.
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{# PAGINATION #}
|
|
<div class="px-8 py-6 bg-slate-50/50 dark:bg-slate-900/40 border-t border-slate-100 dark:border-slate-800">
|
|
<div class="flex flex-col md:flex-row items-center justify-between gap-6">
|
|
<div class="text-xs font-semibold text-slate-400 uppercase tracking-widest">
|
|
Page {{ logs.getCurrentPageNumber }} — {{ logs|length }} logs affichés
|
|
</div>
|
|
<div class="navigation shadow-sm rounded-xl overflow-hidden">
|
|
{{ knp_pagination_render(logs) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|