feat: gestion des remarques chauffagistes + refacto sécurité

- Ajout du champ 'Remarque' dans l'entité Intervention
- Création d'un formulaire RemarqueType dédié
- Ajout d'une route /intervention/{id}/remarque accessible uniquement au chauffagiste assigné
- Mise en place d'un contrôleur sécurisé pour ajouter une remarque
- Création de la vue intervention/remarque.html.twig
- Affichage conditionnel du bouton 'Ajouter une remarque' dans show.html.twig
- Séparation stricte des rôles : seuls les chauffagistes peuvent ajouter leur remarque
- Mise à jour de tous les contrôleurs avec denyUnlessAdminOrSecretaire() pour clarifier les accès
- Redirection des dashboards et calendriers selon rôle (admin, secrétaire, chauffagiste)

 Prochaine étape : liaison compétences ↔ pannes ou gestion des stocks associés
This commit is contained in:
sermandm 2025-05-08 12:31:40 +02:00
parent 5cdf38794a
commit 8580911c1a
14 changed files with 241 additions and 198 deletions

4
.idea/dataSources.xml generated
View File

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true"> <component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="hegreetconfort@localhost" uuid="21423ae4-3232-4641-affb-06399f70655a"> <data-source source="LOCAL" name="@localhost" uuid="21423ae4-3232-4641-affb-06399f70655a">
<driver-ref>postgresql</driver-ref> <driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize> <synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver> <jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://localhost:5432/hegreetconfort</jdbc-url> <jdbc-url>jdbc:postgresql://localhost:5433/</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir> <working-dir>$ProjectFileDir$</working-dir>
</data-source> </data-source>
</component> </component>

View File

