Files
ludikevent_crm/src/Controller/HomeController.php
Serreau Jovann 101990dfbd ```
 feat(Sentry): Initialise Sentry pour le suivi des erreurs et performances.

Ajoute l'initialisation de Sentry avec tunnel, suivi des performances et replay.
```
2026-01-15 20:42:55 +01:00

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);
}
}
}