diff --git a/Inventory_frontend b/Inventory_frontend index 6bed715..cc70fe2 160000 --- a/Inventory_frontend +++ b/Inventory_frontend @@ -1 +1 @@ -Subproject commit 6bed715b7f5ec9aec171c623b6587e285fd41ed7 +Subproject commit cc70fe2b298adb975cf0fa093ee9094756403332 diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 0314b4a..ae8772b 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -29,33 +29,36 @@ security: success_handler: lexik_jwt_authentication.handler.authentication_success failure_handler: lexik_jwt_authentication.handler.authentication_failure - session_profile: - pattern: ^/api/session - stateless: false - - session_api: - pattern: ^/api/(sites|machines|documents|profiles) - stateless: false + session_public: + pattern: ^/api/session/profiles?$ + security: false api: pattern: ^/api - stateless: false + stateless: true + custom_authenticators: + - App\Security\SessionProfileAuthenticator main: lazy: true provider: app_user_provider + role_hierarchy: + ROLE_ADMIN: ROLE_GESTIONNAIRE + ROLE_GESTIONNAIRE: ROLE_VIEWER + ROLE_VIEWER: ROLE_USER + # Note: Only the *first* matching rule is applied access_control: - - { path: ^/api/session/profile, roles: PUBLIC_ACCESS } - - { path: ^/api/session/profiles, roles: PUBLIC_ACCESS } - - { path: ^/api, roles: PUBLIC_ACCESS } + - { path: ^/api/session/profile$, roles: PUBLIC_ACCESS } + - { path: ^/api/session/profiles, roles: PUBLIC_ACCESS, methods: [GET] } + - { path: ^/api/admin, roles: ROLE_ADMIN } - { path: ^/api/docs, roles: PUBLIC_ACCESS } - { path: ^/api/test, roles: PUBLIC_ACCESS } - { path: ^/docs, roles: PUBLIC_ACCESS } - { path: ^/contexts, roles: PUBLIC_ACCESS } - { path: ^/\.well-known, roles: PUBLIC_ACCESS } - - { path: ^/api, roles: IS_AUTHENTICATED_FULLY } + - { path: ^/api, roles: ROLE_VIEWER } when@test: security: diff --git a/src/Command/InitProfilePasswordsCommand.php b/src/Command/InitProfilePasswordsCommand.php new file mode 100644 index 0000000..fbce650 --- /dev/null +++ b/src/Command/InitProfilePasswordsCommand.php @@ -0,0 +1,85 @@ +profiles->findAll(); + + if (0 === count($all)) { + $io->warning('Aucun profil trouvé.'); + + return Command::SUCCESS; + } + + // Promote first profile to ROLE_ADMIN if none exists + $hasAdmin = false; + foreach ($all as $profile) { + if (in_array('ROLE_ADMIN', $profile->getRoles(), true)) { + $hasAdmin = true; + + break; + } + } + + $isFirst = true; + $count = 0; + foreach ($all as $profile) { + // Set password: first letter of firstName + "123" + $firstLetter = mb_strtoupper(mb_substr($profile->getFirstName(), 0, 1)); + $plain = $firstLetter.'123'; + $hashed = $this->passwordHasher->hashPassword($profile, $plain); + $profile->setPassword($hashed); + + // Set roles: first profile → ADMIN, others → VIEWER (minimum to use the app) + if (!$hasAdmin && $isFirst) { + $profile->setRoles(['ROLE_ADMIN']); + $io->writeln(sprintf(' %s %s → mdp: %s — ROLE_ADMIN', $profile->getFirstName(), $profile->getLastName(), $plain)); + $isFirst = false; + } elseif (in_array('ROLE_USER', $profile->getRoles(), true) && !in_array('ROLE_VIEWER', $profile->getRoles(), true) && !in_array('ROLE_GESTIONNAIRE', $profile->getRoles(), true) && !in_array('ROLE_ADMIN', $profile->getRoles(), true)) { + $profile->setRoles(['ROLE_VIEWER']); + $io->writeln(sprintf(' %s %s → mdp: %s — ROLE_VIEWER', $profile->getFirstName(), $profile->getLastName(), $plain)); + } else { + $io->writeln(sprintf(' %s %s → mdp: %s — %s', $profile->getFirstName(), $profile->getLastName(), $plain, implode(', ', $profile->getRoles()))); + } + + ++$count; + } + + $this->em->flush(); + + $io->success(sprintf('%d mot(s) de passe initialisé(s).', $count)); + + return Command::SUCCESS; + } +} diff --git a/src/Controller/ActivityLogController.php b/src/Controller/ActivityLogController.php index 10e030e..91d6545 100644 --- a/src/Controller/ActivityLogController.php +++ b/src/Controller/ActivityLogController.php @@ -7,11 +7,12 @@ namespace App\Controller; use App\Repository\AuditLogRepository; use App\Repository\ProfileRepository; use DateTimeInterface; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Attribute\Route; -final class ActivityLogController +final class ActivityLogController extends AbstractController { public function __construct( private readonly AuditLogRepository $auditLogs, @@ -21,6 +22,8 @@ final class ActivityLogController #[Route('/api/activity-logs', name: 'api_activity_logs', methods: ['GET'])] public function __invoke(Request $request): JsonResponse { + $this->denyAccessUnlessGranted('ROLE_VIEWER'); + $page = max(1, $request->query->getInt('page', 1)); $itemsPerPage = min(100, max(1, $request->query->getInt('itemsPerPage', 30))); diff --git a/src/Controller/AdminProfileController.php b/src/Controller/AdminProfileController.php new file mode 100644 index 0000000..b3171c3 --- /dev/null +++ b/src/Controller/AdminProfileController.php @@ -0,0 +1,193 @@ +denyAccessUnlessGranted('ROLE_ADMIN'); + + $items = $this->profiles->findBy([], ['firstName' => 'ASC']); + + return new JsonResponse(array_map([$this, 'serializeProfile'], $items)); + } + + #[Route('', name: 'admin_profiles_create', methods: ['POST'])] + public function create(Request $request): JsonResponse + { + $this->denyAccessUnlessGranted('ROLE_ADMIN'); + + $payload = $request->toArray(); + $firstName = trim((string) ($payload['firstName'] ?? '')); + $lastName = trim((string) ($payload['lastName'] ?? '')); + + if ('' === $firstName || '' === $lastName) { + return new JsonResponse(['message' => 'firstName et lastName sont requis.'], JsonResponse::HTTP_BAD_REQUEST); + } + + $email = trim((string) ($payload['email'] ?? '')); + $password = $payload['password'] ?? null; + $role = $payload['role'] ?? 'ROLE_VIEWER'; + + $allowedRoles = ['ROLE_ADMIN', 'ROLE_GESTIONNAIRE', 'ROLE_VIEWER', 'ROLE_USER']; + if (!in_array($role, $allowedRoles, true)) { + return new JsonResponse(['message' => 'Role invalide.'], JsonResponse::HTTP_BAD_REQUEST); + } + + $profile = new Profile(); + $profile->setFirstName($firstName); + $profile->setLastName($lastName); + $profile->setIsActive(true); + $profile->setRoles([$role]); + + if ('' !== $email) { + $profile->setEmail($email); + } + + if (null !== $password && '' !== $password) { + $profile->setPassword( + $this->passwordHasher->hashPassword($profile, $password) + ); + } + + $this->entityManager->persist($profile); + $this->entityManager->flush(); + + return new JsonResponse($this->serializeProfile($profile), JsonResponse::HTTP_CREATED); + } + + #[Route('/{id}/role', name: 'admin_profiles_update_role', methods: ['PUT'])] + public function updateRole(string $id, Request $request): JsonResponse + { + $this->denyAccessUnlessGranted('ROLE_ADMIN'); + + $profile = $this->profiles->find($id); + if (!$profile) { + return new JsonResponse(['message' => 'Profil introuvable.'], JsonResponse::HTTP_NOT_FOUND); + } + + $payload = $request->toArray(); + $role = $payload['role'] ?? null; + + $allowedRoles = ['ROLE_ADMIN', 'ROLE_GESTIONNAIRE', 'ROLE_VIEWER', 'ROLE_USER']; + if (!$role || !in_array($role, $allowedRoles, true)) { + return new JsonResponse(['message' => 'Role invalide.'], JsonResponse::HTTP_BAD_REQUEST); + } + + // Prevent removing the last admin + if (in_array('ROLE_ADMIN', $profile->getRoles(), true) && 'ROLE_ADMIN' !== $role) { + $adminCount = $this->countAdmins(); + if ($adminCount <= 1) { + return new JsonResponse( + ['message' => 'Impossible de retirer le dernier administrateur.'], + JsonResponse::HTTP_CONFLICT + ); + } + } + + $profile->setRoles([$role]); + $this->entityManager->flush(); + + return new JsonResponse($this->serializeProfile($profile)); + } + + #[Route('/{id}/password', name: 'admin_profiles_update_password', methods: ['PUT'])] + public function updatePassword(string $id, Request $request): JsonResponse + { + $this->denyAccessUnlessGranted('ROLE_ADMIN'); + + $profile = $this->profiles->find($id); + if (!$profile) { + return new JsonResponse(['message' => 'Profil introuvable.'], JsonResponse::HTTP_NOT_FOUND); + } + + $payload = $request->toArray(); + $password = $payload['password'] ?? ''; + + if ('' === $password) { + return new JsonResponse(['message' => 'Le mot de passe est requis.'], JsonResponse::HTTP_BAD_REQUEST); + } + + $profile->setPassword( + $this->passwordHasher->hashPassword($profile, $password) + ); + $this->entityManager->flush(); + + return new JsonResponse($this->serializeProfile($profile)); + } + + #[Route('/{id}/deactivate', name: 'admin_profiles_deactivate', methods: ['PUT'])] + public function deactivate(string $id): JsonResponse + { + $this->denyAccessUnlessGranted('ROLE_ADMIN'); + + $profile = $this->profiles->find($id); + if (!$profile) { + return new JsonResponse(['message' => 'Profil introuvable.'], JsonResponse::HTTP_NOT_FOUND); + } + + // Prevent deactivating the last admin + if (in_array('ROLE_ADMIN', $profile->getRoles(), true)) { + $adminCount = $this->countAdmins(); + if ($adminCount <= 1) { + return new JsonResponse( + ['message' => 'Impossible de desactiver le dernier administrateur.'], + JsonResponse::HTTP_CONFLICT + ); + } + } + + $profile->setIsActive(false); + $this->entityManager->flush(); + + return new JsonResponse($this->serializeProfile($profile)); + } + + private function serializeProfile(Profile $profile): array + { + return [ + 'id' => $profile->getId(), + 'firstName' => $profile->getFirstName(), + 'lastName' => $profile->getLastName(), + 'email' => $profile->getEmail(), + 'isActive' => $profile->isActive(), + 'hasPassword' => null !== $profile->getPassword() && '' !== $profile->getPassword(), + 'roles' => $profile->getRoles(), + 'createdAt' => $profile->getCreatedAt()->format('c'), + 'updatedAt' => $profile->getUpdatedAt()->format('c'), + ]; + } + + private function countAdmins(): int + { + $all = $this->profiles->findBy(['isActive' => true]); + + return count(array_filter( + $all, + static fn (Profile $p) => in_array('ROLE_ADMIN', $p->getRoles(), true) + )); + } +} diff --git a/src/Controller/ComposantHistoryController.php b/src/Controller/ComposantHistoryController.php index 2552a36..7610771 100644 --- a/src/Controller/ComposantHistoryController.php +++ b/src/Controller/ComposantHistoryController.php @@ -8,11 +8,12 @@ use App\Repository\AuditLogRepository; use App\Repository\ComposantRepository; use App\Repository\ProfileRepository; use DateTimeInterface; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; -final class ComposantHistoryController +final class ComposantHistoryController extends AbstractController { public function __construct( private readonly ComposantRepository $components, @@ -23,6 +24,8 @@ final class ComposantHistoryController #[Route('/api/composants/{id}/history', name: 'api_composant_history', methods: ['GET'])] public function __invoke(string $id): JsonResponse { + $this->denyAccessUnlessGranted('ROLE_VIEWER'); + $component = $this->components->find($id); if (!$component) { return new JsonResponse( diff --git a/src/Controller/CustomFieldValueController.php b/src/Controller/CustomFieldValueController.php index 13388bf..162ee4f 100644 --- a/src/Controller/CustomFieldValueController.php +++ b/src/Controller/CustomFieldValueController.php @@ -34,6 +34,8 @@ class CustomFieldValueController extends AbstractController #[Route('', name: 'custom_field_values_create', methods: ['POST'])] public function create(Request $request): JsonResponse { + $this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE'); + $payload = $this->decodePayload($request); if ($payload instanceof JsonResponse) { return $payload; @@ -63,6 +65,8 @@ class CustomFieldValueController extends AbstractController #[Route('/upsert', name: 'custom_field_values_upsert', methods: ['POST'])] public function upsert(Request $request): JsonResponse { + $this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE'); + $payload = $this->decodePayload($request); if ($payload instanceof JsonResponse) { return $payload; @@ -104,6 +108,8 @@ class CustomFieldValueController extends AbstractController #[Route('/{entityType}/{entityId}', name: 'custom_field_values_list', methods: ['GET'])] public function listByEntity(string $entityType, string $entityId): JsonResponse { + $this->denyAccessUnlessGranted('ROLE_VIEWER'); + $target = $this->resolveTarget([ 'entityType' => $entityType, 'entityId' => $entityId, @@ -126,6 +132,8 @@ class CustomFieldValueController extends AbstractController #[Route('/{id}', name: 'custom_field_values_update', methods: ['PATCH'])] public function update(string $id, Request $request): JsonResponse { + $this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE'); + $value = $this->customFieldValueRepository->find($id); if (!$value instanceof CustomFieldValue) { return $this->json(['success' => false, 'error' => 'Custom field value not found.'], 404); @@ -148,6 +156,8 @@ class CustomFieldValueController extends AbstractController #[Route('/{id}', name: 'custom_field_values_delete', methods: ['DELETE'])] public function delete(string $id): JsonResponse { + $this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE'); + $value = $this->customFieldValueRepository->find($id); if (!$value instanceof CustomFieldValue) { return $this->json(['success' => false, 'error' => 'Custom field value not found.'], 404); diff --git a/src/Controller/DocumentQueryController.php b/src/Controller/DocumentQueryController.php index 660dbd3..f59c512 100644 --- a/src/Controller/DocumentQueryController.php +++ b/src/Controller/DocumentQueryController.php @@ -30,6 +30,8 @@ class DocumentQueryController extends AbstractController #[Route('/site/{id}', name: 'documents_by_site', methods: ['GET'])] public function listBySite(string $id): JsonResponse { + $this->denyAccessUnlessGranted('ROLE_VIEWER'); + $site = $this->siteRepository->find($id); if (!$site) { return $this->json(['success' => false, 'error' => 'Site not found.'], 404); @@ -43,6 +45,8 @@ class DocumentQueryController extends AbstractController #[Route('/machine/{id}', name: 'documents_by_machine', methods: ['GET'])] public function listByMachine(string $id): JsonResponse { + $this->denyAccessUnlessGranted('ROLE_VIEWER'); + $machine = $this->machineRepository->find($id); if (!$machine) { return $this->json(['success' => false, 'error' => 'Machine not found.'], 404); @@ -56,6 +60,8 @@ class DocumentQueryController extends AbstractController #[Route('/composant/{id}', name: 'documents_by_composant', methods: ['GET'])] public function listByComposant(string $id): JsonResponse { + $this->denyAccessUnlessGranted('ROLE_VIEWER'); + $composant = $this->composantRepository->find($id); if (!$composant) { return $this->json(['success' => false, 'error' => 'Composant not found.'], 404); @@ -69,6 +75,8 @@ class DocumentQueryController extends AbstractController #[Route('/piece/{id}', name: 'documents_by_piece', methods: ['GET'])] public function listByPiece(string $id): JsonResponse { + $this->denyAccessUnlessGranted('ROLE_VIEWER'); + $piece = $this->pieceRepository->find($id); if (!$piece) { return $this->json(['success' => false, 'error' => 'Piece not found.'], 404); @@ -82,6 +90,8 @@ class DocumentQueryController extends AbstractController #[Route('/product/{id}', name: 'documents_by_product', methods: ['GET'])] public function listByProduct(string $id): JsonResponse { + $this->denyAccessUnlessGranted('ROLE_VIEWER'); + $product = $this->productRepository->find($id); if (!$product) { return $this->json(['success' => false, 'error' => 'Product not found.'], 404); diff --git a/src/Controller/MachineCustomFieldsController.php b/src/Controller/MachineCustomFieldsController.php index fea3b7f..39303d3 100644 --- a/src/Controller/MachineCustomFieldsController.php +++ b/src/Controller/MachineCustomFieldsController.php @@ -26,6 +26,8 @@ class MachineCustomFieldsController extends AbstractController #[Route('/{id}/add-custom-fields', name: 'machine_add_custom_fields', methods: ['POST'])] public function addMissingCustomFields(string $id): JsonResponse { + $this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE'); + $machine = $this->machineRepository->find($id); if (!$machine instanceof Machine) { return $this->json(['success' => false, 'error' => 'Machine not found.'], 404); diff --git a/src/Controller/MachineHistoryController.php b/src/Controller/MachineHistoryController.php index acc49bb..9d340b7 100644 --- a/src/Controller/MachineHistoryController.php +++ b/src/Controller/MachineHistoryController.php @@ -8,11 +8,12 @@ use App\Repository\AuditLogRepository; use App\Repository\MachineRepository; use App\Repository\ProfileRepository; use DateTimeInterface; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; -final class MachineHistoryController +final class MachineHistoryController extends AbstractController { public function __construct( private readonly MachineRepository $machines, @@ -23,6 +24,8 @@ final class MachineHistoryController #[Route('/api/machines/{id}/history', name: 'api_machine_history', methods: ['GET'])] public function __invoke(string $id): JsonResponse { + $this->denyAccessUnlessGranted('ROLE_VIEWER'); + $machine = $this->machines->find($id); if (!$machine) { return new JsonResponse( diff --git a/src/Controller/MachineSkeletonController.php b/src/Controller/MachineSkeletonController.php index a16697d..027aeeb 100644 --- a/src/Controller/MachineSkeletonController.php +++ b/src/Controller/MachineSkeletonController.php @@ -53,6 +53,8 @@ class MachineSkeletonController extends AbstractController #[Route('/{id}/skeleton', name: 'machine_skeleton_get', methods: ['GET'])] public function getSkeleton(string $id): JsonResponse { + $this->denyAccessUnlessGranted('ROLE_VIEWER'); + $machine = $this->machineRepository->find($id); if (!$machine instanceof Machine) { return $this->json(['success' => false, 'error' => 'Machine not found.'], 404); @@ -73,6 +75,8 @@ class MachineSkeletonController extends AbstractController #[Route('/{id}/skeleton', name: 'machine_skeleton_update', methods: ['PATCH'])] public function updateSkeleton(string $id, Request $request): JsonResponse { + $this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE'); + $machine = $this->machineRepository->find($id); if (!$machine instanceof Machine) { return $this->json(['success' => false, 'error' => 'Machine not found.'], 404); diff --git a/src/Controller/ModelTypeConversionController.php b/src/Controller/ModelTypeConversionController.php index 85bb715..92d5497 100644 --- a/src/Controller/ModelTypeConversionController.php +++ b/src/Controller/ModelTypeConversionController.php @@ -6,11 +6,12 @@ namespace App\Controller; use App\Repository\ModelTypeRepository; use App\Service\ModelTypeCategoryConversionService; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; -final class ModelTypeConversionController +final class ModelTypeConversionController extends AbstractController { public function __construct( private readonly ModelTypeRepository $modelTypes, @@ -20,6 +21,8 @@ final class ModelTypeConversionController #[Route('/api/model_types/{id}/conversion-check', name: 'api_model_type_conversion_check', methods: ['GET'])] public function check(string $id): JsonResponse { + $this->denyAccessUnlessGranted('ROLE_VIEWER'); + $modelType = $this->modelTypes->find($id); if (!$modelType) { @@ -35,6 +38,8 @@ final class ModelTypeConversionController #[Route('/api/model_types/{id}/convert', name: 'api_model_type_convert', methods: ['POST'])] public function convert(string $id): JsonResponse { + $this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE'); + $modelType = $this->modelTypes->find($id); if (!$modelType) { diff --git a/src/Controller/PieceHistoryController.php b/src/Controller/PieceHistoryController.php index 1392b8b..36e7aee 100644 --- a/src/Controller/PieceHistoryController.php +++ b/src/Controller/PieceHistoryController.php @@ -8,11 +8,12 @@ use App\Repository\AuditLogRepository; use App\Repository\PieceRepository; use App\Repository\ProfileRepository; use DateTimeInterface; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; -final class PieceHistoryController +final class PieceHistoryController extends AbstractController { public function __construct( private readonly PieceRepository $pieces, @@ -23,6 +24,8 @@ final class PieceHistoryController #[Route('/api/pieces/{id}/history', name: 'api_piece_history', methods: ['GET'])] public function __invoke(string $id): JsonResponse { + $this->denyAccessUnlessGranted('ROLE_VIEWER'); + $piece = $this->pieces->find($id); if (!$piece) { return new JsonResponse( diff --git a/src/Controller/ProductHistoryController.php b/src/Controller/ProductHistoryController.php index ce1986b..7d7b6d6 100644 --- a/src/Controller/ProductHistoryController.php +++ b/src/Controller/ProductHistoryController.php @@ -8,11 +8,12 @@ use App\Repository\AuditLogRepository; use App\Repository\ProductRepository; use App\Repository\ProfileRepository; use DateTimeInterface; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; -final class ProductHistoryController +final class ProductHistoryController extends AbstractController { public function __construct( private readonly ProductRepository $products, @@ -23,6 +24,8 @@ final class ProductHistoryController #[Route('/api/products/{id}/history', name: 'api_product_history', methods: ['GET'])] public function __invoke(string $id): JsonResponse { + $this->denyAccessUnlessGranted('ROLE_VIEWER'); + $product = $this->products->find($id); if (!$product) { return new JsonResponse( diff --git a/src/Controller/SessionProfileController.php b/src/Controller/SessionProfileController.php index 2c7d2ad..1079f53 100644 --- a/src/Controller/SessionProfileController.php +++ b/src/Controller/SessionProfileController.php @@ -8,11 +8,15 @@ use App\Repository\ProfileRepository; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Session\SessionInterface; +use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Routing\Attribute\Route; final class SessionProfileController { - public function __construct(private readonly ProfileRepository $profiles) {} + public function __construct( + private readonly ProfileRepository $profiles, + private readonly UserPasswordHasherInterface $passwordHasher, + ) {} #[Route('/api/session/profile', name: 'api_session_profile_get', methods: ['GET'])] public function getActiveProfile(Request $request): JsonResponse @@ -64,7 +68,24 @@ final class SessionProfileController return new JsonResponse(['message' => 'Profil introuvable ou inactif.'], JsonResponse::HTTP_UNAUTHORIZED); } + $password = $payload['password'] ?? ''; + if ('' === $password) { + return new JsonResponse(['message' => 'Mot de passe requis.'], JsonResponse::HTTP_BAD_REQUEST); + } + + if (!$profile->getPassword()) { + return new JsonResponse( + ['message' => 'Ce profil n\'a pas de mot de passe. Contactez un administrateur.'], + JsonResponse::HTTP_FORBIDDEN, + ); + } + + if (!$this->passwordHasher->isPasswordValid($profile, $password)) { + return new JsonResponse(['message' => 'Mot de passe incorrect.'], JsonResponse::HTTP_UNAUTHORIZED); + } + $session->set('profileId', $profile->getId()); + $session->set('profileRoles', $profile->getRoles()); return new JsonResponse([ 'id' => $profile->getId(), diff --git a/src/Controller/SessionProfilesController.php b/src/Controller/SessionProfilesController.php index 41217b2..71343b1 100644 --- a/src/Controller/SessionProfilesController.php +++ b/src/Controller/SessionProfilesController.php @@ -4,18 +4,14 @@ declare(strict_types=1); namespace App\Controller; -use App\Entity\Profile; use App\Repository\ProfileRepository; -use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Attribute\Route; final class SessionProfilesController { public function __construct( private readonly ProfileRepository $profiles, - private readonly EntityManagerInterface $entityManager ) {} #[Route('/api/session/profiles', name: 'api_session_profiles_list', methods: ['GET'])] @@ -29,52 +25,13 @@ final class SessionProfilesController ->getResult() ; - return new JsonResponse(array_map([$this, 'serializeProfile'], $items)); - } - - #[Route('/api/session/profiles', name: 'api_session_profiles_create', methods: ['POST'])] - public function create(Request $request): JsonResponse - { - $payload = $request->toArray(); - $firstName = trim((string) ($payload['firstName'] ?? '')); - $lastName = trim((string) ($payload['lastName'] ?? '')); - - if ('' === $firstName || '' === $lastName) { - return new JsonResponse(['message' => 'firstName et lastName sont requis.'], JsonResponse::HTTP_BAD_REQUEST); - } - - $profile = new Profile(); - $profile->setFirstName($firstName); - $profile->setLastName($lastName); - $profile->setIsActive(true); - - $this->entityManager->persist($profile); - $this->entityManager->flush(); - - return new JsonResponse($this->serializeProfile($profile), JsonResponse::HTTP_CREATED); - } - - #[Route('/api/session/profiles/{id}', name: 'api_session_profiles_delete', methods: ['DELETE'])] - public function delete(string $id): JsonResponse - { - $profile = $this->profiles->find($id); - if (!$profile) { - return new JsonResponse(['message' => 'Profil introuvable.'], JsonResponse::HTTP_NOT_FOUND); - } - - $profile->setIsActive(false); - $this->entityManager->flush(); - - return new JsonResponse(['success' => true]); - } - - private function serializeProfile(Profile $profile): array - { - return [ - 'id' => $profile->getId(), - 'firstName' => $profile->getFirstName(), - 'lastName' => $profile->getLastName(), - 'isActive' => $profile->isActive(), - ]; + return new JsonResponse(array_map(static function ($profile): array { + return [ + 'id' => $profile->getId(), + 'firstName' => $profile->getFirstName(), + 'lastName' => $profile->getLastName(), + 'hasPassword' => null !== $profile->getPassword() && '' !== $profile->getPassword(), + ]; + }, $items)); } } diff --git a/src/Entity/Composant.php b/src/Entity/Composant.php index 7552ca2..ffa0f66 100644 --- a/src/Entity/Composant.php +++ b/src/Entity/Composant.php @@ -8,6 +8,12 @@ use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Metadata\ApiFilter; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Metadata\Put; use App\Repository\ComposantRepository; use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; @@ -22,6 +28,14 @@ use Symfony\Component\Serializer\Attribute\Groups; #[ApiFilter(SearchFilter::class, properties: ['name' => 'ipartial', 'reference' => 'ipartial', 'typeComposant' => 'exact'])] #[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])] #[ApiResource( + operations: [ + new Get(security: "is_granted('ROLE_VIEWER')"), + new GetCollection(security: "is_granted('ROLE_VIEWER')"), + new Post(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Put(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"), + ], normalizationContext: ['groups' => ['composant:read']], paginationClientItemsPerPage: true, paginationMaximumItemsPerPage: 200 diff --git a/src/Entity/Constructeur.php b/src/Entity/Constructeur.php index d3cf224..16b81c2 100644 --- a/src/Entity/Constructeur.php +++ b/src/Entity/Constructeur.php @@ -5,6 +5,12 @@ declare(strict_types=1); namespace App\Entity; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Metadata\Put; use App\Repository\ConstructeurRepository; use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; @@ -16,6 +22,14 @@ use Doctrine\ORM\Mapping as ORM; #[ORM\Table(name: 'constructeurs')] #[ORM\HasLifecycleCallbacks] #[ApiResource( + operations: [ + new Get(security: "is_granted('ROLE_VIEWER')"), + new GetCollection(security: "is_granted('ROLE_VIEWER')"), + new Post(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Put(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"), + ], paginationClientItemsPerPage: true, paginationMaximumItemsPerPage: 200 )] diff --git a/src/Entity/CustomField.php b/src/Entity/CustomField.php index eb33089..4c95170 100644 --- a/src/Entity/CustomField.php +++ b/src/Entity/CustomField.php @@ -5,6 +5,12 @@ declare(strict_types=1); namespace App\Entity; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Metadata\Put; use App\Repository\CustomFieldRepository; use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; @@ -16,7 +22,16 @@ use Symfony\Component\Serializer\Attribute\Groups; #[ORM\Entity(repositoryClass: CustomFieldRepository::class)] #[ORM\Table(name: 'custom_fields')] #[ORM\HasLifecycleCallbacks] -#[ApiResource] +#[ApiResource( + operations: [ + new Get(security: "is_granted('ROLE_VIEWER')"), + new GetCollection(security: "is_granted('ROLE_VIEWER')"), + new Post(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Put(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"), + ] +)] class CustomField { #[ORM\Id] diff --git a/src/Entity/CustomFieldValue.php b/src/Entity/CustomFieldValue.php index 36a5514..1be1177 100644 --- a/src/Entity/CustomFieldValue.php +++ b/src/Entity/CustomFieldValue.php @@ -5,6 +5,12 @@ declare(strict_types=1); namespace App\Entity; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Metadata\Put; use App\Repository\CustomFieldValueRepository; use DateTimeImmutable; use Doctrine\DBAL\Types\Types; @@ -14,7 +20,16 @@ use Symfony\Component\Serializer\Attribute\Groups; #[ORM\Entity(repositoryClass: CustomFieldValueRepository::class)] #[ORM\Table(name: 'custom_field_values')] #[ORM\HasLifecycleCallbacks] -#[ApiResource] +#[ApiResource( + operations: [ + new Get(security: "is_granted('ROLE_VIEWER')"), + new GetCollection(security: "is_granted('ROLE_VIEWER')"), + new Post(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Put(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"), + ] +)] class CustomFieldValue { #[ORM\Id] diff --git a/src/Entity/Document.php b/src/Entity/Document.php index be7728b..b5c78d6 100644 --- a/src/Entity/Document.php +++ b/src/Entity/Document.php @@ -21,11 +21,17 @@ use Symfony\Component\Serializer\Attribute\Groups; #[ORM\HasLifecycleCallbacks] #[ApiResource( operations: [ - new GetCollection(normalizationContext: ['groups' => ['document:list']]), - new Get(normalizationContext: ['groups' => ['document:list', 'document:detail']]), - new Post(), - new Put(), - new Delete(), + new GetCollection( + security: "is_granted('ROLE_VIEWER')", + normalizationContext: ['groups' => ['document:list']], + ), + new Get( + security: "is_granted('ROLE_VIEWER')", + normalizationContext: ['groups' => ['document:list', 'document:detail']], + ), + new Post(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Put(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"), ], paginationClientItemsPerPage: true, paginationMaximumItemsPerPage: 200 diff --git a/src/Entity/Machine.php b/src/Entity/Machine.php index 72ad4f6..32687aa 100644 --- a/src/Entity/Machine.php +++ b/src/Entity/Machine.php @@ -5,6 +5,12 @@ declare(strict_types=1); namespace App\Entity; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Metadata\Put; use App\Repository\MachineRepository; use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; @@ -16,7 +22,16 @@ use Symfony\Component\Serializer\Attribute\Groups; #[ORM\Entity(repositoryClass: MachineRepository::class)] #[ORM\Table(name: 'machines')] #[ORM\HasLifecycleCallbacks] -#[ApiResource] +#[ApiResource( + operations: [ + new Get(security: "is_granted('ROLE_VIEWER')"), + new GetCollection(security: "is_granted('ROLE_VIEWER')"), + new Post(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Put(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"), + ] +)] class Machine { #[ORM\Id] diff --git a/src/Entity/MachineComponentLink.php b/src/Entity/MachineComponentLink.php index 89ea580..af7fb47 100644 --- a/src/Entity/MachineComponentLink.php +++ b/src/Entity/MachineComponentLink.php @@ -5,6 +5,12 @@ declare(strict_types=1); namespace App\Entity; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Metadata\Put; use App\Repository\MachineComponentLinkRepository; use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; @@ -15,7 +21,16 @@ use Doctrine\ORM\Mapping as ORM; #[ORM\Entity(repositoryClass: MachineComponentLinkRepository::class)] #[ORM\Table(name: 'machine_component_links')] #[ORM\HasLifecycleCallbacks] -#[ApiResource] +#[ApiResource( + operations: [ + new Get(security: "is_granted('ROLE_VIEWER')"), + new GetCollection(security: "is_granted('ROLE_VIEWER')"), + new Post(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Put(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"), + ] +)] class MachineComponentLink { #[ORM\Id] diff --git a/src/Entity/MachinePieceLink.php b/src/Entity/MachinePieceLink.php index 66b0cbf..5d94561 100644 --- a/src/Entity/MachinePieceLink.php +++ b/src/Entity/MachinePieceLink.php @@ -5,6 +5,12 @@ declare(strict_types=1); namespace App\Entity; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Metadata\Put; use App\Repository\MachinePieceLinkRepository; use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; @@ -15,7 +21,16 @@ use Doctrine\ORM\Mapping as ORM; #[ORM\Entity(repositoryClass: MachinePieceLinkRepository::class)] #[ORM\Table(name: 'machine_piece_links')] #[ORM\HasLifecycleCallbacks] -#[ApiResource] +#[ApiResource( + operations: [ + new Get(security: "is_granted('ROLE_VIEWER')"), + new GetCollection(security: "is_granted('ROLE_VIEWER')"), + new Post(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Put(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"), + ] +)] class MachinePieceLink { #[ORM\Id] diff --git a/src/Entity/MachineProductLink.php b/src/Entity/MachineProductLink.php index 029f4e1..e54da40 100644 --- a/src/Entity/MachineProductLink.php +++ b/src/Entity/MachineProductLink.php @@ -5,6 +5,12 @@ declare(strict_types=1); namespace App\Entity; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Metadata\Put; use App\Repository\MachineProductLinkRepository; use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; @@ -15,7 +21,16 @@ use Doctrine\ORM\Mapping as ORM; #[ORM\Entity(repositoryClass: MachineProductLinkRepository::class)] #[ORM\Table(name: 'machine_product_links')] #[ORM\HasLifecycleCallbacks] -#[ApiResource] +#[ApiResource( + operations: [ + new Get(security: "is_granted('ROLE_VIEWER')"), + new GetCollection(security: "is_granted('ROLE_VIEWER')"), + new Post(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Put(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"), + ] +)] class MachineProductLink { #[ORM\Id] diff --git a/src/Entity/ModelType.php b/src/Entity/ModelType.php index adce939..90e9f51 100644 --- a/src/Entity/ModelType.php +++ b/src/Entity/ModelType.php @@ -8,6 +8,12 @@ use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Metadata\ApiFilter; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Metadata\Put; use App\Enum\ModelCategory; use App\Repository\ModelTypeRepository; use DateTimeImmutable; @@ -24,6 +30,14 @@ use Symfony\Component\Serializer\Annotation\Groups; #[ApiFilter(SearchFilter::class, properties: ['category' => 'exact', 'name' => 'ipartial'])] #[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])] #[ApiResource( + operations: [ + new Get(security: "is_granted('ROLE_VIEWER')"), + new GetCollection(security: "is_granted('ROLE_VIEWER')"), + new Post(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Put(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"), + ], paginationClientItemsPerPage: true, paginationMaximumItemsPerPage: 200 )] diff --git a/src/Entity/Piece.php b/src/Entity/Piece.php index f8b4073..adf3b6f 100644 --- a/src/Entity/Piece.php +++ b/src/Entity/Piece.php @@ -8,6 +8,12 @@ use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Metadata\ApiFilter; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Metadata\Put; use App\Repository\PieceRepository; use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; @@ -22,6 +28,14 @@ use Symfony\Component\Serializer\Attribute\Groups; #[ApiFilter(SearchFilter::class, properties: ['name' => 'ipartial', 'reference' => 'ipartial', 'typePiece' => 'exact'])] #[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])] #[ApiResource( + operations: [ + new Get(security: "is_granted('ROLE_VIEWER')"), + new GetCollection(security: "is_granted('ROLE_VIEWER')"), + new Post(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Put(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"), + ], normalizationContext: ['groups' => ['piece:read']], paginationClientItemsPerPage: true, paginationMaximumItemsPerPage: 200 diff --git a/src/Entity/Product.php b/src/Entity/Product.php index fa45ecf..7c695a8 100644 --- a/src/Entity/Product.php +++ b/src/Entity/Product.php @@ -8,6 +8,12 @@ use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Metadata\ApiFilter; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Metadata\Put; use App\Repository\ProductRepository; use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; @@ -22,6 +28,14 @@ use Symfony\Component\Serializer\Attribute\Groups; #[ApiFilter(SearchFilter::class, properties: ['name' => 'ipartial', 'reference' => 'ipartial', 'typeProduct' => 'exact'])] #[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])] #[ApiResource( + operations: [ + new Get(security: "is_granted('ROLE_VIEWER')"), + new GetCollection(security: "is_granted('ROLE_VIEWER')"), + new Post(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Put(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"), + ], normalizationContext: ['groups' => ['product:read']], paginationClientItemsPerPage: true, paginationMaximumItemsPerPage: 200 diff --git a/src/Entity/Profile.php b/src/Entity/Profile.php index fd943c3..dad9747 100644 --- a/src/Entity/Profile.php +++ b/src/Entity/Profile.php @@ -8,9 +8,11 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; use ApiPlatform\Metadata\Put; use App\Repository\ProfileRepository; +use App\State\ProfilePasswordHasher; use DateTimeImmutable; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; @@ -24,11 +26,24 @@ use Symfony\Component\Validator\Constraints as Assert; #[ORM\HasLifecycleCallbacks] #[ApiResource( operations: [ - new Get(), - new GetCollection(), - new Post(), - new Put(), - new Delete(), + new Get(security: "is_granted('ROLE_VIEWER')"), + new GetCollection(security: "is_granted('ROLE_ADMIN')"), + new Post( + security: "is_granted('ROLE_ADMIN')", + denormalizationContext: ['groups' => ['profile:write', 'profile:admin:write']], + processor: ProfilePasswordHasher::class, + ), + new Put( + security: "is_granted('ROLE_ADMIN')", + denormalizationContext: ['groups' => ['profile:write', 'profile:admin:write']], + processor: ProfilePasswordHasher::class, + ), + new Patch( + security: "is_granted('ROLE_ADMIN')", + denormalizationContext: ['groups' => ['profile:write', 'profile:admin:write']], + processor: ProfilePasswordHasher::class, + ), + new Delete(security: "is_granted('ROLE_ADMIN')"), ], normalizationContext: ['groups' => ['profile:read']], denormalizationContext: ['groups' => ['profile:write']] @@ -63,16 +78,21 @@ class Profile implements UserInterface, PasswordAuthenticatedUserInterface * @var list The user roles */ #[ORM\Column(type: 'json', options: ['default' => '["ROLE_USER"]'])] - #[Groups(['profile:read', 'profile:write'])] + #[Groups(['profile:read', 'profile:admin:write'])] private array $roles = ['ROLE_USER']; /** - * @var string The hashed password + * @var null|string The hashed password */ #[ORM\Column(type: 'string', nullable: true)] - #[Groups(['profile:write'])] private ?string $password = null; + /** + * Non-persisted field used for password hashing via ProfilePasswordHasher. + */ + #[Groups(['profile:write'])] + private ?string $plainPassword = null; + #[ORM\Column(type: 'datetime_immutable', name: 'createdat')] #[Groups(['profile:read'])] private DateTimeImmutable $createdAt; @@ -83,7 +103,6 @@ class Profile implements UserInterface, PasswordAuthenticatedUserInterface public function __construct() { - // Générer un CUID-like ID pour compatibilité avec Prisma $this->id = 'cl'.substr(strtolower(base_convert(random_bytes(12), 2, 36)), 0, 24); $this->createdAt = new DateTimeImmutable(); $this->updatedAt = new DateTimeImmutable(); @@ -157,11 +176,10 @@ class Profile implements UserInterface, PasswordAuthenticatedUserInterface */ public function getRoles(): array { - $roles = $this->roles; - // guarantee every user at least has ROLE_USER + $roles = $this->roles; $roles[] = 'ROLE_USER'; - return array_unique($roles); + return array_values(array_unique($roles)); } /** @@ -182,20 +200,37 @@ class Profile implements UserInterface, PasswordAuthenticatedUserInterface return $this->password; } - public function setPassword(string $password): static + public function setPassword(?string $password): static { $this->password = $password; return $this; } + public function getPlainPassword(): ?string + { + return $this->plainPassword; + } + + public function setPlainPassword(?string $plainPassword): static + { + $this->plainPassword = $plainPassword; + + return $this; + } + + #[Groups(['profile:read'])] + public function getHasPassword(): bool + { + return null !== $this->password && '' !== $this->password; + } + /** * @see UserInterface */ public function eraseCredentials(): void { - // If you store any temporary, sensitive data on the user, clear it here - // $this->plainPassword = null; + $this->plainPassword = null; } public function getCreatedAt(): DateTimeImmutable diff --git a/src/Entity/Site.php b/src/Entity/Site.php index e1e8879..6eb4ae8 100644 --- a/src/Entity/Site.php +++ b/src/Entity/Site.php @@ -24,11 +24,11 @@ use Symfony\Component\Validator\Constraints as Assert; #[ORM\HasLifecycleCallbacks] #[ApiResource( operations: [ - new Get(), - new GetCollection(), - new Post(), - new Put(), - new Delete(), + new Get(security: "is_granted('ROLE_VIEWER')"), + new GetCollection(security: "is_granted('ROLE_VIEWER')"), + new Post(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Put(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"), ], paginationClientItemsPerPage: true, paginationMaximumItemsPerPage: 200 diff --git a/src/Entity/TypeMachine.php b/src/Entity/TypeMachine.php index 30ee9ae..0898fe5 100644 --- a/src/Entity/TypeMachine.php +++ b/src/Entity/TypeMachine.php @@ -27,11 +27,11 @@ use Symfony\Component\Validator\Constraints as Assert; #[UniqueEntity(fields: ['name'], message: 'Ce nom de type de machine existe déjà.')] #[ApiResource( operations: [ - new Get(), - new GetCollection(), - new Post(), - new Put(), - new Delete(), + new Get(security: "is_granted('ROLE_VIEWER')"), + new GetCollection(security: "is_granted('ROLE_VIEWER')"), + new Post(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Put(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"), ], paginationClientItemsPerPage: true, paginationMaximumItemsPerPage: 200 diff --git a/src/Entity/TypeMachineComponentRequirement.php b/src/Entity/TypeMachineComponentRequirement.php index be94926..80b87a5 100644 --- a/src/Entity/TypeMachineComponentRequirement.php +++ b/src/Entity/TypeMachineComponentRequirement.php @@ -6,6 +6,12 @@ namespace App\Entity; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Metadata\Put; use App\Repository\TypeMachineComponentRequirementRepository; use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; @@ -17,7 +23,16 @@ use Symfony\Component\Serializer\Annotation\Groups; #[ORM\Entity(repositoryClass: TypeMachineComponentRequirementRepository::class)] #[ORM\Table(name: 'type_machine_component_requirements')] #[ORM\HasLifecycleCallbacks] -#[ApiResource] +#[ApiResource( + operations: [ + new Get(security: "is_granted('ROLE_VIEWER')"), + new GetCollection(security: "is_granted('ROLE_VIEWER')"), + new Post(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Put(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"), + ] +)] class TypeMachineComponentRequirement { #[ORM\Id] diff --git a/src/Entity/TypeMachinePieceRequirement.php b/src/Entity/TypeMachinePieceRequirement.php index 64d24c8..d76bc83 100644 --- a/src/Entity/TypeMachinePieceRequirement.php +++ b/src/Entity/TypeMachinePieceRequirement.php @@ -6,6 +6,12 @@ namespace App\Entity; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Metadata\Put; use App\Repository\TypeMachinePieceRequirementRepository; use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; @@ -17,7 +23,16 @@ use Symfony\Component\Serializer\Annotation\Groups; #[ORM\Entity(repositoryClass: TypeMachinePieceRequirementRepository::class)] #[ORM\Table(name: 'type_machine_piece_requirements')] #[ORM\HasLifecycleCallbacks] -#[ApiResource] +#[ApiResource( + operations: [ + new Get(security: "is_granted('ROLE_VIEWER')"), + new GetCollection(security: "is_granted('ROLE_VIEWER')"), + new Post(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Put(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"), + ] +)] class TypeMachinePieceRequirement { #[ORM\Id] diff --git a/src/Entity/TypeMachineProductRequirement.php b/src/Entity/TypeMachineProductRequirement.php index 1a6c394..5b56a5a 100644 --- a/src/Entity/TypeMachineProductRequirement.php +++ b/src/Entity/TypeMachineProductRequirement.php @@ -6,6 +6,12 @@ namespace App\Entity; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Metadata\Put; use App\Repository\TypeMachineProductRequirementRepository; use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; @@ -17,7 +23,16 @@ use Symfony\Component\Serializer\Annotation\Groups; #[ORM\Entity(repositoryClass: TypeMachineProductRequirementRepository::class)] #[ORM\Table(name: 'type_machine_product_requirements')] #[ORM\HasLifecycleCallbacks] -#[ApiResource] +#[ApiResource( + operations: [ + new Get(security: "is_granted('ROLE_VIEWER')"), + new GetCollection(security: "is_granted('ROLE_VIEWER')"), + new Post(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Put(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"), + new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"), + ] +)] class TypeMachineProductRequirement { #[ORM\Id] diff --git a/src/Security/SessionProfileAuthenticator.php b/src/Security/SessionProfileAuthenticator.php new file mode 100644 index 0000000..ff6aa72 --- /dev/null +++ b/src/Security/SessionProfileAuthenticator.php @@ -0,0 +1,65 @@ +hasSession()) { + return false; + } + + return $request->getSession()->has('profileId'); + } + + public function authenticate(Request $request): Passport + { + $profileId = $request->getSession()->get('profileId'); + + return new SelfValidatingPassport( + new UserBadge($profileId, function (string $id): Profile { + $profile = $this->profiles->find($id); + + if (!$profile || !$profile->isActive()) { + throw new CustomUserMessageAuthenticationException('Profil introuvable ou inactif.'); + } + + return $profile; + }) + ); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response + { + // Let the request continue normally + return null; + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response + { + return new JsonResponse( + ['message' => $exception->getMessageKey()], + JsonResponse::HTTP_UNAUTHORIZED, + ); + } +} diff --git a/src/State/ProfilePasswordHasher.php b/src/State/ProfilePasswordHasher.php new file mode 100644 index 0000000..dcc029a --- /dev/null +++ b/src/State/ProfilePasswordHasher.php @@ -0,0 +1,36 @@ + $uriVariables + * @param array $context + */ + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed + { + if ($data instanceof Profile && $data->getPlainPassword()) { + $data->setPassword( + $this->passwordHasher->hashPassword($data, $data->getPlainPassword()) + ); + $data->eraseCredentials(); + } + + return $this->decorated->process($data, $operation, $uriVariables, $context); + } +}