feat(permissions) : add role-based access control system

Backend:
- Add role hierarchy (ADMIN > GESTIONNAIRE > VIEWER > USER) in security.yaml
- Add password authentication on profile activation (SessionProfileController)
- Add SessionProfileAuthenticator with stateless API firewall
- Add ProfilePasswordHasher state processor for API Platform
- Add security annotations on all 18 API Platform entities
- Add denyAccessUnlessGranted on all 13 custom controllers
- Add AdminProfileController for profile/role management (/api/admin/profiles)
- Add InitProfilePasswordsCommand for initial admin setup
- Simplify SessionProfilesController to list-only (removed create/delete)

Frontend (submodule update):
- Add usePermissions composable (isAdmin, canEdit, canView, isGranted)
- Add password login modal on profiles page
- Add admin backoffice page for profile management
- Disable all form fields for ROLE_VIEWER across all edit/create pages
- Show navigation buttons for all roles, hide destructive actions for viewers
- Add readonly mode to ModelTypeForm and site/constructeur modals
- Guard /admin routes in middleware
- Configure Vite proxy for API requests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-02-26 13:37:12 +01:00
parent adc44b99d3
commit a3e440c254
36 changed files with 762 additions and 110 deletions

View File

@@ -29,33 +29,36 @@ security:
success_handler: lexik_jwt_authentication.handler.authentication_success success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure failure_handler: lexik_jwt_authentication.handler.authentication_failure
session_profile: session_public:
pattern: ^/api/session pattern: ^/api/session/profiles?$
stateless: false security: false
session_api:
pattern: ^/api/(sites|machines|documents|profiles)
stateless: false
api: api:
pattern: ^/api pattern: ^/api
stateless: false stateless: true
custom_authenticators:
- App\Security\SessionProfileAuthenticator
main: main:
lazy: true lazy: true
provider: app_user_provider 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 # Note: Only the *first* matching rule is applied
access_control: access_control:
- { path: ^/api/session/profile, roles: PUBLIC_ACCESS } - { path: ^/api/session/profile$, roles: PUBLIC_ACCESS }
- { path: ^/api/session/profiles, roles: PUBLIC_ACCESS } - { path: ^/api/session/profiles, roles: PUBLIC_ACCESS, methods: [GET] }
- { path: ^/api, roles: PUBLIC_ACCESS } - { path: ^/api/admin, roles: ROLE_ADMIN }
- { path: ^/api/docs, roles: PUBLIC_ACCESS } - { path: ^/api/docs, roles: PUBLIC_ACCESS }
- { path: ^/api/test, roles: PUBLIC_ACCESS } - { path: ^/api/test, roles: PUBLIC_ACCESS }
- { path: ^/docs, roles: PUBLIC_ACCESS } - { path: ^/docs, roles: PUBLIC_ACCESS }
- { path: ^/contexts, roles: PUBLIC_ACCESS } - { path: ^/contexts, roles: PUBLIC_ACCESS }
- { path: ^/\.well-known, roles: PUBLIC_ACCESS } - { path: ^/\.well-known, roles: PUBLIC_ACCESS }
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY } - { path: ^/api, roles: ROLE_VIEWER }
when@test: when@test:
security: security:

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace App\Command;
use App\Repository\ProfileRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use function count;
use function in_array;
#[AsCommand(
name: 'app:init-profile-passwords',
description: 'Initialize all profile passwords to first letter of firstName + "123"',
)]
class InitProfilePasswordsCommand extends Command
{
public function __construct(
private readonly ProfileRepository $profiles,
private readonly EntityManagerInterface $em,
private readonly UserPasswordHasherInterface $passwordHasher,
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$all = $this->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;
}
}

View File

