feat(security): [FR] Améliore la gestion des erreurs et la sécurité de l'intranet.
```
This commit is contained in:
Serreau Jovann
2026-01-19 11:20:16 +01:00
parent 1afc6a20ea
commit b7a96e76d0
10 changed files with 293 additions and 16 deletions

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Controller;
use App\Entity\Account;
use App\Entity\AccountResetPasswordRequest;
use App\Form\RequestPasswordConfirmType;
use App\Form\RequestPasswordRequestType;
use App\Logger\AppLogger;
use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent;
use App\Service\ResetPassword\Event\ResetPasswordEvent;
use Doctrine\ORM\EntityManagerInterface;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class LegalController extends AbstractController
{
#[Route('/mentions-legal', name: 'mentions-legal')]
public function mentionsLegal()
{
return $this->render('legal/mentions.html.twig');
}
#[Route('/conditions-general-de-vente', name: 'cgv')]
public function cgv()
{
}
}

View File

@@ -2,28 +2,88 @@
namespace App\Security;
use App\Service\Mailer\Mailer;
use App\Service\Signature\Client;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Twig\Environment;
#[AsEventListener(RequestEvent::class,method: 'onLocked')]
#[AsEventListener(RequestEvent::class,method: 'onLocked',priority: 10)]
#[AsEventListener(RequestEvent::class,method: 'onControl',priority: 5)]
class IntranetLocked
{
public function __construct(private readonly Environment $environment)
{
public function __construct(private readonly Environment $environment,
private readonly Client $signatureClient,
private readonly \App\Service\Search\Client $searchClient,
private readonly \App\Service\Stripe\Client $stripeClient,
private readonly Mailer $mailer
){
}
public function onLocked(RequestEvent $requestEvent)
{
if($_ENV['INTRANET_LOCK'] == "true" &&
!str_contains( $requestEvent->getRequest()->getPathInfo(),"_wdt") &&
!str_contains( $requestEvent->getRequest()->getPathInfo(),"_profiler")
) {
if($_ENV['INTRANET_LOCK'] == "true" && ! $this->isDebugRoute($requestEvent)) {
$response = new Response($this->environment->render('security/locked.twig'));
$response->setStatusCode(Response::HTTP_FORBIDDEN);
$requestEvent->setResponse($response);
}
}
private function isDebugRoute(RequestEvent $event): bool
{
$path = $event->getRequest()->getPathInfo();
return str_contains($path, "_wdt") || str_contains($path, "_profiler");
}
public function onControl(RequestEvent $requestEvent)
{
if ($this->isDebugRoute($requestEvent)) {
return;
}
$isValid = true;
$message = [];
if(!$this->signatureClient->status()) {
$isValid = false;
$message = [
'service' => 'Signature',
'status' => 'Hors Service'
];
$this->advertTech($message);
}
if(!$this->searchClient->status()) {
$isValid = false;
$message = [
'service' => 'Recherche',
'status' => 'Hors Service'
];
$this->advertTech($message);
}
if(!$this->stripeClient->status()) {
$isValid = false;
$message = [
'service' => 'Stripe',
'status' => 'Hors Service'
];
$this->advertTech($message);
}
if(!$isValid) {
$response = new Response($this->environment->render('security/error.twig',[
'message' => $message,
]));
$response->setStatusCode(Response::HTTP_FORBIDDEN);
$requestEvent->setResponse($response);
}
}
public function advertTech(array $message) {
$this->mailer->send('notification@siteconseil.fr',"Notification siteconseil",
"[Intranet Ludikevent] - Accées impossible suite à une erreur de service",
"mails/tech/noaccess.twig",[
'message' => $message
]);
}
}

View File

@@ -40,15 +40,19 @@ class DevisPdfService extends Fpdf
}
$this->SetFont('Arial', 'B', 14);
$this->Cell(0, 7, $this->clean('LUDIKEVENT'), 0, 1, 'L');
$this->Cell(0, 7, $this->clean('Lilian SEGARD - LUDIKEVENT'), 0, 1, 'L');
$this->SetX(25);
$this->SetFont('Arial', '', 8);
$this->SetTextColor(80, 80, 80);
$this->Cell(0, 4, $this->clean('SIRET : 930 488 408 00012 | RCS : 930 488 408'), 0, 1, 'L');
$this->SetX(25);
$this->Cell(0, 4, $this->clean('6 Rue du Château 02800 Danizy France'), 0, 1, 'L');
$this->SetX(25);
$this->Cell(0, 4, $this->clean('Tél. : 06 14 17 24 47'), 0, 1, 'L');
$this->SetX(25);
$this->Cell(0, 4, $this->clean('Assurance RC Pro : '), 0, 1, 'L');
$this->SetX(25);
$this->SetTextColor(37, 99, 235);
@@ -134,12 +138,6 @@ class DevisPdfService extends Fpdf
$this->Cell(30, 8, $this->clean('TOTAL HT'), 0, 0, 'L');
$this->Cell(40, 8, number_format($totalHT, 2, ',', ' ') . $this->clean(' €'), 0, 1, 'R');
$this->Cell(120);
$this->SetTextColor(37, 99, 235);
$this->SetFont('Arial', 'B', 12);
$this->Cell(30, 10, $this->clean('TOTAL TTC'), 0, 0, 'L');
$this->Cell(40, 10, number_format($totalHT * 1.20, 2, ',', ' ') . $this->clean(' €'), 0, 1, 'R');
return $this->Output('S');
}

View File

@@ -130,4 +130,14 @@ class Client
$fullName = $this->env . "_" . $key;
$this->client->deleteIndex($fullName);
}
public function status() : bool
{
try {
$result = $this->client->health();
return $result['status'] == "available";
}catch (\Exception $e) {
return false;
}
}
}

View File

@@ -24,7 +24,7 @@ class Client
) {
// Configuration via les variables d'environnement
$key = $_ENV['ESYSIGN_APIEY'] ?? '';
$this->baseUrl = $_ENV['ESYSIGN_URL'] ?? '';
$this->baseUrl = "https://signature.esy-web.dev";
// L'URL API est le point d'entrée pour le SDK Docuseal
$apiUrl = rtrim($this->baseUrl, '/') . '/api';
@@ -101,4 +101,14 @@ class Client
{
return $this->docuseal->getSubmitter($submitterId);
}
public function status(): bool
{
try {
$this->docuseal->listTemplates();
return true;
} catch (\Throwable $e) {
return false;
}
}
}

View File

@@ -365,4 +365,10 @@ class Client
return ['state' => false, 'message' => $e->getMessage()];
}
}
public function status()
{
$result = $this->check();
return $result['state'];
}
}

View File

@@ -0,0 +1,4 @@
{% extends 'base.twig' %}
{% block title %}Mentions légal{% endblock %}
{% block body %}
{% endblock %}

View File

@@ -0,0 +1,48 @@
{% extends 'mails/base.twig' %}
{% block content %}
<mj-section padding-bottom="0px" background-color="transparent">
<mj-column>
<mj-text align="center" font-size="12px" font-weight="900" color="#9ca3af" letter-spacing="2px">
SITECONSEIL MONITORING
</mj-text>
</mj-column>
</mj-section>
<mj-section border-radius="20px 20px 0 0" padding-top="40px">
<mj-column>
<mj-image width="64px" src="https://img.icons8.com/fluency/96/error.png" alt="Alert Icon" />
<mj-text align="center" font-weight="800" font-size="24px" color="#111827" padding-top="20px">
Interruption de Service
</mj-text>
<mj-text align="center" color="#dc2626" font-size="12px" font-weight="bold" text-transform="uppercase" letter-spacing="1px">
Accès Intranet Verrouillé
</mj-text>
</mj-column>
</mj-section>
<mj-section padding="0 40px">
<mj-column background-color="#f9fafb" border="1px solid #e5e7eb" border-radius="12px" padding="20px">
<mj-text font-size="13px" font-weight="bold" color="#6b7280" text-transform="uppercase" padding-bottom="10px">
Détails de l'incident :
</mj-text>
<mj-table color="#374151" font-size="15px">
<tr style="height:30px;">
<td style="font-weight:bold;">Service :</td>
<td style="text-align:right; color:#111827;">{{ datas.message.service|default('Inconnu') }}</td>
</tr>
<tr style="height:30px;">
<td style="font-weight:bold;">Statut :</td>
<td style="text-align:right; color:#dc2626; font-weight:bold;">{{ datas.message.status|default('DOWN') }}</td>
</tr>
<tr style="height:30px;">
<td style="font-weight:bold;">Date :</td>
<td style="text-align:right;">{{ "now"|date("d/m/Y H:i") }}</td>
</tr>
</mj-table>
</mj-column>
</mj-section>
{% endblock %}

View File

@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Erreur Service - Siteconseil</title>
{{ vite_asset('error.js')}}
</head>
<body class="bg-[#0f172a] text-slate-200 font-sans min-h-screen flex items-center justify-center overflow-hidden">
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<div class="absolute -top-[10%] -left-[10%] w-[50%] h-[50%] bg-red-600/5 rounded-full blur-[120px]"></div>
<div class="absolute -bottom-[10%] -right-[10%] w-[50%] h-[50%] bg-orange-600/5 rounded-full blur-[120px]"></div>
</div>
<div class="relative z-10 w-full max-w-lg p-6 mx-auto">
<div class="backdrop-blur-3xl bg-slate-900/60 border border-white/10 rounded-[3rem] p-10 shadow-2xl text-center">
<div class="flex justify-center mb-8">
<div class="relative">
<div class="absolute inset-0 bg-red-500 blur-2xl opacity-20 animate-pulse"></div>
<div class="relative p-6 bg-red-500/10 rounded-3xl border border-red-500/20">
<svg class="w-12 h-12 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
</div>
</div>
<h1 class="text-2xl font-black text-white mb-2 uppercase tracking-tight">
Service <span class="text-red-500">Indisponible</span>
</h1>
<div class="inline-flex items-center px-3 py-1 rounded-full bg-red-500/10 border border-red-500/20 mb-6">
<span class="w-2 h-2 rounded-full bg-red-500 animate-ping mr-2"></span>
<span class="text-[10px] font-bold text-red-400 uppercase tracking-widest">
{{ message.service }} : {{ message.status|upper }}
</span>
</div>
<p class="text-slate-400 text-sm leading-relaxed mb-10">
Une interruption technique affecte actuellement le module de <strong>{{ message.service }}</strong>.
L'accès à l'intranet est suspendu par mesure de sécurité jusqu'au rétablissement du service.
</p>
<div class="grid grid-cols-1 gap-4 text-left">
<div class="p-4 rounded-2xl bg-white/5 border border-white/5">
<p class="text-[9px] text-slate-500 font-black uppercase tracking-widest mb-3">Support Technique Siteconseil</p>
<div class="flex flex-col space-y-3">
<a href="mailto:s.com@siteconseil.fr" class="text-sm font-semibold text-white hover:text-blue-400 transition-colors flex items-center">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>
s.com@siteconseil.fr
</a>
<a href="tel:0323056243" class="text-sm font-semibold text-white hover:text-green-400 transition-colors flex items-center">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"/></svg>
03 23 05 62 43
</a>
</div>
</div>
</div>
<div class="mt-8">
<p class="text-[9px] text-slate-600 uppercase tracking-[0.4em] font-black italic">Siteconseil Intranet</p>
</div>
</div>
</div>
</body>
</html>

View File

@@ -6,7 +6,7 @@
<title>Accès Restreint - Siteconseil</title>
{{ vite_asset('error.js') }}
</head>
<body class="bg-[#0f172a] text-slate-200 font-sans min-h-screen flex items-center justify-center overflow-hidden">
<body class="bg-[#0f172a] text-slate-200 font-sans min-h-screen flex items-center justify-center">
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<div class="absolute -top-[10%] -left-[10%] w-[50%] h-[50%] bg-blue-600/10 rounded-full blur-[120px]"></div>
@@ -68,6 +68,37 @@
</p>
</div>
</div>
{# ... après le div du footer ... #}
<div class="mt-10 p-6 backdrop-blur-md bg-black/20 border border-white/5 rounded-3xl overflow-hidden">
<h3 class="text-[10px] font-black text-blue-500 uppercase tracking-[0.2em] mb-4 flex items-center">
<svg class="w-4 h-4 mr-2" 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>
Request Headers (Debug)
</h3>
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<thead>
<tr class="border-b border-white/10">
<th class="py-2 text-[9px] font-bold text-slate-500 uppercase tracking-wider">Clé</th>
<th class="py-2 text-[9px] font-bold text-slate-500 uppercase tracking-wider pl-4">Valeur</th>
</tr>
</thead>
<tbody class="divide-y divide-white/5">
{% for key, values in app.request.headers.all %}
<tr class="group hover:bg-white/5 transition-colors">
<td class="py-3 text-[11px] font-mono text-blue-400 align-top break-all">{{ key }}</td>
<td class="py-3 pl-4 text-[11px] text-slate-300 align-top break-all">
{# Les headers peuvent être des tableaux, on les joint proprement #}
{{ values|join(', ') }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</body>