feat(caddy): Améliore la sécurité avec CSP et headers standards

 feat(templates): Met à jour le logo sur la page d'inscription réussie

 feat(knp_paginator): Ajoute la configuration pour le style Tailwind

 feat(audit_logs): Crée la page de traçabilité des actions

 feat(logs): Ajoute le contrôleur pour gérer les logs d'audit

 feat(AppLogger): Enregistre l'user agent dans les logs d'audit

 feat(AccountController): Supprime l'appel inutile de l'EventAdminCreate

 feat(AuditLogRepository): Récupère les logs en excluant les ROOT

 feat(base): Ajoute la structure de base pour le dashboard
```
This commit is contained in:
Serreau Jovann
2026-01-15 20:08:04 +01:00
parent 98937f9164
commit aba456e5ca
13 changed files with 414 additions and 115 deletions

View File

@@ -3,110 +3,123 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Administration{% endblock %} - LudikEvent</title>
<title>{% block title %}Administration{% endblock %} — Intranet Ludikevent</title>
{{ vite_asset('admin.js', {}) }}
<style>
.custom-scrollbar::-webkit-scrollbar { width: 5px; height: 5px; }
.custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
.custom-scrollbar::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
.dark .custom-scrollbar::-webkit-scrollbar-thumb { background: #334155; }
.page-transition { animation: fadeIn 0.3s ease-out; }
@keyframes fadeIn {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
</style>
</head>
<body class="bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100 antialiased">
<body class="bg-slate-50 dark:bg-[#0f172a] text-slate-900 dark:text-slate-200 antialiased overflow-hidden font-sans">
<div class="flex h-screen overflow-hidden">
{# Overlay pour mobile #}
<div id="sidebar-overlay" class="fixed inset-0 z-20 bg-black/50 lg:hidden hidden"></div>
{# SIDEBAR MODERNE #}
<aside id="sidebar" class="fixed inset-y-0 left-0 z-40 w-72 bg-white dark:bg-[#1e293b] border-r border-slate-200 dark:border-slate-800 transform -translate-x-full lg:translate-x-0 transition-all duration-300 ease-in-out shadow-xl lg:shadow-none">
{# SIDEBAR #}
<aside id="sidebar" class="fixed inset-y-0 left-0 z-30 w-64 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 transform -translate-x-full lg:translate-x-0 transition-transform duration-300 ease-in-out">
<div class="flex items-center justify-between px-6 h-16 border-b border-gray-200 dark:border-gray-700">
<span class="text-xl font-bold bg-gradient-to-r from-blue-600 to-indigo-500 bg-clip-text text-transparent">LudikEvent CRM</span>
<div class="flex items-center px-8 h-20 border-b border-slate-100 dark:border-slate-800">
<div class="flex items-center space-x-3">
<div class="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center shadow-lg shadow-blue-500/30">
<span class="text-white font-bold text-xl font-serif">L</span>
</div>
<span class="text-lg font-bold tracking-tight text-slate-800 dark:text-white uppercase italic">Intranet <span class="text-blue-600 not-italic">Ludikevent</span></span>
</div>
</div>
<nav class="flex flex-col p-4 space-y-1 h-[calc(100vh-64px)] overflow-y-auto">
{% macro nav_link(path, label, icon_svg, current_route) %}
{% set isActive = app.request.get('_route') == current_route %}
<a href="{{ path }}" class="flex items-center space-x-3 p-3 rounded-lg transition-all duration-200 {{ isActive ? 'bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400' : 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">{{ icon_svg|raw }}</svg>
<span class="font-medium text-sm">{{ label }}</span>
</a>
{% endmacro %}
{% import _self as menu %}
{{ menu.nav_link(path('app_crm'), 'Tableau de bord', '<path d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"></path>', 'app_crm') }}
{{ menu.nav_link('#', 'Clients', '<path d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path>', 'app_clients') }}
{{ menu.nav_link('#', 'Articles', '<path d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"></path>', 'app_articles') }}
{# Groupe Paramètres #}
<div class="pt-2">
{% set isAdminRoute = app.request.get('_route') matches '/^app_crm_administrateur/' %}
<button id="settings-toggle" class="w-full flex items-center justify-between p-3 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition duration-150">
<div class="flex items-center space-x-3">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37a1.724 1.724 0 002.572-1.065z"></path></svg>
<span class="font-medium text-sm">Paramètres</span>
</div>
<svg id="settings-chevron" class="w-4 h-4 transition-transform duration-200 {{ isAdminRoute ? 'rotate-180' }}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M19 9l-7 7-7-7"></path></svg>
</button>
<ul id="settings-submenu" class="ml-9 mt-1 space-y-1 {{ isAdminRoute ? '' : 'hidden' }}">
<li>
<a href="{{ path('app_crm_administrateur') }}" class="block p-2 rounded-lg text-sm {{ isAdminRoute ? 'text-blue-600 font-bold dark:text-blue-400' : 'text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white' }}">
Administrateurs
<nav class="flex flex-col p-6 space-y-8 h-[calc(100vh-80px)] overflow-y-auto custom-scrollbar">
<div>
<p class="px-4 mb-4 text-[10px] font-semibold text-slate-400 dark:text-slate-500 uppercase tracking-[0.2em]">Menu Principal</p>
<div class="space-y-1">
{% macro nav_link(path, label, icon_svg, current_route) %}
{% set isActive = app.request.get('_route') == current_route %}
<a href="{{ path }}" class="flex items-center space-x-3 px-4 py-3 rounded-xl transition-all duration-200 group {{ isActive ? 'bg-blue-600 text-white shadow-lg shadow-blue-500/30' : 'hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-600 dark:text-slate-400' }}">
<svg class="w-5 h-5 {{ isActive ? 'text-white' : 'text-slate-400 group-hover:text-blue-500' }}" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">{{ icon_svg|raw }}</svg>
<span class="font-semibold text-sm">{{ label }}</span>
</a>
</li>
<li>
<a href="#" class="block p-2 rounded-lg text-sm text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white">Services</a>
</li>
</ul>
{% endmacro %}
{% import _self as menu %}
{{ menu.nav_link(path('app_crm'), 'Dashboard', '<path d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>', 'app_crm') }}
{{ menu.nav_link('#', 'Clients', '<path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>', 'app_clients') }}
</div>
</div>
<div>
<p class="px-4 mb-4 text-[10px] font-semibold text-slate-400 dark:text-slate-500 uppercase tracking-[0.2em]">Configuration</p>
<div class="space-y-1">
{% set isAdminActive = app.request.get('_route') matches '/^app_crm_administrateur/' %}
{% set isLogsActive = app.request.get('_route') == 'app_crm_audit_logs' %}
{% set isOpen = isAdminActive or isLogsActive %}
<button id="settings-toggle" class="w-full flex items-center justify-between px-4 py-3 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-600 dark:text-slate-400 transition-all duration-200 group">
<div class="flex items-center space-x-3">
<svg class="w-5 h-5 text-slate-400 group-hover:text-blue-500" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37a1.724 1.724 0 002.572-1.065z"></path></svg>
<span class="font-semibold text-sm">Paramètres</span>
</div>
<svg class="w-4 h-4 transition-transform duration-300 {{ isOpen ? 'rotate-180' }}" 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"></path></svg>
</button>
<div id="settings-submenu" class="mt-2 space-y-1 overflow-hidden transition-all duration-300 {{ isOpen ? 'max-h-40' : 'max-h-0' }}">
<a href="{{ path('app_crm_administrateur') }}" class="block px-12 py-2 text-sm {{ isAdminActive ? 'text-blue-600 font-bold' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}">Gestion Admins</a>
<a href="{{ path('app_crm_audit_logs') }}" class="block px-12 py-2 text-sm {{ isLogsActive ? 'text-blue-600 font-bold' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}">Traçabilité (Logs)</a>
</div>
</div>
</div>
</nav>
</aside>
{# CONTENU PRINCIPAL #}
<main class="flex-1 flex flex-col min-w-0 lg:ml-64 bg-gray-50 dark:bg-gray-900">
{# CONTENU FULL WIDTH #}
<main class="flex-1 flex flex-col min-w-0 lg:ml-72 bg-slate-50 dark:bg-[#0f172a] h-screen overflow-y-auto custom-scrollbar relative">
{# Header #}
<header class="h-16 flex items-center justify-between px-6 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 sticky top-0 z-20">
<button id="sidebar-toggle" class="lg:hidden text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-700 p-2 rounded-md transition-colors">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h16"></path></svg>
{# Header Glassmorphism #}
<header class="h-20 flex items-center justify-between px-8 bg-white/80 dark:bg-[#1e293b]/80 backdrop-blur-md border-b border-slate-200 dark:border-slate-800 sticky top-0 z-30">
<button id="sidebar-toggle" class="lg:hidden p-2 rounded-lg bg-slate-100 dark:bg-slate-800 text-slate-600">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7"></path></svg>
</button>
<div class="flex items-center space-x-4 ml-auto">
<div class="text-right hidden sm:block">
<p class="text-xs font-medium text-gray-900 dark:text-white">{{ app.user.username|default('Admin') }}</p>
<a href="{{ path('app_logout') }}" class="text-[10px] text-red-500 hover:underline">Déconnexion</a>
<div class="flex items-center space-x-6 ml-auto">
<div class="flex items-center space-x-3 px-4 py-2 bg-slate-100 dark:bg-slate-800 rounded-2xl border border-slate-200 dark:border-slate-700">
<div class="w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center text-blue-600 font-bold text-xs">
{{ app.user.firstName|first|upper }}
</div>
<div class="text-left hidden sm:block">
<p class="text-xs font-bold text-slate-800 dark:text-white">{{ app.user.firstName }} {{ app.user.name }}</p>
<a href="{{ path('app_logout') }}" class="text-[10px] text-red-500 font-semibold hover:text-red-600 uppercase tracking-tighter">Déconnexion</a>
</div>
</div>
</div>
</header>
{# Zone Flash Messages #}
<div id="flash-container" class="fixed top-20 right-6 z-50 flex flex-col gap-3 w-80">
{% for label, messages in app.flashes %}
{% for message in messages %}
<div class="flash-message transform transition-all duration-500 p-4 rounded-lg shadow-lg border flex items-center justify-between {{ label == 'success' ? 'bg-green-100 border-green-200 text-green-800 dark:bg-green-900/80 dark:text-green-300 dark:border-green-800' : 'bg-red-100 border-red-200 text-red-800 dark:bg-red-900/80 dark:text-red-300 dark:border-red-800' }}">
<p class="text-sm font-medium">{{ message }}</p>
<button type="button" onclick="this.parentElement.remove()" class="ml-4 opacity-50 hover:opacity-100">×</button>
</div>
{% endfor %}
{% endfor %}
</div>
{# Zone Contenu #}
<div class="p-6 md:p-8">
<div class="flex flex-col md:flex-row md:items-center justify-between mb-8 gap-4">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
{% block title_header %}{{ block('title') }}{% endblock %}
</h1>
{# Zone de contenu 100% #}
<div class="p-6 md:p-8 lg:p-10 page-transition w-full">
<div class="flex flex-col md:flex-row md:items-end justify-between mb-10 gap-4 border-b border-slate-200/60 dark:border-slate-800 pb-8">
<div>
<p class="text-blue-600 font-bold text-[10px] uppercase tracking-[0.4em] mb-2">Espace Administration</p>
<h1 class="text-4xl font-extrabold text-slate-900 dark:text-white tracking-tight">
{% block title_header %}{{ block('title') }}{% endblock %}
</h1>
</div>
<div class="flex items-center space-x-3">
{% block actions %}{% endblock %}
</div>
</div>
<div class="animate-fadeIn w-full">
<div class="w-full">
{% block body %}{% endblock %}
</div>
</div>
</main>
</div>
</body>
</html>