feat: page de verification en ligne des logs + QR code dans le PDF

src/Controller/LogVerifyController.php (nouveau):
- Route GET /admin/logs/verif/{id}/{hmac} accessible publiquement
- Le hmac dans l'URL est les 16 premiers caracteres du HMAC complet
  (suffisant pour identifier le log sans exposer la signature entiere)
- Verifie que le log existe et que le hmac partiel correspond
- Affiche la page de verification avec statut integrite

src/Controller/Admin/LogsController.php - pdf():
- Generation du QR code via Endroid\QrCode pointant vers l'URL
  de verification /admin/logs/verif/{id}/{hmac16}
- QR code encode en base64 et passe au template PDF

templates/admin/logs/verify.html.twig (nouveau):
- Page glassmorphism style attestation:
  - Log introuvable: bandeau rouge avec croix
  - Integrite verifiee: bandeau vert avec checkmark et message
    "La signature HMAC-SHA256 a ete verifiee avec succes"
  - Integrite compromise: bandeau rouge avec message d'alerte
- Tableau des details: ID, date, utilisateur, methode (badge colore),
  action, URL, route, IP
- Signature HMAC-SHA256 affichee en bas

templates/admin/logs/pdf.html.twig:
- Ajout du bloc verification avec QR code (72x72px) et lien URL
  identique au style des attestations RGPD (verify-box avec
  bordure indigo, QR a gauche, texte a droite)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-04-02 23:21:46 +02:00
parent b2c6f0194d
commit 33bd89e617
4 changed files with 168 additions and 0 deletions

View File

@@ -7,12 +7,15 @@ use App\Repository\AppLogRepository;
use App\Service\AppLoggerService;
use Doctrine\ORM\EntityManagerInterface;
use Dompdf\Dompdf;
use Endroid\QrCode\Builder\Builder;
use Endroid\QrCode\Writer\PngWriter;
use Knp\Component\Pager\PaginatorInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Twig\Environment;
@@ -121,10 +124,20 @@ class LogsController extends AbstractController
$logoPath = $projectDir.'/public/logo_facture.png';
$logo = file_exists($logoPath) ? 'data:image/png;base64,'.base64_encode((string) file_get_contents($logoPath)) : '';
$verifyUrl = $this->generateUrl('app_log_verify', [
'id' => $log->getId(),
'hmac' => substr($log->getHmac(), 0, 16),
], UrlGeneratorInterface::ABSOLUTE_URL);
$qrCode = (new Builder(writer: new PngWriter(), data: $verifyUrl, size: 200, margin: 10))->build();
$qrCodeBase64 = 'data:image/png;base64,'.base64_encode($qrCode->getString());
$html = $twig->render('admin/logs/pdf.html.twig', [
'log' => $log,
'hmacValid' => $hmacValid,
'logo' => $logo,
'verifyUrl' => $verifyUrl,
'qrcode' => $qrCodeBase64,
]);
$dompdf = new Dompdf();

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Controller;
use App\Repository\AppLogRepository;
use App\Service\AppLoggerService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class LogVerifyController extends AbstractController
{
#[Route('/admin/logs/verif/{id}/{hmac}', name: 'app_log_verify', methods: ['GET'])]
public function __invoke(
int $id,
string $hmac,
AppLogRepository $repository,
AppLoggerService $loggerService,
): Response {
$log = $repository->find($id);
if (null === $log || !str_starts_with($log->getHmac(), $hmac)) {
return $this->render('admin/logs/verify.html.twig', [
'log' => null,
'valid' => false,
'id' => $id,
]);
}
$valid = $loggerService->verifyLog($log);
return $this->render('admin/logs/verify.html.twig', [
'log' => $log,
'valid' => $valid,
'id' => $id,
]);
}
}

View File

@@ -92,6 +92,22 @@
</div>
<div class="hmac">HMAC-SHA256 : {{ log.hmac }}</div>
{% if verifyUrl is defined and qrcode is defined %}
<div style="margin: 16px 0; border: 1px solid #ddd; border-radius: 8px; display: table; width: 100%;">
<div style="display: table-row;">
<div style="display: table-cell; text-align: center; width: 100px; padding: 8px; border-right: 2px solid #111827; vertical-align: middle;">
<img src="{{ qrcode }}" alt="QR Code" style="width: 72px; height: 72px;">
</div>
<div style="display: table-cell; padding: 8px 12px; font-size: 9px; vertical-align: middle;">
<span style="font-size: 7px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.5px; color: #999; display: block; margin-bottom: 1px;">Verifier ce document</span>
<p style="margin: 2px 0 4px; font-size: 9px; font-weight: 700;">Scannez le QR code ou consultez le lien ci-dessous.</p>
<span style="font-size: 7px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.5px; color: #999; display: block; margin-bottom: 1px;">URL</span>
<span style="font-size: 8px; font-family: monospace; color: #4338ca; word-break: break-all;">{{ verifyUrl }}</span>
</div>
</div>
</div>
{% endif %}
<div style="margin-top: 24px;">
<span class="contact-box">contact@siteconseil.fr</span>
</div>