@ -29,7 +29,7 @@ Cette application permet de gérer les interventions, les utilisateurs (chauffag
``` ```
2. Installez les dépendances avec Composer : 2. Installez les dépendances avec Composer :
```bash ```bash
cd chauffagiste-app cd HegreEtConfort
composer install composer install
``` ```
3. Créez la base de données : 3. Créez la base de données :
@ -40,7 +40,13 @@ Cette application permet de gérer les interventions, les utilisateurs (chauffag
```bash ```bash
php bin/console doctrine:migrations:migrate php bin/console doctrine:migrations:migrate
``` ```
5. Lancez le serveur Symfony : 5. Exécuter cette insertion dans la console PostgreSQL pour créer votre premier utilisateur admin :
```bash
INSERT INTO "HegreEtConfort".public.utilisateur (id, email, first_name, last_name, birth_date, phone, roles, password)
VALUES (1000, 'admin@admin.admin', 'admin', 'admin', '2025-04-10', 'admin', '["ROLE_ADMIN"]', '$2y$13$4jqoZVgncgDJ6oPFDswZeeiVmt9TF2AC.xoBwyyrrbNl5Xz8r.50e');
```
6. Lancez le serveur Symfony :
```bash ```bash
symfony server:start symfony server:start
``` ```

View File

@ -3,25 +3,22 @@
namespace App\Controller; namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
class AuthenticationController extends AbstractController class AuthenticationController extends AbstractController
{ {
#[Route(path: '/', name: 'app_login')] #[Route(path: '/login', name: 'app_login')]
public function login(AuthenticationUtils $authenticationUtils): Response public function login(AuthenticationUtils $authenticationUtils): Response
{ {
// Get the login error if there is one // get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError(); $error = $authenticationUtils->getLastAuthenticationError();
// last username entered by the user
// Last username entered by the user
$lastUsername = $authenticationUtils->getLastUsername(); $lastUsername = $authenticationUtils->getLastUsername();
return $this->render('login/index.html.twig', [ return $this->render('authentication/login.html.twig', ['last_username' => $lastUsername, 'error' => $error]);
'last_username' => $lastUsername,
'error' => $error,
]);
} }
#[Route(path: '/logout', name: 'app_logout')] #[Route(path: '/logout', name: 'app_logout')]

View File

@ -4,28 +4,26 @@ namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Annotation\Route;
#[Route('/dashboard')]
class DashboardController extends AbstractController class DashboardController extends AbstractController
{ {
#[Route('/admin/dashboard', name: 'admin_dashboard')] #[Route('/', name: 'dashboard')]
public function admin(): Response public function index(): Response
{ {
$this->denyAccessUnlessGranted('ROLE_ADMIN'); if ($this->isGranted('ROLE_ADMIN')) {
return $this->render('dashboard/admin.html.twig'); return $this->render('admin.html.twig');
} }
#[Route('/secretaire/dashboard', name: 'secretaire_dashboard')] if ($this->isGranted('ROLE_SECRETAIRE')) {
public function secretaire(): Response return $this->render('secretaire.html.twig');
{ }
$this->denyAccessUnlessGranted('ROLE_SECRETAIRE');
return $this->render('dashboard/secretaire.html.twig');
}
#[Route('/chauffagiste/dashboard', name: 'chauffagiste_dashboard')] if ($this->isGranted('ROLE_CHAUFFAGISTE')) {
public function chauffagiste(): Response return $this->render('chauffagiste.html.twig');
{ }
$this->denyAccessUnlessGranted('ROLE_CHAUFFAGISTE');
return $this->render('dashboard/chauffagiste.html.twig'); throw $this->createAccessDeniedException('Vous ne pouvez pas accéder à ce tableau de bord.');
} }
} }

View File

@ -9,45 +9,36 @@ use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Annotation\Route;
#[Route('/fault')] #[Route('/fault')]
final class FaultController extends AbstractController class FaultController extends AbstractController
{ {
#[Route(name: 'app_fault_index', methods: ['GET'])] #[Route('/', name: 'app_fault_index', methods: ['GET'])]
public function index(FaultRepository $faultRepository): Response public function index(FaultRepository $faultRepository): Response
{ {
// Filtrage des pannes : un chauffagiste ne peut voir que ses pannes $this->denyUnlessAdminOrSecretaire();
$faults = $this->isGranted('ROLE_CHAUFFAGISTE')
? $faultRepository->findByUser($this->getUser()) // Filtre les pannes par utilisateur
: $faultRepository->findAll(); // Admins voient toutes les pannes
return $this->render('fault/index.html.twig', [ return $this->render('fault/index.html.twig', [
'faults' => $faults, 'faults' => $faultRepository->findAll(),
]); ]);
} }
#[Route('/new', name: 'app_fault_new', methods: ['GET', 'POST'])] #[Route('/new', name: 'app_fault_new', methods: ['GET', 'POST'])]
public function new(Request $request, EntityManagerInterface $entityManager): Response public function new(Request $request, EntityManagerInterface $entityManager): Response
{ {
$this->denyUnlessAdminOrSecretaire();
$fault = new Fault(); $fault = new Fault();
$form = $this->createForm(FaultType::class, $fault); $form = $this->createForm(FaultType::class, $fault);
$form->handleRequest($request); $form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
// Associe la panne à un chauffagiste si c'est un chauffagiste
if ($this->isGranted('ROLE_CHAUFFAGISTE')) {
$fault->setUser($this->getUser());
}
$entityManager->persist($fault); $entityManager->persist($fault);
$entityManager->flush(); $entityManager->flush();
return $this->redirectToRoute('app_fault_index');
return $this->redirectToRoute('app_fault_index', [], Response::HTTP_SEE_OTHER);
} }
return $this->render('fault/new.html.twig', [ return $this->render('fault/new.html.twig', [
'fault' => $fault,
'form' => $form, 'form' => $form,
]); ]);
} }
@ -55,11 +46,7 @@ final class FaultController extends AbstractController
#[Route('/{id}', name: 'app_fault_show', methods: ['GET'])] #[Route('/{id}', name: 'app_fault_show', methods: ['GET'])]
public function show(Fault $fault): Response public function show(Fault $fault): Response
{ {
// Un chauffagiste ne peut voir que ses pannes $this->denyUnlessAdminOrSecretaire();
if ($this->isGranted('ROLE_CHAUFFAGISTE') && $fault->getUser() !== $this->getUser()) {
throw $this->createAccessDeniedException('Vous ne pouvez pas voir cette panne.');
}
return $this->render('fault/show.html.twig', [ return $this->render('fault/show.html.twig', [
'fault' => $fault, 'fault' => $fault,
]); ]);
@ -68,39 +55,39 @@ final class FaultController extends AbstractController
#[Route('/{id}/edit', name: 'app_fault_edit', methods: ['GET', 'POST'])] #[Route('/{id}/edit', name: 'app_fault_edit', methods: ['GET', 'POST'])]
public function edit(Request $request, Fault $fault, EntityManagerInterface $entityManager): Response public function edit(Request $request, Fault $fault, EntityManagerInterface $entityManager): Response
{ {
// Un chauffagiste ne peut modifier que ses propres pannes $this->denyUnlessAdminOrSecretaire();
if ($this->isGranted('ROLE_CHAUFFAGISTE') && $fault->getUser() !== $this->getUser()) {
throw $this->createAccessDeniedException('Vous ne pouvez pas modifier cette panne.');
}
$form = $this->createForm(FaultType::class, $fault); $form = $this->createForm(FaultType::class, $fault);
$form->handleRequest($request); $form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
$entityManager->flush(); $entityManager->flush();
return $this->redirectToRoute('app_fault_index');
return $this->redirectToRoute('app_fault_index', [], Response::HTTP_SEE_OTHER);
} }
return $this->render('fault/edit.html.twig', [ return $this->render('fault/edit.html.twig', [
'fault' => $fault,
'form' => $form, 'form' => $form,
'fault' => $fault,
]); ]);
} }
#[Route('/{id}', name: 'app_fault_delete', methods: ['POST'])] #[Route('/{id}', name: 'app_fault_delete', methods: ['POST'])]
public function delete(Request $request, Fault $fault, EntityManagerInterface $entityManager): Response public function delete(Request $request, Fault $fault, EntityManagerInterface $entityManager): Response
{ {
// Un chauffagiste ne peut supprimer que ses propres pannes $this->denyUnlessAdminOrSecretaire();
if ($this->isGranted('ROLE_CHAUFFAGISTE') && $fault->getUser() !== $this->getUser()) {
throw $this->createAccessDeniedException('Vous ne pouvez pas supprimer cette panne.');
}
if ($this->isCsrfTokenValid('delete'.$fault->getId(), $request->get('csrf_token'))) { if ($this->isCsrfTokenValid('delete'.$fault->getId(), $request->request->get('_token'))) {
$entityManager->remove($fault); $entityManager->remove($fault);
$entityManager->flush(); $entityManager->flush();
} }
return $this->redirectToRoute('app_fault_index', [], Response::HTTP_SEE_OTHER); return $this->redirectToRoute('app_fault_index');
}
private function denyUnlessAdminOrSecretaire(): void
{
if (!$this->isGranted('ROLE_ADMIN') && !$this->isGranted('ROLE_SECRETAIRE')) {
throw $this->createAccessDeniedException();
}
} }
} }

