✨ feat(Sentry): Initialise Sentry pour le suivi des erreurs et performances.
Ajoute l'initialisation de Sentry avec tunnel, suivi des performances et replay.
```
187 lines
8.0 KiB
PHP
187 lines
8.0 KiB
PHP
<?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 HomeController extends AbstractController
|
|
{
|
|
|
|
#[Route('/connect/keycloak', name: 'connect_keycloak_start')]
|
|
public function connect(ClientRegistry $clientRegistry)
|
|
{
|
|
// Redirects to Keycloak
|
|
return $clientRegistry
|
|
->getClient('keycloak')
|
|
->redirect(['email', 'profile','openid'], []);
|
|
}
|
|
|
|
#[Route('/oauth/sso', name: 'connect_keycloak_check')]
|
|
public function connectCheck(Request $request)
|
|
{
|
|
// This method stays empty; the authenticator will intercept it!
|
|
}
|
|
#[Route(path: '/', name: 'app_home', options: ['sitemap' => false], methods: ['GET','POST'])]
|
|
public function index(AuthenticationUtils $authenticationUtils): Response
|
|
{
|
|
if($this->getUser()){
|
|
return $this->redirectToRoute('app_crm');
|
|
}
|
|
return $this->render('home.twig',[
|
|
'last_username' => $authenticationUtils->getLastUsername(),
|
|
'error' => $authenticationUtils->getLastAuthenticationError(),
|
|
]);
|
|
}
|
|
|
|
#[Route(path: '/logout', name: 'app_logout', options: ['sitemap' => false], methods: ['GET','POST'])]
|
|
public function logout(): Response
|
|
{
|
|
|
|
}
|
|
#[Route(path: '/mot-de-passe-oublie', name: 'app_forgot_password', options: ['sitemap' => false], methods: ['GET','POST'])]
|
|
public function forgotPassword(Request $request,EventDispatcherInterface $eventDispatcher): Response
|
|
{
|
|
$requestPasswordRequest = new ResetPasswordEvent();
|
|
$form = $this->createForm(RequestPasswordRequestType::class,$requestPasswordRequest);
|
|
$form->handleRequest($request);
|
|
if ($form->isSubmitted() && $form->isValid()) {
|
|
$eventDispatcher->dispatch($requestPasswordRequest);
|
|
return $this->redirectToRoute('app_forgot_password_sent');
|
|
}
|
|
return $this->render('security/forgot_password.twig', [
|
|
'form' => $form->createView(),
|
|
]);
|
|
}
|
|
|
|
#[Route(path: '/mot-de-passe-oublie/sent', name: 'app_forgot_password_sent', options: ['sitemap' => false], methods: ['GET','POST'])]
|
|
public function forgotPasswordSent(Request $request,EventDispatcherInterface $eventDispatcher): Response
|
|
{
|
|
return $this->render('security/forgot_password_success.twig', [
|
|
]);
|
|
}
|
|
|
|
#[Route(path: '/mot-de-passe-oublie/{id}/{token}', name: 'app_forgot_password_confirm', options: ['sitemap' => false], methods: ['GET','POST'])]
|
|
public function forgotPasswordConfirm(UserPasswordHasherInterface $userPasswordHasher,EventDispatcherInterface $eventDispatcher,Request $request,EntityManagerInterface $entityManager,string $id,string $token): Response
|
|
{
|
|
$errorMessage = "Requête non valide.";
|
|
if (!is_numeric($id)) {
|
|
$this->addFlash("error", $errorMessage);
|
|
return $this->redirectToRoute('app_forgot_password');
|
|
}
|
|
$account = $entityManager->getRepository(Account::class)->find((int)$id);
|
|
if (!$account instanceof Account) {
|
|
$this->addFlash("error", $errorMessage);
|
|
return $this->redirectToRoute('app_forgot_password');
|
|
}
|
|
$requestToken = $entityManager->getRepository(AccountResetPasswordRequest::class)->findOneBy([
|
|
'Account' => $account, // Assurez-vous que 'Account' est le nom correct de la propriété/colonne dans votre entité AccountResetPasswordRequest.
|
|
'token' => $token
|
|
]);
|
|
if (!$requestToken instanceof AccountResetPasswordRequest) {
|
|
$this->addFlash("error", $errorMessage);
|
|
return $this->redirectToRoute('app_forgot_password');
|
|
}
|
|
$now = new \DateTimeImmutable();
|
|
if ($requestToken->getExpiresAt() < $now) {
|
|
$this->addFlash("error", "Le lien de réinitialisation de mot de passe a expiré.");
|
|
return $this->redirectToRoute('app_forgot_password');
|
|
}
|
|
|
|
$event = new ResetPasswordConfirmEvent();
|
|
$form = $this->createForm(RequestPasswordConfirmType::class,$event);
|
|
$form->handleRequest($request);
|
|
if ($form->isSubmitted() && $form->isValid()) {
|
|
$account->setPassword($userPasswordHasher->hashPassword($account,$event->getPassword()));
|
|
$entityManager->persist($account);
|
|
$entityManager->flush();
|
|
$this->addFlash("success", "Votre mot de passe a été mis à jour avec succès.");
|
|
return $this->redirectToRoute('app_home');
|
|
}
|
|
return $this->render('security/forgot-password-confirm.twig', [
|
|
'form' => $form->createView(),
|
|
'noIndex' => true,
|
|
'id' => $id,
|
|
'token' => $token,
|
|
'account' => $account,
|
|
]);
|
|
}
|
|
|
|
#[Route('/unscribe/{email}', name: 'app_unscribe', options: ['sitemap' => false], methods: ['GET', 'POST'])]
|
|
public function appUnscribe(
|
|
string $email,
|
|
Request $request,
|
|
EntityManagerInterface $entityManager,
|
|
AppLogger $appLogger
|
|
): Response {
|
|
// 1. Décodage de l'email (au cas où il y aurait des caractères spéciaux)
|
|
$email = urldecode($email);
|
|
|
|
// 3. Gestion du POST (Désinscription en un clic via le client mail)
|
|
if ($request->isMethod('POST')) {
|
|
$appLogger->record('UNSUBSCRIBE', sprintf("Désinscription automatique (One-Click) de : %s", $email));
|
|
return new JsonResponse(['status' => 'success'], Response::HTTP_OK);
|
|
}
|
|
|
|
$appLogger->record('UNSUBSCRIBE', sprintf("Désinscription manuelle de : %s", $email));
|
|
|
|
return $this->render('security/unscribe_success.twig', [
|
|
'email' => $email
|
|
]);
|
|
}
|
|
// Remplace par ton DSN exact pour la vérification
|
|
private const ALLOWED_DSN = 'https://803814be6540031b1c37bf92ba9c0f79@sentry.esy-web.dev/24';
|
|
private const SENTRY_HOST = 'sentry.esy-web.dev';
|
|
private const PROJECT_ID = '24';
|
|
|
|
#[Route('/sentry-tunnel', name: 'sentry_tunnel', methods: ['POST'])]
|
|
public function tunnel(Request $request, HttpClientInterface $httpClient): Response
|
|
{
|
|
try {
|
|
$envelope = $request->getContent();
|
|
$pieces = explode("\n", $envelope);
|
|
|
|
// La première ligne de l'enveloppe Sentry contient le header en JSON
|
|
$header = json_decode($pieces[0], true);
|
|
|
|
// --- SÉCURITÉ : On vérifie que le DSN correspond bien au tien ---
|
|
if (isset($header['dsn']) && $header['dsn'] !== self::ALLOWED_DSN) {
|
|
return new Response('Invalid DSN', 401);
|
|
}
|
|
|
|
// Construction de l'URL finale vers ton instance Sentry
|
|
$url = "https://" . self::SENTRY_HOST . "/api/" . self::PROJECT_ID . "/envelope/";
|
|
|
|
// On renvoie l'enveloppe à Sentry
|
|
$httpClient->request('POST', $url, [
|
|
'body' => $envelope,
|
|
'headers' => [
|
|
'Content-Type' => 'text/plain;charset=UTF-8',
|
|
],
|
|
]);
|
|
|
|
return new Response('', 204);
|
|
} catch (\Exception $e) {
|
|
// En cas d'erreur du tunnel, on reste discret pour ne pas polluer la console client
|
|
return new Response('Tunnel Error', 500);
|
|
}
|
|
}
|
|
}
|