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:
@@ -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();
|
||||
|
||||
38
src/Controller/LogVerifyController.php
Normal file
38
src/Controller/LogVerifyController.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
101
templates/admin/logs/verify.html.twig
Normal file
101
templates/admin/logs/verify.html.twig
Normal 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 %}
|
||||
Reference in New Issue
Block a user