View File

@ -7,51 +7,39 @@ use App\Form\InterventionType;
use App\Repository\InterventionRepository; use App\Repository\InterventionRepository;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Annotation\Route;
#[Route('/intervention')] #[Route('/intervention')]
final class InterventionController extends AbstractController class InterventionController extends AbstractController
{ {
#[Route(name: 'app_intervention_index', methods: ['GET'])] #[Route('/', name: 'app_intervention_index', methods: ['GET'])]
public function index(InterventionRepository $interventionRepository): Response public function index(InterventionRepository $interventionRepository): Response
{ {
// Vérifier si l'utilisateur est un chauffagiste, pour filtrer ses interventions $this->denyUnlessAdminOrSecretaire();
if ($this->isGranted('ROLE_CHAUFFAGISTE')) {
$interventions = $interventionRepository->findByUser($this->getUser()); // On filtre par utilisateur connecté
} else {
// Les autres rôles (admin) peuvent voir toutes les interventions
$interventions = $interventionRepository->findAll();
}
return $this->render('intervention/index.html.twig', [ return $this->render('intervention/index.html.twig', [
'interventions' => $interventions, 'interventions' => $interventionRepository->findAll(),
]); ]);
} }
#[Route('/new', name: 'app_intervention_new', methods: ['GET', 'POST'])] #[Route('/new', name: 'app_intervention_new', methods: ['GET', 'POST'])]
public function new(Request $request, EntityManagerInterface $entityManager): Response public function new(Request $request, EntityManagerInterface $entityManager): Response
{ {
$this->denyUnlessAdminOrSecretaire();
$intervention = new Intervention(); $intervention = new Intervention();
$form = $this->createForm(InterventionType::class, $intervention); $form = $this->createForm(InterventionType::class, $intervention);
$form->handleRequest($request); $form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
// Si l'utilisateur est un chauffagiste, on associe l'intervention à lui
if ($this->isGranted('ROLE_CHAUFFAGISTE')) {
$intervention->setUser($this->getUser());
}
$entityManager->persist($intervention); $entityManager->persist($intervention);
$entityManager->flush(); $entityManager->flush();
return $this->redirectToRoute('app_intervention_index', [], Response::HTTP_SEE_OTHER); return $this->redirectToRoute('app_intervention_index');
} }
return $this->render('intervention/new.html.twig', [ return $this->render('intervention/new.html.twig', [
'intervention' => $intervention,
'form' => $form, 'form' => $form,
]); ]);
} }
@ -59,11 +47,7 @@ final class InterventionController extends AbstractController
#[Route('/{id}', name: 'app_intervention_show', methods: ['GET'])] #[Route('/{id}', name: 'app_intervention_show', methods: ['GET'])]
public function show(Intervention $intervention): Response public function show(Intervention $intervention): Response
{ {
// Vérifier si l'utilisateur peut voir cette intervention (chauffagiste ne voit que ses interventions) $this->denyUnlessAdminOrSecretaire();
if ($this->isGranted('ROLE_CHAUFFAGISTE') && $intervention->getUser() !== $this->getUser()) {
throw $this->createAccessDeniedException('Vous ne pouvez pas voir cette intervention.');
}
return $this->render('intervention/show.html.twig', [ return $this->render('intervention/show.html.twig', [
'intervention' => $intervention, 'intervention' => $intervention,
]); ]);
@ -72,10 +56,7 @@ final class InterventionController extends AbstractController
#[Route('/{id}/edit', name: 'app_intervention_edit', methods: ['GET', 'POST'])] #[Route('/{id}/edit', name: 'app_intervention_edit', methods: ['GET', 'POST'])]
public function edit(Request $request, Intervention $intervention, EntityManagerInterface $entityManager): Response public function edit(Request $request, Intervention $intervention, EntityManagerInterface $entityManager): Response
{ {
// Vérification de sécurité : un chauffagiste ne peut modifier que ses propres interventions $this->denyUnlessAdminOrSecretaire();
if ($this->isGranted('ROLE_CHAUFFAGISTE') && $intervention->getUser() !== $this->getUser()) {
throw $this->createAccessDeniedException('Vous ne pouvez pas modifier cette intervention.');
}
$form = $this->createForm(InterventionType::class, $intervention); $form = $this->createForm(InterventionType::class, $intervention);
$form->handleRequest($request); $form->handleRequest($request);
@ -83,49 +64,57 @@ final class InterventionController extends AbstractController
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
$entityManager->flush(); $entityManager->flush();
return $this->redirectToRoute('app_intervention_index', [], Response::HTTP_SEE_OTHER); return $this->redirectToRoute('app_intervention_index');
} }
return $this->render('intervention/edit.html.twig', [ return $this->render('intervention/edit.html.twig', [
'intervention' => $intervention,
'form' => $form, 'form' => $form,
'intervention' => $intervention,
]); ]);
} }
#[Route('/{id}', name: 'app_intervention_delete', methods: ['POST'])] #[Route('/{id}', name: 'app_intervention_delete', methods: ['POST'])]
public function delete(Request $request, Intervention $intervention, EntityManagerInterface $entityManager): Response public function delete(Request $request, Intervention $intervention, EntityManagerInterface $entityManager): Response
{ {
// Vérification de sécurité : un chauffagiste ne peut supprimer que ses propres interventions $this->denyUnlessAdminOrSecretaire();
if ($this->isGranted('ROLE_CHAUFFAGISTE') && $intervention->getUser() !== $this->getUser()) {
throw $this->createAccessDeniedException('Vous ne pouvez pas supprimer cette intervention.');
}
if ($this->isCsrfTokenValid('delete'.$intervention->getId(), $request->get('csrf_token'))) { if ($this->isCsrfTokenValid('delete' . $intervention->getId(), $request->request->get('_token'))) {
$entityManager->remove($intervention); $entityManager->remove($intervention);
$entityManager->flush(); $entityManager->flush();
} }
return $this->redirectToRoute('app_intervention_index', [], Response::HTTP_SEE_OTHER); return $this->redirectToRoute('app_intervention_index');
} }
#[Route('/{id}/remarque', name: 'app_intervention_remarque', methods: ['GET', 'POST'])]
public function ajouterRemarque(Request $request, Intervention $intervention, EntityManagerInterface $entityManager): Response
#[Route('/api/interventions', name: 'api_interventions')]
public function apiInterventions(InterventionRepository $repo): JsonResponse
{ {
$interventions = $repo->findAll(); // Vérifie que l'utilisateur est le chauffagiste assigné à l'intervention
$user = $this->getUser();
$events = []; if (!$this->isGranted('ROLE_CHAUFFAGISTE') || $intervention->getUser() !== $user) {
throw $this->createAccessDeniedException("Vous ne pouvez modifier que vos propres interventions.");
foreach ($interventions as $intervention) {
$events[] = [
'id' => $intervention->getId(),
'title' => $intervention->getWording(), // ou getTitre() selon ton entité
'start' => $intervention->getDate()->format('Y-m-d\TH:i:s'),
// ajoute 'end' si tu veux une durée
];
} }
return $this->json($events); $form = $this->createForm(RemarqueType::class, $intervention);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$entityManager->flush();
$this->addFlash('success', 'Remarque ajoutée avec succès.');
return $this->redirectToRoute('app_intervention_show', ['id' => $intervention->getId()]);
}
return $this->render('intervention/remarque.html.twig', [
'form' => $form,
'intervention' => $intervention,
]);
}
private function denyUnlessAdminOrSecretaire(): void
{
if (!$this->isGranted('ROLE_ADMIN') && !$this->isGranted('ROLE_SECRETAIRE')) {
throw $this->createAccessDeniedException();
}
} }
} }

