[+] chore(root): Initialise le projet avec une structure de base

Crée la structure de base du projet Symfony, incluant les entités,
services, formulaires, et templates nécessaires pour la gestion des
comptes utilisateurs, la sécurité, et la gestion des mots de passe
oubliés. Ajoute également la configuration pour la gestion des assets
avec Vite, la gestion des fichiers avec Flysystem, et la
génération de sitemaps.
```
This commit is contained in:
Serreau Jovann
2025-12-11 17:22:26 +01:00
parent f9987d525e
commit 662bb0bcc6
89 changed files with 18001 additions and 6950 deletions

View File

@@ -0,0 +1,181 @@
<?php
namespace App\Controller;
use App\Entity\Account;
use App\Entity\AccountResetPasswordRequest;
use App\Form\RequestPasswordConfirmType;
use App\Form\RequestPasswordRequestType;
use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent;
use App\Service\ResetPassword\Event\ResetPasswordEvent;
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(path: '/', name: 'app_home', options: ['sitemap' => false], methods: ['GET'])]
public function index(AuthenticationUtils $authenticationUtils): Response
{
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,
]);
}
const SENTRY_HOST = '';
const SENTRY_PROJECT_IDS = [''];
#[Route('/uptime',name: 'app_uptime',options: ['sitemap' => false], methods: ['GET'])]
public function app_uptime(Request $request,HttpClientInterface $httpClient): Response
{
return $this->json([]);
}
#[Route('/tunnel',name: 'app_tunnel',options: ['sitemap' => false], methods: ['POST'])]
public function tunnel(Request $request,HttpClientInterface $httpClient): Response
{
$envelope = $request->getContent();
if (empty($envelope)) {
return $this->json([]);
}
try {
// 2. Extract the header piece (first line)
$pieces = explode("\n", $envelope, 2);
$piece = $pieces[0];
// 3. Parse the header (which is JSON)
$header = json_decode($piece, true);
if (!isset($header['dsn'])) {
throw new \Exception("Missing DSN in envelope header.");
}
// 4. Extract and validate DSN and Project ID
$dsnUrl = parse_url($header['dsn']);
$dsnHostname = $dsnUrl['host'] ?? null;
$dsnPath = $dsnUrl['path'] ?? '/';
// Remove leading/trailing slashes from the path to get the project_id
$projectId = trim($dsnPath, '/');
if ($dsnHostname !== self::SENTRY_HOST) {
throw new \Exception("Invalid sentry hostname: {$dsnHostname}");
}
if (empty($projectId) || !in_array($projectId, self::SENTRY_PROJECT_IDS)) {
throw new \Exception("Invalid sentry project id: {$projectId}");
}
// 5. Construct the upstream Sentry URL
$upstreamSentryUrl = "https://" . self::SENTRY_HOST . "/api/" . $projectId . "/envelope/";
// 6. Forward the request using an HTTP client (e.g., Guzzle)
$response = $httpClient->request("POST",$upstreamSentryUrl, [
'body' => $envelope,
'headers' => [
// Sentry expects this content type
'Content-Type' => 'application/x-sentry-envelope',
// Forward the content encoding if present, though often not needed
// 'Content-Encoding' => $request->headers->get('Content-Encoding'),
],
]);
// 7. Return the status from the upstream Sentry response
return new JsonResponse([], $response->getStatusCode());
} catch (\Exception $e) {
// Log the error for server-side debugging
error_log("Error tunneling to Sentry: " . $e->getMessage());
// Return a success status (200/202) or a non-specific 500 to the client.
// Returning a non-error status (like 200) is often preferred for tunnels
// to avoid triggering ad-blockers on failures.
return new JsonResponse([
'error' => 'An error occurred during tunneling.'
], JsonResponse::HTTP_INTERNAL_SERVER_ERROR);
}
}
}