@@ -7,11 +7,12 @@ namespace App\Controller;
use App\Repository\AuditLogRepository; use App\Repository\AuditLogRepository;
use App\Repository\ProfileRepository; use App\Repository\ProfileRepository;
use DateTimeInterface; use DateTimeInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
final class ActivityLogController final class ActivityLogController extends AbstractController
{ {
public function __construct( public function __construct(
private readonly AuditLogRepository $auditLogs, private readonly AuditLogRepository $auditLogs,
@@ -21,6 +22,8 @@ final class ActivityLogController
#[Route('/api/activity-logs', name: 'api_activity_logs', methods: ['GET'])] #[Route('/api/activity-logs', name: 'api_activity_logs', methods: ['GET'])]
public function __invoke(Request $request): JsonResponse public function __invoke(Request $request): JsonResponse
{ {
$this->denyAccessUnlessGranted('ROLE_VIEWER');
$page = max(1, $request->query->getInt('page', 1)); $page = max(1, $request->query->getInt('page', 1));
$itemsPerPage = min(100, max(1, $request->query->getInt('itemsPerPage', 30))); $itemsPerPage = min(100, max(1, $request->query->getInt('itemsPerPage', 30)));

View File

@@ -0,0 +1,193 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Entity\Profile;
use App\Repository\ProfileRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;
use function count;
use function in_array;
#[Route('/api/admin/profiles')]
final class AdminProfileController extends AbstractController
{
public function __construct(
private readonly ProfileRepository $profiles,
private readonly EntityManagerInterface $entityManager,
private readonly UserPasswordHasherInterface $passwordHasher,
) {}
#[Route('', name: 'admin_profiles_list', methods: ['GET'])]
public function list(): JsonResponse
{
$this->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)
));
}
}

View File

@@ -8,11 +8,12 @@ use App\Repository\AuditLogRepository;
use App\Repository\ComposantRepository; use App\Repository\ComposantRepository;
use App\Repository\ProfileRepository; use App\Repository\ProfileRepository;
use DateTimeInterface; use DateTimeInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
final class ComposantHistoryController final class ComposantHistoryController extends AbstractController
{ {
public function __construct( public function __construct(
private readonly ComposantRepository $components, private readonly ComposantRepository $components,
@@ -23,6 +24,8 @@ final class ComposantHistoryController
#[Route('/api/composants/{id}/history', name: 'api_composant_history', methods: ['GET'])] #[Route('/api/composants/{id}/history', name: 'api_composant_history', methods: ['GET'])]
public function __invoke(string $id): JsonResponse public function __invoke(string $id): JsonResponse
{ {
$this->denyAccessUnlessGranted('ROLE_VIEWER');
$component = $this->components->find($id); $component = $this->components->find($id);
if (!$component) { if (!$component) {
return new JsonResponse( return new JsonResponse(

View File

@@ -34,6 +34,8 @@ class CustomFieldValueController extends AbstractController
#[Route('', name: 'custom_field_values_create', methods: ['POST'])] #[Route('', name: 'custom_field_values_create', methods: ['POST'])]
public function create(Request $request): JsonResponse public function create(Request $request): JsonResponse
{ {
$this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE');
$payload = $this->decodePayload($request); $payload = $this->decodePayload($request);
if ($payload instanceof JsonResponse) { if ($payload instanceof JsonResponse) {
return $payload; return $payload;
@@ -63,6 +65,8 @@ class CustomFieldValueController extends AbstractController
#[Route('/upsert', name: 'custom_field_values_upsert', methods: ['POST'])] #[Route('/upsert', name: 'custom_field_values_upsert', methods: ['POST'])]
public function upsert(Request $request): JsonResponse public function upsert(Request $request): JsonResponse
{ {
$this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE');
$payload = $this->decodePayload($request); $payload = $this->decodePayload($request);
if ($payload instanceof JsonResponse) { if ($payload instanceof JsonResponse) {
return $payload; return $payload;
@@ -104,6 +108,8 @@ class CustomFieldValueController extends AbstractController
#[Route('/{entityType}/{entityId}', name: 'custom_field_values_list', methods: ['GET'])] #[Route('/{entityType}/{entityId}', name: 'custom_field_values_list', methods: ['GET'])]
public function listByEntity(string $entityType, string $entityId): JsonResponse public function listByEntity(string $entityType, string $entityId): JsonResponse
{ {
$this->denyAccessUnlessGranted('ROLE_VIEWER');
$target = $this->resolveTarget([ $target = $this->resolveTarget([
'entityType' => $entityType, 'entityType' => $entityType,
'entityId' => $entityId, 'entityId' => $entityId,
@@ -126,6 +132,8 @@ class CustomFieldValueController extends AbstractController
#[Route('/{id}', name: 'custom_field_values_update', methods: ['PATCH'])] #[Route('/{id}', name: 'custom_field_values_update', methods: ['PATCH'])]
public function update(string $id, Request $request): JsonResponse public function update(string $id, Request $request): JsonResponse
{ {
$this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE');
$value = $this->customFieldValueRepository->find($id); $value = $this->customFieldValueRepository->find($id);
if (!$value instanceof CustomFieldValue) { if (!$value instanceof CustomFieldValue) {
return $this->json(['success' => false, 'error' => 'Custom field value not found.'], 404); 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'])] #[Route('/{id}', name: 'custom_field_values_delete', methods: ['DELETE'])]
public function delete(string $id): JsonResponse public function delete(string $id): JsonResponse
{ {
$this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE');
$value = $this->customFieldValueRepository->find($id); $value = $this->customFieldValueRepository->find($id);
if (!$value instanceof CustomFieldValue) { if (!$value instanceof CustomFieldValue) {
return $this->json(['success' => false, 'error' => 'Custom field value not found.'], 404); return $this->json(['success' => false, 'error' => 'Custom field value not found.'], 404);

View File

@@ -30,6 +30,8 @@ class DocumentQueryController extends AbstractController
#[Route('/site/{id}', name: 'documents_by_site', methods: ['GET'])] #[Route('/site/{id}', name: 'documents_by_site', methods: ['GET'])]
public function listBySite(string $id): JsonResponse public function listBySite(string $id): JsonResponse
{ {
$this->denyAccessUnlessGranted('ROLE_VIEWER');
$site = $this->siteRepository->find($id); $site = $this->siteRepository->find($id);
if (!$site) { if (!$site) {
return $this->json(['success' => false, 'error' => 'Site not found.'], 404); 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'])] #[Route('/machine/{id}', name: 'documents_by_machine', methods: ['GET'])]
public function listByMachine(string $id): JsonResponse public function listByMachine(string $id): JsonResponse
{ {
$this->denyAccessUnlessGranted('ROLE_VIEWER');
$machine = $this->machineRepository->find($id); $machine = $this->machineRepository->find($id);
if (!$machine) { if (!$machine) {
return $this->json(['success' => false, 'error' => 'Machine not found.'], 404); 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'])] #[Route('/composant/{id}', name: 'documents_by_composant', methods: ['GET'])]
public function listByComposant(string $id): JsonResponse public function listByComposant(string $id): JsonResponse
{ {
$this->denyAccessUnlessGranted('ROLE_VIEWER');
$composant = $this->composantRepository->find($id); $composant = $this->composantRepository->find($id);
if (!$composant) { if (!$composant) {
return $this->json(['success' => false, 'error' => 'Composant not found.'], 404); 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'])] #[Route('/piece/{id}', name: 'documents_by_piece', methods: ['GET'])]
public function listByPiece(string $id): JsonResponse public function listByPiece(string $id): JsonResponse
{ {
$this->denyAccessUnlessGranted('ROLE_VIEWER');
$piece = $this->pieceRepository->find($id); $piece = $this->pieceRepository->find($id);
if (!$piece) { if (!$piece) {
return $this->json(['success' => false, 'error' => 'Piece not found.'], 404); 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'])] #[Route('/product/{id}', name: 'documents_by_product', methods: ['GET'])]
public function listByProduct(string $id): JsonResponse public function listByProduct(string $id): JsonResponse
{ {
$this->denyAccessUnlessGranted('ROLE_VIEWER');
$product = $this->productRepository->find($id); $product = $this->productRepository->find($id);
if (!$product) { if (!$product) {
return $this->json(['success' => false, 'error' => 'Product not found.'], 404); return $this->json(['success' => false, 'error' => 'Product not found.'], 404);

View File

@@ -26,6 +26,8 @@ class MachineCustomFieldsController extends AbstractController
#[Route('/{id}/add-custom-fields', name: 'machine_add_custom_fields', methods: ['POST'])] #[Route('/{id}/add-custom-fields', name: 'machine_add_custom_fields', methods: ['POST'])]
public function addMissingCustomFields(string $id): JsonResponse public function addMissingCustomFields(string $id): JsonResponse
{ {
$this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE');
$machine = $this->machineRepository->find($id); $machine = $this->machineRepository->find($id);
if (!$machine instanceof Machine) { if (!$machine instanceof Machine) {
return $this->json(['success' => false, 'error' => 'Machine not found.'], 404); return $this->json(['success' => false, 'error' => 'Machine not found.'], 404);

View File

@@ -8,11 +8,12 @@ use App\Repository\AuditLogRepository;
use App\Repository\MachineRepository; use App\Repository\MachineRepository;
use App\Repository\ProfileRepository; use App\Repository\ProfileRepository;
use DateTimeInterface; use DateTimeInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
final class MachineHistoryController final class MachineHistoryController extends AbstractController
{ {
public function __construct( public function __construct(
private readonly MachineRepository $machines, private readonly MachineRepository $machines,
@@ -23,6 +24,8 @@ final class MachineHistoryController
#[Route('/api/machines/{id}/history', name: 'api_machine_history', methods: ['GET'])] #[Route('/api/machines/{id}/history', name: 'api_machine_history', methods: ['GET'])]
public function __invoke(string $id): JsonResponse public function __invoke(string $id): JsonResponse
{ {
$this->denyAccessUnlessGranted('ROLE_VIEWER');
$machine = $this->machines->find($id); $machine = $this->machines->find($id);
if (!$machine) { if (!$machine) {
return new JsonResponse( return new JsonResponse(

View File

@@ -53,6 +53,8 @@ class MachineSkeletonController extends AbstractController
#[Route('/{id}/skeleton', name: 'machine_skeleton_get', methods: ['GET'])] #[Route('/{id}/skeleton', name: 'machine_skeleton_get', methods: ['GET'])]
public function getSkeleton(string $id): JsonResponse public function getSkeleton(string $id): JsonResponse
{ {
$this->denyAccessUnlessGranted('ROLE_VIEWER');
$machine = $this->machineRepository->find($id); $machine = $this->machineRepository->find($id);
if (!$machine instanceof Machine) { if (!$machine instanceof Machine) {
return $this->json(['success' => false, 'error' => 'Machine not found.'], 404); 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'])] #[Route('/{id}/skeleton', name: 'machine_skeleton_update', methods: ['PATCH'])]
public function updateSkeleton(string $id, Request $request): JsonResponse public function updateSkeleton(string $id, Request $request): JsonResponse
{ {
$this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE');
$machine = $this->machineRepository->find($id); $machine = $this->machineRepository->find($id);
if (!$machine instanceof Machine) { if (!$machine instanceof Machine) {
return $this->json(['success' => false, 'error' => 'Machine not found.'], 404); return $this->json(['success' => false, 'error' => 'Machine not found.'], 404);

View File

@@ -6,11 +6,12 @@ namespace App\Controller;
use App\Repository\ModelTypeRepository; use App\Repository\ModelTypeRepository;
use App\Service\ModelTypeCategoryConversionService; use App\Service\ModelTypeCategoryConversionService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
final class ModelTypeConversionController final class ModelTypeConversionController extends AbstractController
{ {
public function __construct( public function __construct(
private readonly ModelTypeRepository $modelTypes, 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'])] #[Route('/api/model_types/{id}/conversion-check', name: 'api_model_type_conversion_check', methods: ['GET'])]
public function check(string $id): JsonResponse public function check(string $id): JsonResponse
{ {
$this->denyAccessUnlessGranted('ROLE_VIEWER');
$modelType = $this->modelTypes->find($id); $modelType = $this->modelTypes->find($id);
if (!$modelType) { if (!$modelType) {
@@ -35,6 +38,8 @@ final class ModelTypeConversionController
#[Route('/api/model_types/{id}/convert', name: 'api_model_type_convert', methods: ['POST'])] #[Route('/api/model_types/{id}/convert', name: 'api_model_type_convert', methods: ['POST'])]
public function convert(string $id): JsonResponse public function convert(string $id): JsonResponse
{ {
$this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE');
$modelType = $this->modelTypes->find($id); $modelType = $this->modelTypes->find($id);
if (!$modelType) { if (!$modelType) {

View File

@@ -8,11 +8,12 @@ use App\Repository\AuditLogRepository;
use App\Repository\PieceRepository; use App\Repository\PieceRepository;
use App\Repository\ProfileRepository; use App\Repository\ProfileRepository;
use DateTimeInterface; use DateTimeInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
final class PieceHistoryController final class PieceHistoryController extends AbstractController
{ {
public function __construct( public function __construct(
private readonly PieceRepository $pieces, private readonly PieceRepository $pieces,
@@ -23,6 +24,8 @@ final class PieceHistoryController
#[Route('/api/pieces/{id}/history', name: 'api_piece_history', methods: ['GET'])] #[Route('/api/pieces/{id}/history', name: 'api_piece_history', methods: ['GET'])]
public function __invoke(string $id): JsonResponse public function __invoke(string $id): JsonResponse
{ {
$this->denyAccessUnlessGranted('ROLE_VIEWER');
$piece = $this->pieces->find($id); $piece = $this->pieces->find($id);
if (!$piece) { if (!$piece) {
return new JsonResponse( return new JsonResponse(

View File

@@ -8,11 +8,12 @@ use App\Repository\AuditLogRepository;
use App\Repository\ProductRepository; use App\Repository\ProductRepository;
use App\Repository\ProfileRepository; use App\Repository\ProfileRepository;
use DateTimeInterface; use DateTimeInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
final class ProductHistoryController final class ProductHistoryController extends AbstractController
{ {
public function __construct( public function __construct(
private readonly ProductRepository $products, private readonly ProductRepository $products,
@@ -23,6 +24,8 @@ final class ProductHistoryController
#[Route('/api/products/{id}/history', name: 'api_product_history', methods: ['GET'])] #[Route('/api/products/{id}/history', name: 'api_product_history', methods: ['GET'])]
public function __invoke(string $id): JsonResponse public function __invoke(string $id): JsonResponse
{ {
$this->denyAccessUnlessGranted('ROLE_VIEWER');
$product = $this->products->find($id); $product = $this->products->find($id);
if (!$product) { if (!$product) {
return new JsonResponse( return new JsonResponse(

View File

@@ -8,11 +8,15 @@ use App\Repository\ProfileRepository;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
final class SessionProfileController 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'])] #[Route('/api/session/profile', name: 'api_session_profile_get', methods: ['GET'])]
public function getActiveProfile(Request $request): JsonResponse public function getActiveProfile(Request $request): JsonResponse
@@ -64,7 +68,24 @@ final class SessionProfileController
return new JsonResponse(['message' => 'Profil introuvable ou inactif.'], JsonResponse::HTTP_UNAUTHORIZED); 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('profileId', $profile->getId());
$session->set('profileRoles', $profile->getRoles());
return new JsonResponse([ return new JsonResponse([
'id' => $profile->getId(), 'id' => $profile->getId(),

View File

@@ -4,18 +4,14 @@ declare(strict_types=1);
namespace App\Controller; namespace App\Controller;
use App\Entity\Profile;
use App\Repository\ProfileRepository; use App\Repository\ProfileRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
final class SessionProfilesController final class SessionProfilesController
{ {
public function __construct( public function __construct(
private readonly ProfileRepository $profiles, private readonly ProfileRepository $profiles,
private readonly EntityManagerInterface $entityManager
) {} ) {}
#[Route('/api/session/profiles', name: 'api_session_profiles_list', methods: ['GET'])] #[Route('/api/session/profiles', name: 'api_session_profiles_list', methods: ['GET'])]
@@ -29,52 +25,13 @@ final class SessionProfilesController
->getResult() ->getResult()
; ;
return new JsonResponse(array_map([$this, 'serializeProfile'], $items)); return new JsonResponse(array_map(static function ($profile): array {
} return [
'id' => $profile->getId(),
#[Route('/api/session/profiles', name: 'api_session_profiles_create', methods: ['POST'])] 'firstName' => $profile->getFirstName(),
public function create(Request $request): JsonResponse 'lastName' => $profile->getLastName(),
{ 'hasPassword' => null !== $profile->getPassword() && '' !== $profile->getPassword(),
$payload = $request->toArray(); ];
$firstName = trim((string) ($payload['firstName'] ?? '')); }, $items));
$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(),
];
} }
} }

View File

@@ -8,6 +8,12 @@ use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Metadata\ApiFilter; use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource; 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 App\Repository\ComposantRepository;
use DateTimeImmutable; use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection; 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(SearchFilter::class, properties: ['name' => 'ipartial', 'reference' => 'ipartial', 'typeComposant' => 'exact'])]
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])] #[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])]
#[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')"),
],
normalizationContext: ['groups' => ['composant:read']], normalizationContext: ['groups' => ['composant:read']],
paginationClientItemsPerPage: true, paginationClientItemsPerPage: true,
paginationMaximumItemsPerPage: 200 paginationMaximumItemsPerPage: 200

View File

@@ -5,6 +5,12 @@ declare(strict_types=1);
namespace App\Entity; namespace App\Entity;
use ApiPlatform\Metadata\ApiResource; 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 App\Repository\ConstructeurRepository;
use DateTimeImmutable; use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
@@ -16,6 +22,14 @@ use Doctrine\ORM\Mapping as ORM;
#[ORM\Table(name: 'constructeurs')] #[ORM\Table(name: 'constructeurs')]
#[ORM\HasLifecycleCallbacks] #[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')"),
],
paginationClientItemsPerPage: true, paginationClientItemsPerPage: true,
paginationMaximumItemsPerPage: 200 paginationMaximumItemsPerPage: 200
)] )]

View File

@@ -5,6 +5,12 @@ declare(strict_types=1);
namespace App\Entity; namespace App\Entity;
use ApiPlatform\Metadata\ApiResource; 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 App\Repository\CustomFieldRepository;
use DateTimeImmutable; use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
@@ -16,7 +22,16 @@ use Symfony\Component\Serializer\Attribute\Groups;
#[ORM\Entity(repositoryClass: CustomFieldRepository::class)] #[ORM\Entity(repositoryClass: CustomFieldRepository::class)]
#[ORM\Table(name: 'custom_fields')] #[ORM\Table(name: 'custom_fields')]
#[ORM\HasLifecycleCallbacks] #[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 class CustomField
{ {
#[ORM\Id] #[ORM\Id]

View File

@@ -5,6 +5,12 @@ declare(strict_types=1);
namespace App\Entity; namespace App\Entity;
use ApiPlatform\Metadata\ApiResource; 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 App\Repository\CustomFieldValueRepository;
use DateTimeImmutable; use DateTimeImmutable;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
@@ -14,7 +20,16 @@ use Symfony\Component\Serializer\Attribute\Groups;
#[ORM\Entity(repositoryClass: CustomFieldValueRepository::class)] #[ORM\Entity(repositoryClass: CustomFieldValueRepository::class)]
#[ORM\Table(name: 'custom_field_values')] #[ORM\Table(name: 'custom_field_values')]
#[ORM\HasLifecycleCallbacks] #[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 class CustomFieldValue
{ {
#[ORM\Id] #[ORM\Id]

View File

@@ -21,11 +21,17 @@ use Symfony\Component\Serializer\Attribute\Groups;
#[ORM\HasLifecycleCallbacks] #[ORM\HasLifecycleCallbacks]
#[ApiResource( #[ApiResource(
operations: [ operations: [
new GetCollection(normalizationContext: ['groups' => ['document:list']]), new GetCollection(
new Get(normalizationContext: ['groups' => ['document:list', 'document:detail']]), security: "is_granted('ROLE_VIEWER')",
new Post(), normalizationContext: ['groups' => ['document:list']],
new Put(), ),
new Delete(), 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, paginationClientItemsPerPage: true,
paginationMaximumItemsPerPage: 200 paginationMaximumItemsPerPage: 200

View File

@@ -5,6 +5,12 @@ declare(strict_types=1);
namespace App\Entity; namespace App\Entity;
use ApiPlatform\Metadata\ApiResource; 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 App\Repository\MachineRepository;
use DateTimeImmutable; use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
@@ -16,7 +22,16 @@ use Symfony\Component\Serializer\Attribute\Groups;
#[ORM\Entity(repositoryClass: MachineRepository::class)] #[ORM\Entity(repositoryClass: MachineRepository::class)]
#[ORM\Table(name: 'machines')] #[ORM\Table(name: 'machines')]
#[ORM\HasLifecycleCallbacks] #[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 class Machine
{ {
#[ORM\Id] #[ORM\Id]

View File

@@ -5,6 +5,12 @@ declare(strict_types=1);
namespace App\Entity; namespace App\Entity;
use ApiPlatform\Metadata\ApiResource; 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 App\Repository\MachineComponentLinkRepository;
use DateTimeImmutable; use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
@@ -15,7 +21,16 @@ use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: MachineComponentLinkRepository::class)] #[ORM\Entity(repositoryClass: MachineComponentLinkRepository::class)]
#[ORM\Table(name: 'machine_component_links')] #[ORM\Table(name: 'machine_component_links')]
#[ORM\HasLifecycleCallbacks] #[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 class MachineComponentLink
{ {
#[ORM\Id] #[ORM\Id]

View File

@@ -5,6 +5,12 @@ declare(strict_types=1);
namespace App\Entity; namespace App\Entity;
use ApiPlatform\Metadata\ApiResource; 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 App\Repository\MachinePieceLinkRepository;
use DateTimeImmutable; use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
@@ -15,7 +21,16 @@ use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: MachinePieceLinkRepository::class)] #[ORM\Entity(repositoryClass: MachinePieceLinkRepository::class)]
#[ORM\Table(name: 'machine_piece_links')] #[ORM\Table(name: 'machine_piece_links')]
#[ORM\HasLifecycleCallbacks] #[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 class MachinePieceLink
{ {
#[ORM\Id] #[ORM\Id]

View File

@@ -5,6 +5,12 @@ declare(strict_types=1);
namespace App\Entity; namespace App\Entity;
use ApiPlatform\Metadata\ApiResource; 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 App\Repository\MachineProductLinkRepository;
use DateTimeImmutable; use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
@@ -15,7 +21,16 @@ use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: MachineProductLinkRepository::class)] #[ORM\Entity(repositoryClass: MachineProductLinkRepository::class)]
#[ORM\Table(name: 'machine_product_links')] #[ORM\Table(name: 'machine_product_links')]
#[ORM\HasLifecycleCallbacks] #[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 class MachineProductLink
{ {
#[ORM\Id] #[ORM\Id]

View File

@@ -8,6 +8,12 @@ use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Metadata\ApiFilter; use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource; 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\Enum\ModelCategory;
use App\Repository\ModelTypeRepository; use App\Repository\ModelTypeRepository;
use DateTimeImmutable; use DateTimeImmutable;
@@ -24,6 +30,14 @@ use Symfony\Component\Serializer\Annotation\Groups;
#[ApiFilter(SearchFilter::class, properties: ['category' => 'exact', 'name' => 'ipartial'])] #[ApiFilter(SearchFilter::class, properties: ['category' => 'exact', 'name' => 'ipartial'])]
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])] #[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])]
#[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')"),
],
paginationClientItemsPerPage: true, paginationClientItemsPerPage: true,
paginationMaximumItemsPerPage: 200 paginationMaximumItemsPerPage: 200
)] )]

View File

@@ -8,6 +8,12 @@ use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Metadata\ApiFilter; use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource; 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 App\Repository\PieceRepository;
use DateTimeImmutable; use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection; 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(SearchFilter::class, properties: ['name' => 'ipartial', 'reference' => 'ipartial', 'typePiece' => 'exact'])]
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])] #[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])]
#[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')"),
],
normalizationContext: ['groups' => ['piece:read']], normalizationContext: ['groups' => ['piece:read']],
paginationClientItemsPerPage: true, paginationClientItemsPerPage: true,
paginationMaximumItemsPerPage: 200 paginationMaximumItemsPerPage: 200

View File

@@ -8,6 +8,12 @@ use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Metadata\ApiFilter; use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource; 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 App\Repository\ProductRepository;
use DateTimeImmutable; use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection; 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(SearchFilter::class, properties: ['name' => 'ipartial', 'reference' => 'ipartial', 'typeProduct' => 'exact'])]
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])] #[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])]
#[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')"),
],
normalizationContext: ['groups' => ['product:read']], normalizationContext: ['groups' => ['product:read']],
paginationClientItemsPerPage: true, paginationClientItemsPerPage: true,
paginationMaximumItemsPerPage: 200 paginationMaximumItemsPerPage: 200

View File

@@ -8,9 +8,11 @@ use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post; use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put; use ApiPlatform\Metadata\Put;
use App\Repository\ProfileRepository; use App\Repository\ProfileRepository;
use App\State\ProfilePasswordHasher;
use DateTimeImmutable; use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
@@ -24,11 +26,24 @@ use Symfony\Component\Validator\Constraints as Assert;
#[ORM\HasLifecycleCallbacks] #[ORM\HasLifecycleCallbacks]
#[ApiResource( #[ApiResource(
operations: [ operations: [
new Get(), new Get(security: "is_granted('ROLE_VIEWER')"),
new GetCollection(), new GetCollection(security: "is_granted('ROLE_ADMIN')"),
new Post(), new Post(
new Put(), security: "is_granted('ROLE_ADMIN')",
new Delete(), 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']], normalizationContext: ['groups' => ['profile:read']],
denormalizationContext: ['groups' => ['profile:write']] denormalizationContext: ['groups' => ['profile:write']]
@@ -63,16 +78,21 @@ class Profile implements UserInterface, PasswordAuthenticatedUserInterface
* @var list<string> The user roles * @var list<string> The user roles
*/ */
#[ORM\Column(type: 'json', options: ['default' => '["ROLE_USER"]'])] #[ORM\Column(type: 'json', options: ['default' => '["ROLE_USER"]'])]
#[Groups(['profile:read', 'profile:write'])] #[Groups(['profile:read', 'profile:admin:write'])]
private array $roles = ['ROLE_USER']; private array $roles = ['ROLE_USER'];
/** /**
* @var string The hashed password * @var null|string The hashed password
*/ */
#[ORM\Column(type: 'string', nullable: true)] #[ORM\Column(type: 'string', nullable: true)]
#[Groups(['profile:write'])]
private ?string $password = null; 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')] #[ORM\Column(type: 'datetime_immutable', name: 'createdat')]
#[Groups(['profile:read'])] #[Groups(['profile:read'])]
private DateTimeImmutable $createdAt; private DateTimeImmutable $createdAt;
@@ -83,7 +103,6 @@ class Profile implements UserInterface, PasswordAuthenticatedUserInterface
public function __construct() 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->id = 'cl'.substr(strtolower(base_convert(random_bytes(12), 2, 36)), 0, 24);
$this->createdAt = new DateTimeImmutable(); $this->createdAt = new DateTimeImmutable();
$this->updatedAt = new DateTimeImmutable(); $this->updatedAt = new DateTimeImmutable();
@@ -157,11 +176,10 @@ class Profile implements UserInterface, PasswordAuthenticatedUserInterface
*/ */
public function getRoles(): array public function getRoles(): array
{ {
$roles = $this->roles; $roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER'; $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; return $this->password;
} }
public function setPassword(string $password): static public function setPassword(?string $password): static
{ {
$this->password = $password; $this->password = $password;
return $this; 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 * @see UserInterface
*/ */
public function eraseCredentials(): void 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 public function getCreatedAt(): DateTimeImmutable

View File

@@ -24,11 +24,11 @@ use Symfony\Component\Validator\Constraints as Assert;
#[ORM\HasLifecycleCallbacks] #[ORM\HasLifecycleCallbacks]
#[ApiResource( #[ApiResource(
operations: [ operations: [
new Get(), new Get(security: "is_granted('ROLE_VIEWER')"),
new GetCollection(), new GetCollection(security: "is_granted('ROLE_VIEWER')"),
new Post(), new Post(security: "is_granted('ROLE_GESTIONNAIRE')"),
new Put(), new Put(security: "is_granted('ROLE_GESTIONNAIRE')"),
new Delete(), new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"),
], ],
paginationClientItemsPerPage: true, paginationClientItemsPerPage: true,
paginationMaximumItemsPerPage: 200 paginationMaximumItemsPerPage: 200

View File

@@ -27,11 +27,11 @@ use Symfony\Component\Validator\Constraints as Assert;
#[UniqueEntity(fields: ['name'], message: 'Ce nom de type de machine existe déjà.')] #[UniqueEntity(fields: ['name'], message: 'Ce nom de type de machine existe déjà.')]
#[ApiResource( #[ApiResource(
operations: [ operations: [
new Get(), new Get(security: "is_granted('ROLE_VIEWER')"),
new GetCollection(), new GetCollection(security: "is_granted('ROLE_VIEWER')"),
new Post(), new Post(security: "is_granted('ROLE_GESTIONNAIRE')"),
new Put(), new Put(security: "is_granted('ROLE_GESTIONNAIRE')"),
new Delete(), new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"),
], ],
paginationClientItemsPerPage: true, paginationClientItemsPerPage: true,
paginationMaximumItemsPerPage: 200 paginationMaximumItemsPerPage: 200

View File

@@ -6,6 +6,12 @@ namespace App\Entity;
use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource; 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 App\Repository\TypeMachineComponentRequirementRepository;
use DateTimeImmutable; use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
@@ -17,7 +23,16 @@ use Symfony\Component\Serializer\Annotation\Groups;
#[ORM\Entity(repositoryClass: TypeMachineComponentRequirementRepository::class)] #[ORM\Entity(repositoryClass: TypeMachineComponentRequirementRepository::class)]
#[ORM\Table(name: 'type_machine_component_requirements')] #[ORM\Table(name: 'type_machine_component_requirements')]
#[ORM\HasLifecycleCallbacks] #[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 class TypeMachineComponentRequirement
{ {
#[ORM\Id] #[ORM\Id]

View File

@@ -6,6 +6,12 @@ namespace App\Entity;
use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource; 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 App\Repository\TypeMachinePieceRequirementRepository;
use DateTimeImmutable; use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
@@ -17,7 +23,16 @@ use Symfony\Component\Serializer\Annotation\Groups;
#[ORM\Entity(repositoryClass: TypeMachinePieceRequirementRepository::class)] #[ORM\Entity(repositoryClass: TypeMachinePieceRequirementRepository::class)]
#[ORM\Table(name: 'type_machine_piece_requirements')] #[ORM\Table(name: 'type_machine_piece_requirements')]
#[ORM\HasLifecycleCallbacks] #[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 class TypeMachinePieceRequirement
{ {
#[ORM\Id] #[ORM\Id]

View File

@@ -6,6 +6,12 @@ namespace App\Entity;
use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource; 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 App\Repository\TypeMachineProductRequirementRepository;
use DateTimeImmutable; use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
@@ -17,7 +23,16 @@ use Symfony\Component\Serializer\Annotation\Groups;
#[ORM\Entity(repositoryClass: TypeMachineProductRequirementRepository::class)] #[ORM\Entity(repositoryClass: TypeMachineProductRequirementRepository::class)]
#[ORM\Table(name: 'type_machine_product_requirements')] #[ORM\Table(name: 'type_machine_product_requirements')]
#[ORM\HasLifecycleCallbacks] #[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 class TypeMachineProductRequirement
{ {
#[ORM\Id] #[ORM\Id]

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace App\Security;
use App\Entity\Profile;
use App\Repository\ProfileRepository;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
final class SessionProfileAuthenticator extends AbstractAuthenticator
{
public function __construct(
private readonly ProfileRepository $profiles,
) {}
public function supports(Request $request): ?bool
{
if (!$request->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,
);
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\Profile;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
final class ProfilePasswordHasher implements ProcessorInterface
{
public function __construct(
#[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')]
private readonly ProcessorInterface $decorated,
private readonly UserPasswordHasherInterface $passwordHasher,
) {}
/**
* @param array<string, mixed> $uriVariables
* @param array<string, mixed> $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);
}
}