View File

@ -9,16 +9,16 @@ use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Annotation\Route;
#[Route('/skill')] #[Route('/skill')]
final class SkillController extends AbstractController class SkillController extends AbstractController
{ {
#[Route(name: 'app_skill_index', methods: ['GET'])] #[Route('/', name: 'app_skill_index', methods: ['GET'])]
public function index(SkillRepository $skillRepository): Response public function index(SkillRepository $skillRepository): Response
{ {
$this->denyAccessUnlessGranted('ROLE_ADMIN'); $this->denyUnlessAdminOrSecretaire();
return $this->render('skill/admin.html.twig', [ return $this->render('skill/index.html.twig', [
'skills' => $skillRepository->findAll(), 'skills' => $skillRepository->findAll(),
]); ]);
} }
@ -26,7 +26,8 @@ final class SkillController extends AbstractController
#[Route('/new', name: 'app_skill_new', methods: ['GET', 'POST'])] #[Route('/new', name: 'app_skill_new', methods: ['GET', 'POST'])]
public function new(Request $request, EntityManagerInterface $entityManager): Response public function new(Request $request, EntityManagerInterface $entityManager): Response
{ {
$this->denyAccessUnlessGranted('ROLE_ADMIN'); $this->denyUnlessAdminOrSecretaire();
$skill = new Skill(); $skill = new Skill();
$form = $this->createForm(SkillType::class, $skill); $form = $this->createForm(SkillType::class, $skill);
$form->handleRequest($request); $form->handleRequest($request);
@ -35,11 +36,10 @@ final class SkillController extends AbstractController
$entityManager->persist($skill); $entityManager->persist($skill);
$entityManager->flush(); $entityManager->flush();
return $this->redirectToRoute('app_skill_index', [], Response::HTTP_SEE_OTHER); return $this->redirectToRoute('app_skill_index');
} }
return $this->render('skill/new.html.twig', [ return $this->render('skill/new.html.twig', [
'skill' => $skill,
'form' => $form, 'form' => $form,
]); ]);
} }
@ -47,6 +47,7 @@ final class SkillController extends AbstractController
#[Route('/{id}', name: 'app_skill_show', methods: ['GET'])] #[Route('/{id}', name: 'app_skill_show', methods: ['GET'])]
public function show(Skill $skill): Response public function show(Skill $skill): Response
{ {
$this->denyUnlessAdminOrSecretaire();
return $this->render('skill/show.html.twig', [ return $this->render('skill/show.html.twig', [
'skill' => $skill, 'skill' => $skill,
]); ]);
@ -55,31 +56,40 @@ final class SkillController extends AbstractController
#[Route('/{id}/edit', name: 'app_skill_edit', methods: ['GET', 'POST'])] #[Route('/{id}/edit', name: 'app_skill_edit', methods: ['GET', 'POST'])]
public function edit(Request $request, Skill $skill, EntityManagerInterface $entityManager): Response public function edit(Request $request, Skill $skill, EntityManagerInterface $entityManager): Response
{ {
$this->denyAccessUnlessGranted('ROLE_ADMIN'); $this->denyUnlessAdminOrSecretaire();
$form = $this->createForm(SkillType::class, $skill); $form = $this->createForm(SkillType::class, $skill);
$form->handleRequest($request); $form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
$entityManager->flush(); $entityManager->flush();
return $this->redirectToRoute('app_skill_index', [], Response::HTTP_SEE_OTHER); return $this->redirectToRoute('app_skill_index');
} }
return $this->render('skill/edit.html.twig', [ return $this->render('skill/edit.html.twig', [
'skill' => $skill,
'form' => $form, 'form' => $form,
'skill' => $skill,
]); ]);
} }
#[Route('/{id}', name: 'app_skill_delete', methods: ['POST'])] #[Route('/{id}', name: 'app_skill_delete', methods: ['POST'])]
public function delete(Request $request, Skill $skill, EntityManagerInterface $entityManager): Response public function delete(Request $request, Skill $skill, EntityManagerInterface $entityManager): Response
{ {
$this->denyAccessUnlessGranted('ROLE_ADMIN'); $this->denyUnlessAdminOrSecretaire();
if ($this->isCsrfTokenValid('delete'.$skill->getId(), $request->get('csrf_token'))) {
if ($this->isCsrfTokenValid('delete' . $skill->getId(), $request->request->get('_token'))) {
$entityManager->remove($skill); $entityManager->remove($skill);
$entityManager->flush(); $entityManager->flush();
} }
return $this->redirectToRoute('app_skill_index', [], Response::HTTP_SEE_OTHER); return $this->redirectToRoute('app_skill_index');
}
private function denyUnlessAdminOrSecretaire(): void
{
if (!$this->isGranted('ROLE_ADMIN') && !$this->isGranted('ROLE_SECRETAIRE')) {
throw $this->createAccessDeniedException();
}
} }
} }

