feat: systeme de logs d'activite admin avec HMAC + export PDF
src/Entity/AppLog.php (nouveau):
- id, user (ManyToOne nullable, SET NULL on delete), method (GET/POST/etc),
url (500 chars), route (nom de la route Symfony), action (description
lisible de l'action), ip (nullable), hmac (SHA-256), createdAt
- Index sur created_at pour les requetes paginées
- HMAC genere dans le constructeur avec payload:
method|url|route|action|ip|userId|createdAt (microsecondes)
- verifyHmac(): verification timing-safe avec hash_equals
- Aucun setter sur les champs (immutable apres creation)
src/Repository/AppLogRepository.php (nouveau):
- createPaginatedQueryBuilder(): ORDER BY createdAt DESC avec jointure user
src/Service/AppLoggerService.php (nouveau):
- Dictionnaire ROUTE_LABELS: 30+ routes admin avec descriptions
lisibles (ex: app_admin_clients_create → "Creation d'un client")
- log(): cree un AppLog avec l'action lisible, persiste et flush
- verifyLog(): verifie le HMAC d'un log
- Si la route n'est pas dans le dictionnaire, utilise "Acces a {route}"
- Ajoute "(soumission)" pour les POST
src/EventListener/AdminLogListener.php (nouveau):
- Ecoute KernelEvents::CONTROLLER avec priorite -10
- Intercepte toutes les requetes dont la route commence par app_admin_
- Ignore les requetes AJAX de recherche (evite le spam)
- Recupere l'utilisateur connecte via TokenStorage
- Appelle AppLoggerService::log() dans un try/catch
(ne bloque jamais la requete si le logging echoue)
src/Controller/Admin/LogsController.php (nouveau):
- Route /admin/logs, ROLE_ROOT
- index(): pagination KnpPaginator (20 par page), verifie le HMAC
de chaque log affiche
- pdf(): genere un PDF Dompdf avec toutes les infos du log
+ verification HMAC (CONFORME vert / ALTEREES rouge)
templates/admin/logs/index.html.twig (nouveau):
- Tableau glassmorphism: date, utilisateur, methode (badge colore),
action, URL (tronquee), IP, colonne HMAC (rond vert/rouge),
bouton PDF par ligne
- Pagination KnpPaginator en bas
templates/admin/logs/pdf.html.twig (nouveau):
- PDF A4 avec tableau d'informations du log
- Bloc HMAC avec fond vert "INTEGRITE VERIFIEE" ou rouge
"INTEGRITE COMPROMISE" + signature HMAC complete
- Footer avec mention SARL SITECONSEIL
templates/admin/_layout.html.twig:
- Ajout lien "Logs" dans la sidebar Super Admin avec icone document
migrations/Version20260402211054.php:
- Table app_log avec FK user_id, index sur created_at
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
35
migrations/Version20260402211054.php
Normal file
35
migrations/Version20260402211054.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20260402211054 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE TABLE app_log (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, method VARCHAR(10) NOT NULL, url VARCHAR(500) NOT NULL, route VARCHAR(255) NOT NULL, action TEXT NOT NULL, ip VARCHAR(50) DEFAULT NULL, hmac VARCHAR(128) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, user_id INT DEFAULT NULL, PRIMARY KEY (id))');
|
||||
$this->addSql('CREATE INDEX IDX_A14DDB02A76ED395 ON app_log (user_id)');
|
||||
$this->addSql('CREATE INDEX idx_applog_created ON app_log (created_at)');
|
||||
$this->addSql('ALTER TABLE app_log ADD CONSTRAINT FK_A14DDB02A76ED395 FOREIGN KEY (user_id) REFERENCES "user" (id) ON DELETE SET NULL NOT DEFERRABLE');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE app_log DROP CONSTRAINT FK_A14DDB02A76ED395');
|
||||
$this->addSql('DROP TABLE app_log');
|
||||
}
|
||||
}
|
||||
78
src/Controller/Admin/LogsController.php
Normal file
78
src/Controller/Admin/LogsController.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Admin;
|
||||
|
||||
use App\Entity\AppLog;
|
||||
use App\Repository\AppLogRepository;
|
||||
use App\Service\AppLoggerService;
|
||||
use Dompdf\Dompdf;
|
||||
use Knp\Component\Pager\PaginatorInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Twig\Environment;
|
||||
|
||||
#[Route('/admin/logs', name: 'app_admin_logs')]
|
||||
#[IsGranted('ROLE_ROOT')]
|
||||
class LogsController extends AbstractController
|
||||
{
|
||||
#[Route('', name: '')]
|
||||
public function index(
|
||||
Request $request,
|
||||
AppLogRepository $repository,
|
||||
PaginatorInterface $paginator,
|
||||
AppLoggerService $loggerService,
|
||||
): Response {
|
||||
$query = $repository->createPaginatedQueryBuilder();
|
||||
|
||||
$pagination = $paginator->paginate(
|
||||
$query,
|
||||
$request->query->getInt('page', 1),
|
||||
20,
|
||||
);
|
||||
|
||||
// Verifier le HMAC de chaque log
|
||||
$hmacResults = [];
|
||||
foreach ($pagination as $log) {
|
||||
$hmacResults[$log->getId()] = $loggerService->verifyLog($log);
|
||||
}
|
||||
|
||||
return $this->render('admin/logs/index.html.twig', [
|
||||
'pagination' => $pagination,
|
||||
'hmacResults' => $hmacResults,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/{id}/pdf', name: '_pdf', methods: ['GET'])]
|
||||
public function pdf(
|
||||
int $id,
|
||||
AppLogRepository $repository,
|
||||
AppLoggerService $loggerService,
|
||||
Environment $twig,
|
||||
): Response {
|
||||
$log = $repository->find($id);
|
||||
|
||||
if (null === $log) {
|
||||
throw $this->createNotFoundException('Log introuvable.');
|
||||
}
|
||||
|
||||
$hmacValid = $loggerService->verifyLog($log);
|
||||
|
||||
$html = $twig->render('admin/logs/pdf.html.twig', [
|
||||
'log' => $log,
|
||||
'hmacValid' => $hmacValid,
|
||||
]);
|
||||
|
||||
$dompdf = new Dompdf();
|
||||
$dompdf->loadHtml($html);
|
||||
$dompdf->setPaper('A4');
|
||||
$dompdf->render();
|
||||
|
||||
return new Response($dompdf->output(), 200, [
|
||||
'Content-Type' => 'application/pdf',
|
||||
'Content-Disposition' => 'inline; filename="log-'.$log->getId().'.pdf"',
|
||||
]);
|
||||
}
|
||||
}
|
||||
118
src/Entity/AppLog.php
Normal file
118
src/Entity/AppLog.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\AppLogRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: AppLogRepository::class)]
|
||||
#[ORM\Index(columns: ['created_at'], name: 'idx_applog_created')]
|
||||
class AppLog
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
|
||||
private ?User $user = null;
|
||||
|
||||
#[ORM\Column(length: 10)]
|
||||
private string $method;
|
||||
|
||||
#[ORM\Column(length: 500)]
|
||||
private string $url;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private string $route;
|
||||
|
||||
#[ORM\Column(type: 'text')]
|
||||
private string $action;
|
||||
|
||||
#[ORM\Column(length: 50, nullable: true)]
|
||||
private ?string $ip = null;
|
||||
|
||||
#[ORM\Column(length: 128)]
|
||||
private string $hmac;
|
||||
|
||||
#[ORM\Column]
|
||||
private \DateTimeImmutable $createdAt;
|
||||
|
||||
public function __construct(string $method, string $url, string $route, string $action, string $hmacSecret, ?User $user = null, ?string $ip = null)
|
||||
{
|
||||
$this->method = $method;
|
||||
$this->url = $url;
|
||||
$this->route = $route;
|
||||
$this->action = $action;
|
||||
$this->user = $user;
|
||||
$this->ip = $ip;
|
||||
$this->createdAt = new \DateTimeImmutable();
|
||||
$this->hmac = $this->generateHmac($hmacSecret);
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function getMethod(): string
|
||||
{
|
||||
return $this->method;
|
||||
}
|
||||
|
||||
public function getUrl(): string
|
||||
{
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
public function getRoute(): string
|
||||
{
|
||||
return $this->route;
|
||||
}
|
||||
|
||||
public function getAction(): string
|
||||
{
|
||||
return $this->action;
|
||||
}
|
||||
|
||||
public function getIp(): ?string
|
||||
{
|
||||
return $this->ip;
|
||||
}
|
||||
|
||||
public function getHmac(): string
|
||||
{
|
||||
return $this->hmac;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): \DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function verifyHmac(string $hmacSecret): bool
|
||||
{
|
||||
return hash_equals($this->hmac, $this->generateHmac($hmacSecret));
|
||||
}
|
||||
|
||||
private function generateHmac(string $secret): string
|
||||
{
|
||||
$payload = implode('|', [
|
||||
$this->method,
|
||||
$this->url,
|
||||
$this->route,
|
||||
$this->action,
|
||||
$this->ip ?? '',
|
||||
null !== $this->user ? (string) $this->user->getId() : '',
|
||||
$this->createdAt->format('Y-m-d\TH:i:s.u'),
|
||||
]);
|
||||
|
||||
return hash_hmac('sha256', $payload, $secret);
|
||||
}
|
||||
}
|
||||
57
src/EventListener/AdminLogListener.php
Normal file
57
src/EventListener/AdminLogListener.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\EventListener;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Service\AppLoggerService;
|
||||
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
|
||||
use Symfony\Component\HttpKernel\Event\ControllerEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
|
||||
#[AsEventListener(event: KernelEvents::CONTROLLER, priority: -10)]
|
||||
class AdminLogListener
|
||||
{
|
||||
public function __construct(
|
||||
private AppLoggerService $logger,
|
||||
private TokenStorageInterface $tokenStorage,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(ControllerEvent $event): void
|
||||
{
|
||||
if (!$event->isMainRequest()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$request = $event->getRequest();
|
||||
$route = $request->attributes->get('_route', '');
|
||||
|
||||
if (!str_starts_with($route, 'app_admin_')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignorer les requetes AJAX de recherche pour eviter le spam de logs
|
||||
if ($request->isXmlHttpRequest() && str_contains($route, 'search')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user = null;
|
||||
$token = $this->tokenStorage->getToken();
|
||||
if (null !== $token && $token->getUser() instanceof User) {
|
||||
$user = $token->getUser();
|
||||
}
|
||||
|
||||
try {
|
||||
$this->logger->log(
|
||||
$request->getMethod(),
|
||||
$request->getRequestUri(),
|
||||
$route,
|
||||
$user,
|
||||
$request->getClientIp(),
|
||||
);
|
||||
} catch (\Throwable) {
|
||||
// Ne jamais bloquer la requete si le logging echoue
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/Repository/AppLogRepository.php
Normal file
27
src/Repository/AppLogRepository.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\AppLog;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<AppLog>
|
||||
*/
|
||||
class AppLogRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, AppLog::class);
|
||||
}
|
||||
|
||||
public function createPaginatedQueryBuilder(): QueryBuilder
|
||||
{
|
||||
return $this->createQueryBuilder('l')
|
||||
->leftJoin('l.user', 'u')
|
||||
->addSelect('u')
|
||||
->orderBy('l.createdAt', 'DESC');
|
||||
}
|
||||
}
|
||||
73
src/Service/AppLoggerService.php
Normal file
73
src/Service/AppLoggerService.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Entity\AppLog;
|
||||
use App\Entity\User;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
|
||||
class AppLoggerService
|
||||
{
|
||||
/** @var array<string, string> Routes admin → descriptions lisibles */
|
||||
private const ROUTE_LABELS = [
|
||||
'app_admin_dashboard' => 'Consultation du tableau de bord',
|
||||
'app_admin_clients_index' => 'Consultation de la liste des clients',
|
||||
'app_admin_clients_create' => 'Creation d\'un client',
|
||||
'app_admin_clients_show' => 'Consultation d\'un client',
|
||||
'app_admin_clients_edit' => 'Modification d\'un client',
|
||||
'app_admin_clients_search' => 'Recherche de clients',
|
||||
'app_admin_facturation_index' => 'Consultation de la facturation',
|
||||
'app_admin_revendeurs_index' => 'Consultation des revendeurs',
|
||||
'app_admin_revendeurs_create' => 'Creation d\'un revendeur',
|
||||
'app_admin_revendeurs_edit' => 'Modification d\'un revendeur',
|
||||
'app_admin_revendeurs_contrat' => 'Generation d\'un contrat revendeur',
|
||||
'app_admin_revendeurs_search' => 'Recherche de revendeurs',
|
||||
'app_admin_membres' => 'Consultation des membres',
|
||||
'app_admin_membres_create' => 'Creation d\'un membre',
|
||||
'app_admin_membres_delete' => 'Suppression d\'un membre',
|
||||
'app_admin_membres_resend' => 'Renvoi des identifiants d\'un membre',
|
||||
'app_admin_stats_index' => 'Consultation des statistiques',
|
||||
'app_admin_status_index' => 'Consultation du status des services',
|
||||
'app_admin_status_resolve' => 'Resolution d\'un message de service',
|
||||
'app_admin_sync_index' => 'Consultation de la synchronisation',
|
||||
'app_admin_sync_all' => 'Synchronisation complete Meilisearch',
|
||||
'app_admin_sync_customers' => 'Synchronisation des clients Meilisearch',
|
||||
'app_admin_sync_revendeurs' => 'Synchronisation des revendeurs Meilisearch',
|
||||
'app_admin_sync_prices' => 'Synchronisation des tarifs Meilisearch',
|
||||
'app_admin_sync_stripe_prices' => 'Synchronisation des tarifs Stripe',
|
||||
'app_admin_sync_stripe_webhooks' => 'Creation des webhooks Stripe',
|
||||
'app_admin_profil' => 'Consultation du profil',
|
||||
'app_admin_order_number' => 'Consultation de la numerotation',
|
||||
'app_admin_order_number_update' => 'Modification de la numerotation',
|
||||
'app_admin_tarification' => 'Consultation de la tarification',
|
||||
'app_admin_tarification_edit' => 'Modification d\'un tarif',
|
||||
'app_admin_services_index' => 'Consultation des services',
|
||||
'app_admin_logs' => 'Consultation des logs',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private EntityManagerInterface $em,
|
||||
#[Autowire('%env(APP_SECRET)%')] private string $hmacSecret,
|
||||
) {
|
||||
}
|
||||
|
||||
public function log(string $method, string $url, string $route, ?User $user = null, ?string $ip = null): void
|
||||
{
|
||||
$action = self::ROUTE_LABELS[$route] ?? 'Acces a '.$route;
|
||||
|
||||
if ('POST' === $method) {
|
||||
$action .= ' (soumission)';
|
||||
}
|
||||
|
||||
$log = new AppLog($method, $url, $route, $action, $this->hmacSecret, $user, $ip);
|
||||
|
||||
$this->em->persist($log);
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
public function verifyLog(AppLog $log): bool
|
||||
{
|
||||
return $log->verifyHmac($this->hmacSecret);
|
||||
}
|
||||
}
|
||||
@@ -81,6 +81,10 @@
|
||||
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14"/></svg>
|
||||
Numerotation
|
||||
</a>
|
||||
<a href="{{ path('app_admin_logs') }}" class="sidebar-nav-item {{ current_route starts with 'app_admin_logs' ? 'active-danger' : '' }}" style="color: {{ current_route starts with 'app_admin_logs' ? 'white' : 'rgba(248,113,113,0.7)' }}">
|
||||
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 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>
|
||||
Logs
|
||||
</a>
|
||||
<a href="{{ path('app_admin_tarification') }}" class="sidebar-nav-item {{ current_route starts with 'app_admin_tarification' ? 'active-danger' : '' }}" style="color: {{ current_route starts with 'app_admin_tarification' ? 'white' : 'rgba(248,113,113,0.7)' }}">
|
||||
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
Tarification
|
||||
|
||||
72
templates/admin/logs/index.html.twig
Normal file
72
templates/admin/logs/index.html.twig
Normal file
@@ -0,0 +1,72 @@
|
||||
{% extends 'admin/_layout.html.twig' %}
|
||||
|
||||
{% block title %}Logs - Administration - CRM SITECONSEIL{% endblock %}
|
||||
|
||||
{% block admin_content %}
|
||||
<div class="page-container">
|
||||
<h1 class="text-2xl font-bold heading-page mb-8">Logs d'activite</h1>
|
||||
|
||||
<div class="glass overflow-hidden">
|
||||
<table class="w-full text-xs">
|
||||
<thead>
|
||||
<tr class="glass-dark" style="border-radius: 0;">
|
||||
<th class="px-3 py-2 text-left font-bold uppercase tracking-wider text-white/80">Date</th>
|
||||
<th class="px-3 py-2 text-left font-bold uppercase tracking-wider text-white/80">Utilisateur</th>
|
||||
<th class="px-3 py-2 text-center font-bold uppercase tracking-wider text-white/80">Methode</th>
|
||||
<th class="px-3 py-2 text-left font-bold uppercase tracking-wider text-white/80">Action</th>
|
||||
<th class="px-3 py-2 text-left font-bold uppercase tracking-wider text-white/80">URL</th>
|
||||
<th class="px-3 py-2 text-left font-bold uppercase tracking-wider text-white/80">IP</th>
|
||||
<th class="px-3 py-2 text-center font-bold uppercase tracking-wider text-white/80">HMAC</th>
|
||||
<th class="px-3 py-2 text-center font-bold uppercase tracking-wider text-white/80">PDF</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for log in pagination %}
|
||||
<tr class="border-b border-white/10 hover:bg-white/30 transition-colors">
|
||||
<td class="px-3 py-2 text-gray-500 whitespace-nowrap">{{ log.createdAt|date('d/m/Y H:i:s') }}</td>
|
||||
<td class="px-3 py-2 font-bold">
|
||||
{% if log.user %}
|
||||
{{ log.user.fullName }}
|
||||
{% else %}
|
||||
<span class="text-gray-400">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-center">
|
||||
<span class="px-1.5 py-0.5 rounded text-[9px] font-bold
|
||||
{% if log.method == 'POST' %}bg-orange-500/20 text-orange-700
|
||||
{% elseif log.method == 'DELETE' %}bg-red-500/20 text-red-700
|
||||
{% elseif log.method == 'PUT' or log.method == 'PATCH' %}bg-blue-500/20 text-blue-700
|
||||
{% else %}bg-gray-500/20 text-gray-600{% endif %}">
|
||||
{{ log.method }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-3 py-2">{{ log.action }}</td>
|
||||
<td class="px-3 py-2 text-gray-500 font-mono text-[10px] max-w-[200px] truncate" title="{{ log.url }}">{{ log.url }}</td>
|
||||
<td class="px-3 py-2 text-gray-400 font-mono text-[10px]">{{ log.ip }}</td>
|
||||
<td class="px-3 py-2 text-center">
|
||||
{% if hmacResults[log.id] %}
|
||||
<span class="inline-block w-5 h-5 rounded-full bg-green-500/20 text-green-600 font-bold leading-5 text-[10px]" title="Integrite verifiee">✓</span>
|
||||
{% else %}
|
||||
<span class="inline-block w-5 h-5 rounded-full bg-red-500/20 text-red-600 font-bold leading-5 text-[10px]" title="Donnees alterees !">✗</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-center">
|
||||
<a href="{{ path('app_admin_logs_pdf', {id: log.id}) }}" class="btn-glass px-2 py-1 text-[9px] font-bold uppercase tracking-wider text-gray-600" target="_blank">PDF</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="8" class="px-4 py-8 text-center text-gray-400 font-bold">Aucun log enregistre.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if pagination.totalItemCount > 20 %}
|
||||
<div class="mt-6 flex justify-center">
|
||||
{{ knp_pagination_render(pagination) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
66
templates/admin/logs/pdf.html.twig
Normal file
66
templates/admin/logs/pdf.html.twig
Normal file
@@ -0,0 +1,66 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Log #{{ log.id }} - CRM SITECONSEIL</title>
|
||||
<style>
|
||||
body { font-family: Arial, Helvetica, sans-serif; font-size: 12px; color: #111827; margin: 40px; }
|
||||
h1 { font-size: 18px; margin-bottom: 4px; }
|
||||
.subtitle { font-size: 11px; color: #888; margin-bottom: 24px; }
|
||||
table { width: 100%; border-collapse: collapse; margin-bottom: 20px; }
|
||||
th { text-align: left; background-color: #111827; color: #fff; padding: 8px 12px; font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
td { padding: 8px 12px; border-bottom: 1px solid #eee; font-size: 11px; }
|
||||
.label { color: #666; width: 150px; font-weight: bold; }
|
||||
.value { color: #111; }
|
||||
.hmac-ok { color: #16a34a; font-weight: bold; }
|
||||
.hmac-ko { color: #dc2626; font-weight: bold; }
|
||||
.hmac-box { padding: 12px; border: 1px solid; margin-top: 20px; }
|
||||
.hmac-box.ok { border-color: #16a34a; background-color: #f0fdf4; }
|
||||
.hmac-box.ko { border-color: #dc2626; background-color: #fef2f2; }
|
||||
.footer { margin-top: 40px; font-size: 9px; color: #999; border-top: 1px solid #ddd; padding-top: 8px; }
|
||||
.mono { font-family: monospace; font-size: 10px; word-break: break-all; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Rapport de log #{{ log.id }}</h1>
|
||||
<p class="subtitle">CRM SITECONSEIL - Genere le {{ "now"|date("d/m/Y H:i:s") }}</p>
|
||||
|
||||
<table>
|
||||
<tr><th colspan="2">Informations du log</th></tr>
|
||||
<tr><td class="label">ID</td><td class="value">{{ log.id }}</td></tr>
|
||||
<tr><td class="label">Date</td><td class="value">{{ log.createdAt|date('d/m/Y H:i:s') }}</td></tr>
|
||||
<tr>
|
||||
<td class="label">Utilisateur</td>
|
||||
<td class="value">
|
||||
{% if log.user %}
|
||||
{{ log.user.fullName }} ({{ log.user.email }})
|
||||
{% else %}
|
||||
Non connecte
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td class="label">Methode HTTP</td><td class="value">{{ log.method }}</td></tr>
|
||||
<tr><td class="label">URL</td><td class="value mono">{{ log.url }}</td></tr>
|
||||
<tr><td class="label">Route</td><td class="value mono">{{ log.route }}</td></tr>
|
||||
<tr><td class="label">Action</td><td class="value">{{ log.action }}</td></tr>
|
||||
<tr><td class="label">Adresse IP</td><td class="value mono">{{ log.ip }}</td></tr>
|
||||
</table>
|
||||
|
||||
<div class="hmac-box {{ hmacValid ? 'ok' : 'ko' }}">
|
||||
<p style="font-weight: bold; font-size: 13px; margin: 0 0 8px;">
|
||||
{% if hmacValid %}
|
||||
<span class="hmac-ok">✓ INTEGRITE VERIFIEE - DONNEES CONFORMES</span>
|
||||
{% else %}
|
||||
<span class="hmac-ko">✗ INTEGRITE COMPROMISE - DONNEES ALTEREES</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
<p style="font-size: 10px; color: #666; margin: 0 0 4px;">Signature HMAC SHA-256 :</p>
|
||||
<p class="mono" style="margin: 0;">{{ log.hmac }}</p>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>Document genere automatiquement par le CRM SITECONSEIL. Ce rapport atteste de l'integrite des donnees de log au moment de sa generation.</p>
|
||||
<p>SARL SITECONSEIL - Siret : 418 664 058 - 27 rue Le Serurier, 02100 Saint-Quentin</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user