✨ feat(all): Ajoute l'attribut `data-turbo="false"` sur les liens. 🐛 fix(security): Corrige le chemin d'accès de l'espace client. ```
204 lines
15 KiB
Twig
204 lines
15 KiB
Twig
{% extends 'dashboard/base.twig' %}
|
|
|
|
{% block title %}Traçabilité des actions{% endblock %}
|
|
{% block title_header %}Journal d' <span class="text-blue-500">Audit</span>{% endblock %}
|
|
|
|
{# HEADER : ACTIONS GLOBALES #}
|
|
{% block actions %}
|
|
<div class="flex items-center space-x-3">
|
|
{# Bouton Exporter XLSX #}
|
|
<a data-turbo="false" href="{{ path('app_crm_audit_logs', {extract: true, account: app.request.query.get('account')}) }}" 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="space-y-6">
|
|
|
|
{# BARRE DE FILTRES #}
|
|
{# BARRE DE FILTRES #}
|
|
<div class="backdrop-blur-xl bg-white/5 border border-white/5 p-6 rounded-[2rem] shadow-sm mb-8">
|
|
<form method="GET" action="{{ path('app_crm_audit_logs') }}" class="flex flex-col md:flex-row items-end gap-5">
|
|
|
|
{# 1. SELECT COMPTE #}
|
|
<div class="flex-1 min-w-[240px]">
|
|
<label for="account" class="block text-[10px] font-black text-slate-500 uppercase tracking-[0.2em] mb-2 ml-2">Filtrer par compte</label>
|
|
<div class="relative">
|
|
<select name="account" id="account" class="w-full bg-slate-900/50 border border-white/10 rounded-xl px-4 py-3.5 text-sm text-white focus:border-blue-500 focus:ring-0 outline-none transition-all appearance-none cursor-pointer">
|
|
<option value="">Tous les utilisateurs</option>
|
|
{% for u in users_list %}
|
|
<option value="{{ u.id }}" {{ app.request.query.get('account') == u.id ? 'selected' : '' }}>
|
|
{{ u.firstName }} {{ u.name }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
<div class="absolute inset-y-0 right-4 flex items-center pointer-events-none text-slate-500">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{# 2. BOUTON SUBMIT (APPLIQUER) #}
|
|
<div class="w-full md:w-auto">
|
|
<button type="submit" class="w-full md:w-auto flex items-center justify-center space-x-2 px-8 py-3.5 bg-blue-600 hover:bg-blue-500 text-white rounded-xl shadow-lg shadow-blue-600/20 transition-all duration-300 group">
|
|
<svg class="w-4 h-4 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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
|
</svg>
|
|
<span class="text-[10px] font-black uppercase tracking-[0.2em]">Filtrer</span>
|
|
</button>
|
|
</div>
|
|
|
|
{# 3. BOUTON RESET (Si un filtre est actif) #}
|
|
{% if app.request.query.get('account') %}
|
|
<div class="w-full md:w-auto">
|
|
<a data-turbo="false" href="{{ path('app_crm_audit_logs') }}" class="flex items-center justify-center px-4 py-3.5 text-slate-400 hover:text-rose-500 text-[10px] font-black uppercase tracking-widest transition-colors group">
|
|
<svg class="w-4 h-4 mr-2 group-hover:rotate-90 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
</svg>
|
|
Réinitialiser
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
|
|
</form>
|
|
</div>
|
|
{# TABLEAU DES LOGS #}
|
|
<div class="backdrop-blur-xl bg-[#1e293b]/40 border border-white/5 rounded-[2.5rem] shadow-2xl overflow-hidden">
|
|
|
|
{# HEADER TABLEAU #}
|
|
<div class="px-8 py-6 border-b border-white/5 flex items-center justify-between bg-white/5">
|
|
<div>
|
|
<h2 class="text-xl font-bold text-white tracking-tight">Historique d'activité</h2>
|
|
<p class="text-[10px] text-slate-500 font-bold uppercase tracking-widest mt-1">Sécurisé par signature cryptographique SHA-256</p>
|
|
</div>
|
|
<div class="flex items-center">
|
|
<span class="px-4 py-1.5 bg-blue-500/10 text-blue-400 text-[10px] font-black uppercase rounded-lg border border-blue-500/20">
|
|
{{ 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-900/40 border-b border-white/5">
|
|
<th class="px-8 py-5 text-[11px] font-black text-slate-500 uppercase tracking-[0.2em]">Horodatage</th>
|
|
<th class="px-8 py-5 text-[11px] font-black text-slate-500 uppercase tracking-[0.2em]">Agent & Session</th>
|
|
<th class="px-8 py-5 text-[11px] font-black text-slate-500 uppercase tracking-[0.2em] text-center">Action</th>
|
|
<th class="px-8 py-5 text-[11px] font-black text-slate-500 uppercase tracking-[0.2em] text-center">Intégrité</th>
|
|
<th class="px-8 py-5 text-[11px] font-black text-slate-500 uppercase tracking-[0.2em]">Détails</th>
|
|
{% if is_granted('ROLE_ROOT') %}
|
|
<th class="px-8 py-5 text-[11px] font-black text-slate-500 uppercase tracking-[0.2em] text-right">Admin</th>
|
|
{% endif %}
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-white/5">
|
|
{% for log in logs %}
|
|
<tr class="hover:bg-white/5 transition-colors duration-150 group">
|
|
{# 1. DATE #}
|
|
<td class="px-8 py-6 whitespace-nowrap align-top">
|
|
<div class="text-sm font-bold text-white">{{ log.actionAt|date('d/m/Y') }}</div>
|
|
<div class="text-[10px] text-slate-500 font-mono mt-0.5 tracking-wider">{{ log.actionAt|date('H:i:s') }}</div>
|
|
</td>
|
|
|
|
{# 2. ADMIN & UA #}
|
|
<td class="px-8 py-6 whitespace-nowrap align-top">
|
|
<div class="flex items-start">
|
|
<div class="h-10 w-10 rounded-xl bg-blue-600 flex flex-shrink-0 items-center justify-center text-white font-black text-xs shadow-lg shadow-blue-600/20 group-hover:scale-110 transition-transform">
|
|
{{ log.account.firstName|first|upper }}{{ log.account.name|first|upper }}
|
|
</div>
|
|
<div class="ml-4">
|
|
<div class="text-sm font-bold 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-rose-500/10 text-rose-500 font-black uppercase border border-rose-500/20">Root</span>
|
|
{% endif %}
|
|
</div>
|
|
<div class="text-[10px] text-slate-500 font-medium truncate max-w-[180px]">{{ log.userAgent|default('Agent inconnu') }}</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
|
|
{# 3. TYPE D'ACTION (Mapping INFO inclus) #}
|
|
<td class="px-8 py-6 whitespace-nowrap text-center align-top">
|
|
{% set typeMapping = {
|
|
'CREATE': { 'label': 'CRÉATION', 'style': 'bg-emerald-500/10 text-emerald-500 border-emerald-500/20' },
|
|
'DELETE': { 'label': 'SUPPRESSION', 'style': 'bg-rose-500/10 text-rose-500 border-rose-500/20' },
|
|
'UPDATE': { 'label': 'MODIFICATION', 'style': 'bg-blue-500/10 text-blue-500 border-blue-500/20' },
|
|
'AUTH': { 'label': 'CONNEXION', 'style': 'bg-amber-500/10 text-amber-500 border-amber-500/20' },
|
|
'VIEW': { 'label': 'CONSULTATION', 'style': 'bg-sky-500/10 text-sky-500 border-sky-500/20' },
|
|
'INFO': { 'label': 'INFO', 'style': 'bg-slate-500/10 text-slate-400 border-slate-500/20' },
|
|
'SECURITY_ALERT': { 'label': 'ALERTE', 'style': 'bg-orange-500/10 text-orange-500 border-orange-500/20' },
|
|
'SECURITY_CRITICAL': { 'label': 'CRITIQUE', 'style': 'bg-red-600 text-white border-red-700 animate-pulse font-black' },
|
|
'2FA_INVITE': { 'label': '2FA INVITE', 'style': 'bg-indigo-500/10 text-indigo-400 border-indigo-500/20' },
|
|
'2FA_DISABLED': { 'label': '2FA OFF', 'style': 'bg-rose-500/10 text-rose-500 border-rose-500/20 font-bold' }
|
|
} %}
|
|
{% set config = typeMapping[log.type] ?? { 'label': log.type, 'style': 'bg-slate-500/10 text-slate-500 border-white/10' } %}
|
|
<span class="px-3 py-1.5 rounded-lg text-[9px] font-black border uppercase tracking-[0.15em] {{ config.style }}">
|
|
{{ config.label }}
|
|
</span>
|
|
</td>
|
|
|
|
{# 4. INTÉGRITÉ #}
|
|
<td class="px-8 py-6 whitespace-nowrap align-top text-center">
|
|
{% if log.hashCode == log.generateSignature %}
|
|
<div class="inline-flex flex-col items-center text-emerald-500" title="Signature Valide">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path d="M5 13l4 4L19 7"/></svg>
|
|
<span class="text-[7px] font-black uppercase mt-1">Scellé</span>
|
|
</div>
|
|
{% else %}
|
|
<div class="inline-flex flex-col items-center text-rose-500 animate-bounce" title="Données corrompues !">
|
|
<svg class="w-5 h-5" 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>
|
|
<span class="text-[7px] font-black uppercase mt-1">Altéré</span>
|
|
</div>
|
|
{% endif %}
|
|
</td>
|
|
|
|
{# 5. DÉTAILS #}
|
|
<td class="px-8 py-6 align-top">
|
|
<div class="text-sm text-slate-300 font-medium leading-relaxed max-w-lg mb-2">{{ log.message }}</div>
|
|
<code class="text-[9px] bg-black/40 px-2 py-1 rounded text-blue-400 font-mono border border-white/5">{{ log.path }}</code>
|
|
</td>
|
|
|
|
{# 6. ACTIONS ROOT #}
|
|
{% if is_granted('ROLE_ROOT') %}
|
|
<td class="px-8 py-6 whitespace-nowrap text-right align-top">
|
|
<form action="{{ path('app_crm_audit_logs_delete', {id: log.id}) }}" method="POST" onsubmit="return confirm('Confirmer la suppression définitive de cet enregistrement ?');">
|
|
<input type="hidden" name="_token" value="{{ csrf_token('delete' ~ log.id) }}">
|
|
<button type="submit" class="p-2 text-slate-500 hover:text-rose-500 hover:bg-rose-500/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>
|
|
<td colspan="6" class="px-8 py-20 text-center italic text-slate-500 font-medium">
|
|
Aucun enregistrement trouvé dans le journal d'audit.
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{# PAGINATION #}
|
|
<div class="px-8 py-8 bg-black/20 border-t border-white/5">
|
|
<div class="flex flex-col md:flex-row items-center justify-between gap-6">
|
|
<div class="text-[10px] font-black text-slate-500 uppercase tracking-[0.2em]">
|
|
Page {{ logs.getCurrentPageNumber }} sur {{ (logs.getTotalItemCount / 25)|round(0, 'ceil') }}
|
|
</div>
|
|
<div class="navigation custom-pagination">
|
|
{{ knp_pagination_render(logs) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|