View File

@ -9,14 +9,15 @@ use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Annotation\Route;
#[Route('/stock')] #[Route('/stock')]
final class StockController extends AbstractController class StockController extends AbstractController
{ {
#[Route(name: 'app_stock_index', methods: ['GET'])] #[Route('/', name: 'app_stock_index', methods: ['GET'])]
public function index(StockRepository $stockRepository): Response public function index(StockRepository $stockRepository): Response
{ {
$this->denyUnlessAdminOrSecretaire();
return $this->render('stock/index.html.twig', [ return $this->render('stock/index.html.twig', [
'stocks' => $stockRepository->findAll(), 'stocks' => $stockRepository->findAll(),
]); ]);
@ -25,6 +26,8 @@ final class StockController extends AbstractController
#[Route('/new', name: 'app_stock_new', methods: ['GET', 'POST'])] #[Route('/new', name: 'app_stock_new', methods: ['GET', 'POST'])]
public function new(Request $request, EntityManagerInterface $entityManager): Response public function new(Request $request, EntityManagerInterface $entityManager): Response
{ {
$this->denyUnlessAdminOrSecretaire();
$stock = new Stock(); $stock = new Stock();
$form = $this->createForm(StockType::class, $stock); $form = $this->createForm(StockType::class, $stock);
$form->handleRequest($request); $form->handleRequest($request);
@ -33,11 +36,10 @@ final class StockController extends AbstractController
$entityManager->persist($stock); $entityManager->persist($stock);
$entityManager->flush(); $entityManager->flush();
return $this->redirectToRoute('app_stock_index', [], Response::HTTP_SEE_OTHER); return $this->redirectToRoute('app_stock_index');
} }
return $this->render('stock/new.html.twig', [ return $this->render('stock/new.html.twig', [
'stock' => $stock,
'form' => $form, 'form' => $form,
]); ]);
} }
@ -45,6 +47,7 @@ final class StockController extends AbstractController
#[Route('/{id}', name: 'app_stock_show', methods: ['GET'])] #[Route('/{id}', name: 'app_stock_show', methods: ['GET'])]
public function show(Stock $stock): Response public function show(Stock $stock): Response
{ {
$this->denyUnlessAdminOrSecretaire();
return $this->render('stock/show.html.twig', [ return $this->render('stock/show.html.twig', [
'stock' => $stock, 'stock' => $stock,
]); ]);
@ -53,10 +56,7 @@ final class StockController extends AbstractController
#[Route('/{id}/edit', name: 'app_stock_edit', methods: ['GET', 'POST'])] #[Route('/{id}/edit', name: 'app_stock_edit', methods: ['GET', 'POST'])]
public function edit(Request $request, Stock $stock, EntityManagerInterface $entityManager): Response public function edit(Request $request, Stock $stock, EntityManagerInterface $entityManager): Response
{ {
// Si un chauffagiste essaie de modifier un stock d'admin $this->denyUnlessAdminOrSecretaire();
if ($this->isGranted('ROLE_CHAUFFAGISTE')) {
throw $this->createAccessDeniedException('Vous ne pouvez pas modifier ce stock.');
}
$form = $this->createForm(StockType::class, $stock); $form = $this->createForm(StockType::class, $stock);
$form->handleRequest($request); $form->handleRequest($request);
@ -64,23 +64,32 @@ final class StockController extends AbstractController
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
$entityManager->flush(); $entityManager->flush();
return $this->redirectToRoute('app_stock_index', [], Response::HTTP_SEE_OTHER); return $this->redirectToRoute('app_stock_index');
} }
return $this->render('stock/edit.html.twig', [ return $this->render('stock/edit.html.twig', [
'stock' => $stock,
'form' => $form, 'form' => $form,
'stock' => $stock,
]); ]);
} }
#[Route('/{id}', name: 'app_stock_delete', methods: ['POST'])] #[Route('/{id}', name: 'app_stock_delete', methods: ['POST'])]
public function delete(Request $request, Stock $stock, EntityManagerInterface $entityManager): Response public function delete(Request $request, Stock $stock, EntityManagerInterface $entityManager): Response
{ {
if ($this->isCsrfTokenValid('delete'.$stock->getId(), $request->get('csrf_token'))) { $this->denyUnlessAdminOrSecretaire();
if ($this->isCsrfTokenValid('delete' . $stock->getId(), $request->request->get('_token'))) {
$entityManager->remove($stock); $entityManager->remove($stock);
$entityManager->flush(); $entityManager->flush();
} }
return $this->redirectToRoute('app_stock_index', [], Response::HTTP_SEE_OTHER); return $this->redirectToRoute('app_stock_index');
}
private function denyUnlessAdminOrSecretaire(): void
{
if (!$this->isGranted('ROLE_ADMIN') && !$this->isGranted('ROLE_SECRETAIRE')) {
throw $this->createAccessDeniedException();
}
} }
} }

