fix: corriger HMAC des logs + PDF style attestation + pagination glassmorphism + purge logs
src/Entity/AppLog.php:
- createdAt initialise avec date('Y-m-d H:i:s') au lieu de
new DateTimeImmutable() pour tronquer les microsecondes
(PostgreSQL arrondit les microsecondes differemment de PHP,
ce qui causait des HMAC invalides a la relecture)
- generateHmac(): format Y-m-d\TH:i:s sans microsecondes
templates/admin/logs/pdf.html.twig (reecrit):
- Meme style que les attestations RGPD (templates/pdf/rgpd_*.html.twig):
banniere gold avec logo, doc-type badge indigo, titre italic uppercase,
info-grid avec cellules bordure indigo, tableaux data avec header dark,
bloc HMAC avec encadre vert/rouge, footer SARL SITECONSEIL
- Logo passe au template via base64
src/Controller/Admin/LogsController.php:
- pdf(): injection de kernel.project_dir, chargement du logo en base64
et passage au template
src/Command/PurgeEmailTrackingCommand.php:
- Ajout de la purge des AppLog de plus de 90 jours (meme seuil
que EmailTracking), affiche le nombre de logs supprimes
templates/components/pagination/glass.html.twig (nouveau):
- Template de pagination KnpPaginator style glassmorphism:
boutons glass avec hover, page active en glass-gold,
fleches precedent/suivant
config/packages/knp_paginator.yaml (nouveau):
- Configuration KnpPaginator pour utiliser le template glass
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
3
config/packages/knp_paginator.yaml
Normal file
3
config/packages/knp_paginator.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
knp_paginator:
|
||||
template:
|
||||
pagination: 'components/pagination/glass.html.twig'
|
||||
@@ -13,7 +13,7 @@ use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:email-tracking:purge',
|
||||
description: 'Supprime les enregistrements EmailTracking de plus de 90 jours',
|
||||
description: 'Supprime les EmailTracking et AppLog de plus de 90 jours',
|
||||
)]
|
||||
class PurgeEmailTrackingCommand extends Command
|
||||
{
|
||||
@@ -45,7 +45,18 @@ class PurgeEmailTrackingCommand extends Command
|
||||
|
||||
$deleted = $qb->getQuery()->execute();
|
||||
|
||||
$io->success("$deleted enregistrement(s) supprime(s).");
|
||||
$io->text("EmailTracking : $deleted enregistrement(s) supprime(s).");
|
||||
|
||||
$deletedLogs = $this->em->createQueryBuilder()
|
||||
->delete('App\Entity\AppLog', 'l')
|
||||
->where('l.createdAt < :threshold')
|
||||
->setParameter('threshold', $threshold)
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
$io->text("AppLog : $deletedLogs enregistrement(s) supprime(s).");
|
||||
|
||||
$io->success(($deleted + $deletedLogs).' enregistrement(s) supprime(s) au total.');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Entity\AppLog;
|
||||
use App\Repository\AppLogRepository;
|
||||
use App\Service\AppLoggerService;
|
||||
use Dompdf\Dompdf;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Knp\Component\Pager\PaginatorInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -51,6 +52,7 @@ class LogsController extends AbstractController
|
||||
AppLogRepository $repository,
|
||||
AppLoggerService $loggerService,
|
||||
Environment $twig,
|
||||
#[Autowire('%kernel.project_dir%')] string $projectDir,
|
||||
): Response {
|
||||
$log = $repository->find($id);
|
||||
|
||||
@@ -60,9 +62,13 @@ class LogsController extends AbstractController
|
||||
|
||||
$hmacValid = $loggerService->verifyLog($log);
|
||||
|
||||
$logoPath = $projectDir.'/public/logo_facture.png';
|
||||
$logo = file_exists($logoPath) ? 'data:image/png;base64,'.base64_encode((string) file_get_contents($logoPath)) : '';
|
||||
|
||||
$html = $twig->render('admin/logs/pdf.html.twig', [
|
||||
'log' => $log,
|
||||
'hmacValid' => $hmacValid,
|
||||
'logo' => $logo,
|
||||
]);
|
||||
|
||||
$dompdf = new Dompdf();
|
||||
|
||||
@@ -47,7 +47,7 @@ class AppLog
|
||||
$this->action = $action;
|
||||
$this->user = $user;
|
||||
$this->ip = $ip;
|
||||
$this->createdAt = new \DateTimeImmutable();
|
||||
$this->createdAt = new \DateTimeImmutable(date('Y-m-d H:i:s'));
|
||||
$this->hmac = $this->generateHmac($hmacSecret);
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ class AppLog
|
||||
$this->action,
|
||||
$this->ip ?? '',
|
||||
null !== $this->user ? (string) $this->user->getId() : '',
|
||||
$this->createdAt->format('Y-m-d\TH:i:s.u'),
|
||||
$this->createdAt->format('Y-m-d\TH:i:s'),
|
||||
]);
|
||||
|
||||
return hash_hmac('sha256', $payload, $secret);
|
||||
|
||||
@@ -4,63 +4,103 @@
|
||||
<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; }
|
||||
@page { margin: 0; size: A4; }
|
||||
body { font-family: Arial, Helvetica, sans-serif; font-size: 10px; color: #111827; margin: 0; padding: 0; }
|
||||
.banner { background: #fabf04; padding: 16px 32px; border-bottom: 1px solid #111827; }
|
||||
.banner img { height: 36px; }
|
||||
.banner-title { font-size: 8px; font-weight: 700; text-transform: uppercase; letter-spacing: 2px; color: #111827; margin-top: 4px; opacity: 0.7; }
|
||||
.container { padding: 24px 32px 16px; }
|
||||
.doc-type { display: inline-block; padding: 4px 12px; background: #4338ca; color: #fff; font-size: 8px; font-weight: 700; text-transform: uppercase; letter-spacing: 2px; margin-bottom: 8px; }
|
||||
h1 { font-size: 18px; font-weight: 700; text-transform: uppercase; letter-spacing: -0.5px; font-style: italic; margin: 0 0 4px 0; line-height: 1.1; }
|
||||
h2 { font-size: 11px; font-weight: 700; text-transform: uppercase; margin: 16px 0 4px; padding: 4px 10px; background: #fabf04; border: 1px solid #ddd; border-radius: 8px; display: inline-block; }
|
||||
.subtitle { font-size: 9px; color: #666; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 16px; }
|
||||
.info-grid { display: table; width: 100%; margin-bottom: 12px; }
|
||||
.info-row { display: table-row; }
|
||||
.info-grid .info-cell { display: table-cell; padding: 6px 10px; vertical-align: top; }
|
||||
.info-label { font-size: 7px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.5px; color: #999; display: block; margin-bottom: 1px; }
|
||||
.info-value { font-size: 11px; font-weight: 700; color: #111827; }
|
||||
.info-cell { border-left: 3px solid #4338ca; }
|
||||
table.data { width: 100%; border-collapse: collapse; margin-top: 8px; font-size: 9px; border: 1px solid #ddd; border-radius: 8px; }
|
||||
table.data th { background: #111827; color: #fff; padding: 4px 8px; text-align: left; text-transform: uppercase; font-size: 8px; font-weight: 700; letter-spacing: 0.5px; }
|
||||
table.data td { padding: 5px 8px; border-bottom: 1px solid #e5e7eb; }
|
||||
table.data tr:nth-child(even) td { background: #f9fafb; }
|
||||
.hmac-box { margin: 16px 0; padding: 12px; border: 2px solid; border-radius: 8px; }
|
||||
.hmac-box.ok { border-color: #16a34a; background: #f0fdf4; }
|
||||
.hmac-box.ko { border-color: #dc2626; background: #fef2f2; }
|
||||
.hmac-ok { color: #16a34a; font-weight: 700; font-size: 12px; }
|
||||
.hmac-ko { color: #dc2626; font-weight: 700; font-size: 12px; }
|
||||
.hmac { font-size: 7px; color: #aaa; word-break: break-all; margin: 8px 0; padding: 6px 8px; background: #f9fafb; border: 1px solid #e5e7eb; font-family: monospace; }
|
||||
.contact-box { display: inline-block; padding: 6px 16px; background: #111827; color: #fff; font-weight: 700; text-transform: uppercase; font-size: 9px; letter-spacing: 1px; }
|
||||
.mono { font-family: monospace; font-size: 9px; 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 class="banner">
|
||||
{% if logo %}<img src="{{ logo }}" alt="CRM SITECONSEIL">{% endif %}
|
||||
<div class="banner-title">SARL SITECONSEIL</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<span class="doc-type">Log d'activite</span>
|
||||
<h1>Rapport de log #{{ log.id }}</h1>
|
||||
<div class="subtitle">Trace d'activite — CRM SITECONSEIL</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 class="info-grid">
|
||||
<div class="info-row">
|
||||
<div class="info-cell" style="width: 25%;"><span class="info-label">ID</span><span class="info-value">#{{ log.id }}</span></div>
|
||||
<div class="info-cell" style="width: 25%;"><span class="info-label">Date</span><span class="info-value">{{ log.createdAt|date('d/m/Y a H:i:s') }}</span></div>
|
||||
<div class="info-cell" style="width: 25%;"><span class="info-label">Methode</span><span class="info-value">{{ log.method }}</span></div>
|
||||
<div class="info-cell" style="width: 25%;"><span class="info-label">Adresse IP</span><span class="info-value">{{ log.ip ?? 'N/A' }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Utilisateur</h2>
|
||||
<table class="data">
|
||||
<thead><tr><th>Champ</th><th>Valeur</th></tr></thead>
|
||||
<tbody>
|
||||
{% if log.user %}
|
||||
<tr><td>Nom complet</td><td><strong>{{ log.user.fullName }}</strong></td></tr>
|
||||
<tr><td>Email</td><td class="mono">{{ log.user.email }}</td></tr>
|
||||
<tr><td>ID utilisateur</td><td>{{ log.user.id }}</td></tr>
|
||||
{% else %}
|
||||
<tr><td>Utilisateur</td><td><em>Non connecte</em></td></tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Requete</h2>
|
||||
<table class="data">
|
||||
<thead><tr><th>Champ</th><th>Valeur</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Methode HTTP</td><td><strong>{{ log.method }}</strong></td></tr>
|
||||
<tr><td>URL</td><td class="mono">{{ log.url }}</td></tr>
|
||||
<tr><td>Route</td><td class="mono">{{ log.route }}</td></tr>
|
||||
<tr><td>Action</td><td><strong>{{ log.action }}</strong></td></tr>
|
||||
<tr><td>Adresse IP</td><td class="mono">{{ log.ip ?? 'N/A' }}</td></tr>
|
||||
<tr><td>Date et heure</td><td>{{ log.createdAt|date('d/m/Y H:i:s') }}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Integrite</h2>
|
||||
<div class="hmac-box {{ hmacValid ? 'ok' : 'ko' }}">
|
||||
{% if hmacValid %}
|
||||
<span class="hmac-ok">✓ INTEGRITE VERIFIEE — DONNEES CONFORMES</span>
|
||||
<p style="font-size: 9px; color: #666; margin: 4px 0 0;">Les donnees de ce log n'ont pas ete alterees depuis leur enregistrement.</p>
|
||||
{% else %}
|
||||
<span class="hmac-ko">✗ INTEGRITE COMPROMISE — DONNEES ALTEREES</span>
|
||||
<p style="font-size: 9px; color: #991b1b; margin: 4px 0 0;">Attention : les donnees de ce log ont ete modifiees apres leur enregistrement initial.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="hmac">HMAC-SHA256 : {{ log.hmac }}</div>
|
||||
|
||||
<div style="margin-top: 24px;">
|
||||
<span class="contact-box">contact@siteconseil.fr</span>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 16px; padding-top: 8px; border-top: 1px solid #ddd; font-size: 7px; color: #999; line-height: 1.6;">
|
||||
SARL SITECONSEIL — Siret : 418 664 058 — TVA : FR05 418 664 058<br>
|
||||
27 rue Le Serurier, 02100 Saint-Quentin, France — contact@siteconseil.fr<br>
|
||||
<a href="https://www.siteconseil.fr" style="color: #999;">www.siteconseil.fr</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
23
templates/components/pagination/glass.html.twig
Normal file
23
templates/components/pagination/glass.html.twig
Normal file
@@ -0,0 +1,23 @@
|
||||
{% if pageCount > 1 %}
|
||||
<nav class="flex items-center justify-center gap-1">
|
||||
{% if previous is defined and previous %}
|
||||
<a href="{{ path(route, query|merge({(pageParameterName): previous})) }}" class="glass px-3 py-2 text-xs font-bold text-gray-600 hover:bg-white/80 transition-all" rel="prev">«</a>
|
||||
{% else %}
|
||||
<span class="glass px-3 py-2 text-xs font-bold text-gray-300 cursor-default">«</span>
|
||||
{% endif %}
|
||||
|
||||
{% for page in pagesInRange %}
|
||||
{% if page != current %}
|
||||
<a href="{{ path(route, query|merge({(pageParameterName): page})) }}" class="glass px-3 py-2 text-xs font-bold text-gray-600 hover:bg-white/80 transition-all">{{ page }}</a>
|
||||
{% else %}
|
||||
<span class="glass-gold px-3 py-2 text-xs font-bold text-gray-900">{{ page }}</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if next is defined and next %}
|
||||
<a href="{{ path(route, query|merge({(pageParameterName): next})) }}" class="glass px-3 py-2 text-xs font-bold text-gray-600 hover:bg-white/80 transition-all" rel="next">»</a>
|
||||
{% else %}
|
||||
<span class="glass px-3 py-2 text-xs font-bold text-gray-300 cursor-default">»</span>
|
||||
{% endif %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
Reference in New Issue
Block a user