View File

@@ -0,0 +1,101 @@
{% extends 'legal/_layout.html.twig' %}
{% block title %}Verification log #{{ id }} - CRM SITECONSEIL{% endblock %}
{% block description %}Verification de l'integrite du log #{{ id }}.{% endblock %}
{% block body %}
<div class="page-container">
<h1 class="text-2xl font-bold heading-page mb-8">Verification de log</h1>
<div class="flex flex-col gap-8">
{% if log is null %}
<div class="glass p-6" style="border-left: 4px solid #dc2626;">
<div class="flex items-center gap-3 mb-2">
<svg class="w-8 h-8 text-red-600 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M6 18L18 6M6 6l12 12"/></svg>
<span class="text-xl font-bold uppercase text-red-800">Log introuvable</span>
</div>
<p class="text-sm text-red-700">Le log #{{ id }} n'existe pas ou le lien de verification est invalide.</p>
</div>
{% elseif valid %}
<div class="glass p-6" style="border-left: 4px solid #16a34a;">
<div class="flex items-center gap-3 mb-2">
<svg class="w-8 h-8 text-green-600 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"/></svg>
<span class="text-xl font-bold uppercase text-green-800">Integrite verifiee</span>
</div>
<p class="text-sm text-green-700 font-bold">La signature HMAC-SHA256 de ce log a ete verifiee avec succes. Les donnees sont conformes et n'ont pas ete alterees.</p>
</div>
{% else %}
<div class="glass p-6" style="border-left: 4px solid #dc2626;">
<div class="flex items-center gap-3 mb-2">
<svg class="w-8 h-8 text-red-600 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M6 18L18 6M6 6l12 12"/></svg>
<span class="text-xl font-bold uppercase text-red-800">Integrite compromise</span>
</div>
<p class="text-sm text-red-700 font-bold">La signature de ce log est invalide. Les donnees ont ete modifiees apres leur enregistrement initial.</p>
</div>
{% endif %}
{% if log %}
<div class="glass overflow-hidden">
<div class="glass-dark text-white px-6 py-3" style="border-radius: 0;">
<span class="text-xs font-bold uppercase tracking-wider">Details du log #{{ log.id }}</span>
</div>
<div class="p-6">
<table class="w-full text-sm">
<tr class="border-b border-white/20">
<th scope="row" class="py-3 pr-4 font-bold uppercase text-xs text-gray-500 w-1/3 text-left">ID</th>
<td class="py-3 font-bold">#{{ log.id }}</td>
</tr>
<tr class="border-b border-white/20">
<th scope="row" class="py-3 pr-4 font-bold uppercase text-xs text-gray-500 text-left">Date</th>
<td class="py-3 font-bold">{{ log.createdAt|date('d/m/Y a H:i:s') }}</td>
</tr>
<tr class="border-b border-white/20">
<th scope="row" class="py-3 pr-4 font-bold uppercase text-xs text-gray-500 text-left">Utilisateur</th>
<td class="py-3 font-bold">
{% if log.user %}
{{ log.user.fullName }} ({{ log.user.email }})
{% else %}
<span class="text-gray-400">Non connecte</span>
{% endif %}
</td>
</tr>
<tr class="border-b border-white/20">
<th scope="row" class="py-3 pr-4 font-bold uppercase text-xs text-gray-500 text-left">Methode</th>
<td class="py-3">
<span class="px-2 py-0.5 rounded text-xs font-bold
{% if log.method == 'POST' %}bg-orange-500/20 text-orange-700
{% elseif log.method == 'DELETE' %}bg-red-500/20 text-red-700
{% else %}bg-gray-500/20 text-gray-600{% endif %}">
{{ log.method }}
</span>
</td>
</tr>
<tr class="border-b border-white/20">
<th scope="row" class="py-3 pr-4 font-bold uppercase text-xs text-gray-500 text-left">Action</th>
<td class="py-3 font-bold">{{ log.action }}</td>
</tr>
<tr class="border-b border-white/20">
<th scope="row" class="py-3 pr-4 font-bold uppercase text-xs text-gray-500 text-left">URL</th>
<td class="py-3 font-mono text-xs break-all">{{ log.url }}</td>
</tr>
<tr class="border-b border-white/20">
<th scope="row" class="py-3 pr-4 font-bold uppercase text-xs text-gray-500 text-left">Route</th>
<td class="py-3 font-mono text-xs">{{ log.route }}</td>
</tr>
<tr>
<th scope="row" class="py-3 pr-4 font-bold uppercase text-xs text-gray-500 text-left">Adresse IP</th>
<td class="py-3 font-mono text-xs">{{ log.ip ?? 'N/A' }}</td>
</tr>
</table>
</div>
</div>
<div class="glass p-4">
<p class="text-xs font-bold uppercase tracking-wider text-gray-400 mb-2">Signature HMAC-SHA256</p>
<p class="text-xs font-mono text-gray-500 break-all">{{ log.hmac }}</p>
</div>
{% endif %}
</div>
</div>
{% endblock %}