View File

@ -11,6 +11,7 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
final class UserController extends AbstractController final class UserController extends AbstractController
{ {
@ -89,27 +90,19 @@ final class UserController extends AbstractController
} }
// Route pour supprimer un utilisateur spécifique // Route pour supprimer un utilisateur spécifique
#[IsGranted('ROLE_ADMIN', 'ROLE_SECRETAIRE')]
#[Route('/user/{id}', name: 'app_user_delete', methods: ['POST'])] #[Route('/user/{id}', name: 'app_user_delete', methods: ['POST'])]
public function delete(Request $request, Utilisateur $user, EntityManagerInterface $entityManager): Response public function delete(Request $request, Utilisateur $utilisateur): Response
{ {
// Si l'utilisateur est un secrétaire et qu'il essaie de supprimer un autre secrétaire if ($this->isCsrfTokenValid('delete' . $utilisateur->getId(), $request->request->get('_token'))) {
if ($this->isGranted('ROLE_SECRETAIRE') && $user->hasRole('ROLE_SECRETAIRE')) { $this->entityManager->remove($utilisateur);
throw $this->createAccessDeniedException('Vous ne pouvez pas supprimer un autre secrétaire ou un administrateur.'); $this->entityManager->flush();
$this->addFlash('success', 'Utilisateur supprimé avec succès.');
} else {
$this->addFlash('error', 'Token CSRF invalide.');
} }
// Si l'utilisateur est un chauffagiste et qu'il essaie de supprimer un admin return $this->redirectToRoute('app_user_index');
if ($this->isGranted('ROLE_CHAUFFAGISTE') && ($user->hasRole('ROLE_SECRETAIRE') || $user->hasRole('ROLE_CHAUFFAGISTE'))) {
throw $this->createAccessDeniedException('Vous ne pouvez pas supprimer un utilisateur.');
}
// On s'assure que seul un admin peut supprimer un autre admin
$this->denyAccessUnlessGranted("ROLE_ADMIN");
if ($this->isCsrfTokenValid('delete' . $user->getId(), $request->get('csrf_token'))) {
$entityManager->remove($user);
$entityManager->flush();
}
return $this->redirectToRoute('app_user_index', [], Response::HTTP_SEE_OTHER);
} }
} }

View File

@ -9,45 +9,36 @@ use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Annotation\Route;
#[Route('/vehicle')] #[Route('/vehicle')]
final class VehicleController extends AbstractController class VehicleController extends AbstractController
{ {
#[Route(name: 'app_vehicle_index', methods: ['GET'])] #[Route('/', name: 'app_vehicle_index', methods: ['GET'])]
public function index(VehicleRepository $vehicleRepository): Response public function index(VehicleRepository $vehicleRepository): Response
{ {
// Admin peut voir tous les véhicules, chauffagiste ne peut voir que ses véhicules $this->denyUnlessAdminOrSecretaire();
$vehicles = $this->isGranted('ROLE_CHAUFFAGISTE')
? $vehicleRepository->findByUser($this->getUser()) // Filtre les véhicules par utilisateur
: $vehicleRepository->findAll(); // Les admins voient tout
return $this->render('vehicle/index.html.twig', [ return $this->render('vehicle/index.html.twig', [
'vehicles' => $vehicles, 'vehicles' => $vehicleRepository->findAll(),
]); ]);
} }
#[Route('/new', name: 'app_vehicle_new', methods: ['GET', 'POST'])] #[Route('/new', name: 'app_vehicle_new', methods: ['GET', 'POST'])]
public function new(Request $request, EntityManagerInterface $entityManager): Response public function new(Request $request, EntityManagerInterface $entityManager): Response
{ {
$this->denyUnlessAdminOrSecretaire();
$vehicle = new Vehicle(); $vehicle = new Vehicle();
$form = $this->createForm(VehicleType::class, $vehicle); $form = $this->createForm(VehicleType::class, $vehicle);
$form->handleRequest($request); $form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
// Si l'utilisateur est un chauffagiste, on associe le véhicule à lui
if ($this->isGranted('ROLE_CHAUFFAGISTE')) {
$vehicle->setUser($this->getUser());
}
$entityManager->persist($vehicle); $entityManager->persist($vehicle);
$entityManager->flush(); $entityManager->flush();
return $this->redirectToRoute('app_vehicle_index');
return $this->redirectToRoute('app_vehicle_index', [], Response::HTTP_SEE_OTHER);
} }
return $this->render('vehicle/new.html.twig', [ return $this->render('vehicle/new.html.twig', [
'vehicle' => $vehicle,
'form' => $form, 'form' => $form,
]); ]);
} }
@ -55,11 +46,7 @@ final class VehicleController extends AbstractController
#[Route('/{id}', name: 'app_vehicle_show', methods: ['GET'])] #[Route('/{id}', name: 'app_vehicle_show', methods: ['GET'])]
public function show(Vehicle $vehicle): Response public function show(Vehicle $vehicle): Response
{ {
// Si l'utilisateur est un chauffagiste et essaie de voir un véhicule d'un autre chauffagiste, on bloque $this->denyUnlessAdminOrSecretaire();
if ($this->isGranted('ROLE_CHAUFFAGISTE') && $vehicle->getUser() !== $this->getUser()) {
throw $this->createAccessDeniedException('Vous ne pouvez pas voir ce véhicule.');
}
return $this->render('vehicle/show.html.twig', [ return $this->render('vehicle/show.html.twig', [
'vehicle' => $vehicle, 'vehicle' => $vehicle,
]); ]);
@ -68,39 +55,39 @@ final class VehicleController extends AbstractController
#[Route('/{id}/edit', name: 'app_vehicle_edit', methods: ['GET', 'POST'])] #[Route('/{id}/edit', name: 'app_vehicle_edit', methods: ['GET', 'POST'])]
public function edit(Request $request, Vehicle $vehicle, EntityManagerInterface $entityManager): Response public function edit(Request $request, Vehicle $vehicle, EntityManagerInterface $entityManager): Response
{ {
// Vérifier si un chauffagiste essaie de modifier un véhicule d'un autre chauffagiste $this->denyUnlessAdminOrSecretaire();
if ($this->isGranted('ROLE_CHAUFFAGISTE') && $vehicle->getUser() !== $this->getUser()) {
throw $this->createAccessDeniedException('Vous ne pouvez pas modifier ce véhicule.');
}
$form = $this->createForm(VehicleType::class, $vehicle); $form = $this->createForm(VehicleType::class, $vehicle);
$form->handleRequest($request); $form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
$entityManager->flush(); $entityManager->flush();
return $this->redirectToRoute('app_vehicle_index');
return $this->redirectToRoute('app_vehicle_index', [], Response::HTTP_SEE_OTHER);
} }
return $this->render('vehicle/edit.html.twig', [ return $this->render('vehicle/edit.html.twig', [
'vehicle' => $vehicle,
'form' => $form, 'form' => $form,
'vehicle' => $vehicle,
]); ]);
} }
#[Route('/{id}', name: 'app_vehicle_delete', methods: ['POST'])] #[Route('/{id}', name: 'app_vehicle_delete', methods: ['POST'])]
public function delete(Request $request, Vehicle $vehicle, EntityManagerInterface $entityManager): Response public function delete(Request $request, Vehicle $vehicle, EntityManagerInterface $entityManager): Response
{ {
// Vérification de sécurité : un chauffagiste ne peut supprimer un véhicule d'un autre chauffagiste $this->denyUnlessAdminOrSecretaire();
if ($this->isGranted('ROLE_CHAUFFAGISTE') && $vehicle->getUser() !== $this->getUser()) {
throw $this->createAccessDeniedException('Vous ne pouvez pas supprimer ce véhicule.');
}
if ($this->isCsrfTokenValid('delete'.$vehicle->getId(), $request->get('csrf_token'))) { if ($this->isCsrfTokenValid('delete' . $vehicle->getId(), $request->request->get('_token'))) {
$entityManager->remove($vehicle); $entityManager->remove($vehicle);
$entityManager->flush(); $entityManager->flush();
} }
return $this->redirectToRoute('app_vehicle_index', [], Response::HTTP_SEE_OTHER); return $this->redirectToRoute('app_vehicle_index');
}
private function denyUnlessAdminOrSecretaire(): void
{
if (!$this->isGranted('ROLE_ADMIN') && !$this->isGranted('ROLE_SECRETAIRE')) {
throw $this->createAccessDeniedException();
}
} }
} }

View File

@ -202,6 +202,9 @@ class Intervention
#[ORM\Column(length: 255)] #[ORM\Column(length: 255)]
private ?string $Status = null; private ?string $Status = null;
#[ORM\Column(type: 'text', nullable: true)]
private ?string $Remarque = null;
#[ORM\ManyToOne(inversedBy: 'interventions')] #[ORM\ManyToOne(inversedBy: 'interventions')]
#[ORM\JoinColumn(nullable: false)] #[ORM\JoinColumn(nullable: false)]
private ?Utilisateur $user = null; private ?Utilisateur $user = null;
@ -355,5 +358,15 @@ class Intervention
{ {
return $this->Wording; return $this->Wording;
} }
public function getRemarque(): ?string
{
return $this->Remarque;
}
public function setRemarque(?string $Remarque): void
{
$this->Remarque = $Remarque;
}
} }

31
src/Form/RemarqueType.php Normal file
View File

@ -0,0 +1,31 @@
<?php
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use App\Entity\Intervention;
class RemarqueType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('Remarque', TextareaType::class, [
'label' => 'Ajouter une remarque',
'attr' => [
'rows' => 5,
'placeholder' => 'Renseignez les observations de lintervention…'
]
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Intervention::class,
]);
}
}

View File

@ -0,0 +1,18 @@
{% extends 'base.html.twig' %}
{% block title %}Ajouter une remarque{% endblock %}
{% block body %}
<h1 class="mb-4">📝 Ajouter une remarque à l'intervention #{{ intervention.id }}</h1>
{{ form_start(form) }}
{{ form_widget(form) }}
<button class="btn btn-primary mt-3">Enregistrer la remarque</button>
{{ form_end(form) }}
<div class="mt-3">
<a href="{{ path('app_intervention_show', {'id': intervention.id}) }}" class="btn btn-secondary">
← Retour à l'intervention
</a>
</div>
{% endblock %}

View File

@ -63,6 +63,11 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% if is_granted('ROLE_CHAUFFAGISTE') and intervention.user == app.user %}
<a href="{{ path('app_intervention_remarque', {'id': intervention.id}) }}" class="btn btn-outline-primary">
📝 Ajouter une remarque
</a>
{% endif %}
</tbody> </tbody>
</table> </table>