From 72b40e965a2bfb8f56719aa1fd8f83f0472e7d24 Mon Sep 17 00:00:00 2001 From: Charles Date: Mon, 16 Feb 2026 16:52:12 +0100 Subject: [PATCH 01/19] renamed entity --- migrations/Version20260216155056.php | 53 +++++++++++++++++++ src/Command/DeleteRoleCommand.php | 2 +- src/Controller/OrganizationController.php | 2 +- src/Controller/UserController.php | 2 +- .../api/Check/EasyCheckController.php | 20 +++++++ src/Entity/Apps.php | 10 ++-- src/Entity/Organizations.php | 10 ++-- ...nizatonApp.php => UserOrganizationApp.php} | 20 ++----- src/Entity/UsersOrganizations.php | 10 ++-- .../UserOrganizatonAppRepository.php | 6 +-- src/Service/OrganizationsService.php | 4 +- src/Service/UserOrganizationAppService.php | 20 +++---- src/Service/UserService.php | 4 +- tests/Controller/OrganizationController.php | 6 +-- tests/Controller/UserController.php | 4 +- tests/Functional/AbstractFunctional.php | 6 +-- tests/Service/OrganizationsServiceTest.php | 16 +++--- .../UserOrganizationAppServiceTest.php | 20 +++---- tests/Service/UserServiceTest.php | 6 +-- 19 files changed, 140 insertions(+), 81 deletions(-) create mode 100644 migrations/Version20260216155056.php create mode 100644 src/Controller/api/Check/EasyCheckController.php rename src/Entity/{UserOrganizatonApp.php => UserOrganizationApp.php} (82%) diff --git a/migrations/Version20260216155056.php b/migrations/Version20260216155056.php new file mode 100644 index 0000000..55d547c --- /dev/null +++ b/migrations/Version20260216155056.php @@ -0,0 +1,53 @@ +addSql('DROP SEQUENCE user_organizaton_app_id_seq CASCADE'); + $this->addSql('CREATE TABLE user_organization_app (id SERIAL NOT NULL, user_organization_id INT NOT NULL, application_id INT NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, is_active BOOLEAN NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_BEF66DF12014CF51 ON user_organization_app (user_organization_id)'); + $this->addSql('CREATE INDEX IDX_BEF66DF13E030ACD ON user_organization_app (application_id)'); + $this->addSql('COMMENT ON COLUMN user_organization_app.created_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE user_organization_app ADD CONSTRAINT FK_BEF66DF12014CF51 FOREIGN KEY (user_organization_id) REFERENCES users_organizations (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE user_organization_app ADD CONSTRAINT FK_BEF66DF13E030ACD FOREIGN KEY (application_id) REFERENCES apps (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE user_organizaton_app DROP CONSTRAINT fk_2c952fc72014cf51'); + $this->addSql('ALTER TABLE user_organizaton_app DROP CONSTRAINT fk_2c952fc73e030acd'); + $this->addSql('ALTER TABLE user_organizaton_app DROP CONSTRAINT fk_2c952fc7d60322ac'); + $this->addSql('DROP TABLE user_organizaton_app'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('CREATE SEQUENCE user_organizaton_app_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE user_organizaton_app (id SERIAL NOT NULL, user_organization_id INT NOT NULL, role_id INT DEFAULT NULL, application_id INT NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, is_active BOOLEAN NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX idx_2c952fc72014cf51 ON user_organizaton_app (user_organization_id)'); + $this->addSql('CREATE INDEX idx_2c952fc73e030acd ON user_organizaton_app (application_id)'); + $this->addSql('CREATE INDEX idx_2c952fc7d60322ac ON user_organizaton_app (role_id)'); + $this->addSql('COMMENT ON COLUMN user_organizaton_app.created_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE user_organizaton_app ADD CONSTRAINT fk_2c952fc72014cf51 FOREIGN KEY (user_organization_id) REFERENCES users_organizations (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE user_organizaton_app ADD CONSTRAINT fk_2c952fc73e030acd FOREIGN KEY (application_id) REFERENCES apps (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE user_organizaton_app ADD CONSTRAINT fk_2c952fc7d60322ac FOREIGN KEY (role_id) REFERENCES roles (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE user_organization_app DROP CONSTRAINT FK_BEF66DF12014CF51'); + $this->addSql('ALTER TABLE user_organization_app DROP CONSTRAINT FK_BEF66DF13E030ACD'); + $this->addSql('DROP TABLE user_organization_app'); + } +} diff --git a/src/Command/DeleteRoleCommand.php b/src/Command/DeleteRoleCommand.php index de29992..f99eed3 100644 --- a/src/Command/DeleteRoleCommand.php +++ b/src/Command/DeleteRoleCommand.php @@ -51,7 +51,7 @@ class DeleteRoleCommand extends Command } // Check if role is being used (optional safety check) - $usageCount = $this->entityManager->getRepository(\App\Entity\UserOrganizatonApp::class) + $usageCount = $this->entityManager->getRepository(\App\Entity\UserOrganizationApp::class) ->count(['role' => $role]); if ($usageCount > 0) { diff --git a/src/Controller/OrganizationController.php b/src/Controller/OrganizationController.php index b36cc29..fddc367 100644 --- a/src/Controller/OrganizationController.php +++ b/src/Controller/OrganizationController.php @@ -6,7 +6,7 @@ use App\Entity\Actions; use App\Entity\Apps; use App\Entity\Roles; use App\Entity\User; -use App\Entity\UserOrganizatonApp; +use App\Entity\UserOrganizationApp; use App\Entity\UsersOrganizations; use App\Form\OrganizationForm; use App\Repository\OrganizationsRepository; diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 64fade1..d7cf277 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -5,7 +5,7 @@ namespace App\Controller; use App\Entity\Apps; use App\Entity\Roles; use App\Entity\User; -use App\Entity\UserOrganizatonApp; +use App\Entity\UserOrganizationApp; use App\Entity\UsersOrganizations; use App\Form\UserForm; use App\Repository\AppsRepository; diff --git a/src/Controller/api/Check/EasyCheckController.php b/src/Controller/api/Check/EasyCheckController.php new file mode 100644 index 0000000..ffc7990 --- /dev/null +++ b/src/Controller/api/Check/EasyCheckController.php @@ -0,0 +1,20 @@ +render('easy_check/index.html.twig'); + } +} diff --git a/src/Entity/Apps.php b/src/Entity/Apps.php index 99c6f7e..4b97a39 100644 --- a/src/Entity/Apps.php +++ b/src/Entity/Apps.php @@ -38,9 +38,9 @@ class Apps private ?string $descriptionSmall = null; /** - * @var Collection + * @var Collection */ - #[ORM\OneToMany(targetEntity: UserOrganizatonApp::class, mappedBy: 'application')] + #[ORM\OneToMany(targetEntity: UserOrganizationApp::class, mappedBy: 'application')] private Collection $userOrganizatonApps; /** @@ -152,14 +152,14 @@ class Apps } /** - * @return Collection + * @return Collection */ public function getUserOrganizatonApps(): Collection { return $this->userOrganizatonApps; } - public function addUserOrganizatonApp(UserOrganizatonApp $userOrganizatonApp): static + public function addUserOrganizatonApp(UserOrganizationApp $userOrganizatonApp): static { if (!$this->userOrganizatonApps->contains($userOrganizatonApp)) { $this->userOrganizatonApps->add($userOrganizatonApp); @@ -169,7 +169,7 @@ class Apps return $this; } - public function removeUserOrganizatonApp(UserOrganizatonApp $userOrganizatonApp): static + public function removeUserOrganizatonApp(UserOrganizationApp $userOrganizatonApp): static { if ($this->userOrganizatonApps->removeElement($userOrganizatonApp)) { if ($userOrganizatonApp->getApplication() === $this) { diff --git a/src/Entity/Organizations.php b/src/Entity/Organizations.php index 470316d..9080665 100644 --- a/src/Entity/Organizations.php +++ b/src/Entity/Organizations.php @@ -56,9 +56,9 @@ class Organizations private Collection $actions; /** - * @var Collection + * @var Collection */ - #[ORM\OneToMany(targetEntity: UserOrganizatonApp::class, mappedBy: 'organization')] + #[ORM\OneToMany(targetEntity: UserOrganizationApp::class, mappedBy: 'organization')] private Collection $userOrganizatonApps; #[ORM\Column(length: 4, nullable: true)] @@ -238,14 +238,14 @@ class Organizations } /** - * @return Collection + * @return Collection */ public function getUserOrganizatonApps(): Collection { return $this->userOrganizatonApps; } - public function addUserOrganizatonApp(UserOrganizatonApp $userOrganizatonApp): static + public function addUserOrganizatonApp(UserOrganizationApp $userOrganizatonApp): static { if (!$this->userOrganizatonApps->contains($userOrganizatonApp)) { $this->userOrganizatonApps->add($userOrganizatonApp); @@ -255,7 +255,7 @@ class Organizations return $this; } - public function removeUserOrganizatonApp(UserOrganizatonApp $userOrganizatonApp): static + public function removeUserOrganizatonApp(UserOrganizationApp $userOrganizatonApp): static { if ($this->userOrganizatonApps->removeElement($userOrganizatonApp)) { // set the owning side to null (unless already changed) diff --git a/src/Entity/UserOrganizatonApp.php b/src/Entity/UserOrganizationApp.php similarity index 82% rename from src/Entity/UserOrganizatonApp.php rename to src/Entity/UserOrganizationApp.php index ee79cd5..d4a6a35 100644 --- a/src/Entity/UserOrganizatonApp.php +++ b/src/Entity/UserOrganizationApp.php @@ -6,7 +6,7 @@ use App\Repository\UserOrganizatonAppRepository; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity(repositoryClass: UserOrganizatonAppRepository::class)] -class UserOrganizatonApp +class UserOrganizationApp { #[ORM\Id] #[ORM\GeneratedValue] @@ -16,17 +16,15 @@ class UserOrganizatonApp #[ORM\Column] private ?\DateTimeImmutable $createdAt = null; - #[ORM\ManyToOne] - private ?Roles $role = null; #[ORM\Column] private ?bool $isActive; - #[ORM\ManyToOne(inversedBy: 'userOrganizatonApps')] + #[ORM\ManyToOne(inversedBy: 'userOrganizationApps')] #[ORM\JoinColumn(nullable: false)] private ?UsersOrganizations $userOrganization = null; - #[ORM\ManyToOne(inversedBy: 'userOrganizatonApps')] + #[ORM\ManyToOne(inversedBy: 'userOrganizationApps')] #[ORM\JoinColumn(nullable: false)] private ?Apps $application = null; @@ -53,18 +51,6 @@ class UserOrganizatonApp return $this; } - public function getRole(): ?Roles - { - return $this->role; - } - - public function setRole(?Roles $role): static - { - $this->role = $role; - - return $this; - } - public function isActive(): ?bool { return $this->isActive; diff --git a/src/Entity/UsersOrganizations.php b/src/Entity/UsersOrganizations.php index efe84e0..300debb 100644 --- a/src/Entity/UsersOrganizations.php +++ b/src/Entity/UsersOrganizations.php @@ -30,9 +30,9 @@ class UsersOrganizations private ?\DateTimeImmutable $createdAt = null; /** - * @var Collection + * @var Collection */ - #[ORM\OneToMany(targetEntity: UserOrganizatonApp::class, mappedBy: 'userOrganization')] + #[ORM\OneToMany(targetEntity: UserOrganizationApp::class, mappedBy: 'userOrganization')] private Collection $userOrganizatonApps; #[ORM\Column(length: 255, nullable: true)] @@ -98,14 +98,14 @@ class UsersOrganizations } /** - * @return Collection + * @return Collection */ public function getUserOrganizatonApps(): Collection { return $this->userOrganizatonApps; } - public function addUserOrganizatonApp(UserOrganizatonApp $userOrganizatonApp): static + public function addUserOrganizatonApp(UserOrganizationApp $userOrganizatonApp): static { if (!$this->userOrganizatonApps->contains($userOrganizatonApp)) { $this->userOrganizatonApps->add($userOrganizatonApp); @@ -115,7 +115,7 @@ class UsersOrganizations return $this; } - public function removeUserOrganizatonApp(UserOrganizatonApp $userOrganizatonApp): static + public function removeUserOrganizatonApp(UserOrganizationApp $userOrganizatonApp): static { if ($this->userOrganizatonApps->removeElement($userOrganizatonApp)) { // set the owning side to null (unless already changed) diff --git a/src/Repository/UserOrganizatonAppRepository.php b/src/Repository/UserOrganizatonAppRepository.php index e4e29ff..0942b4d 100644 --- a/src/Repository/UserOrganizatonAppRepository.php +++ b/src/Repository/UserOrganizatonAppRepository.php @@ -3,18 +3,18 @@ namespace App\Repository; use App\Entity\User; -use App\Entity\UserOrganizatonApp; +use App\Entity\UserOrganizationApp; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; /** - * @extends ServiceEntityRepository + * @extends ServiceEntityRepository */ class UserOrganizatonAppRepository extends ServiceEntityRepository { public function __construct(ManagerRegistry $registry) { - parent::__construct($registry, UserOrganizatonApp::class); + parent::__construct($registry, UserOrganizationApp::class); } // /** diff --git a/src/Service/OrganizationsService.php b/src/Service/OrganizationsService.php index cfb6263..1835d12 100644 --- a/src/Service/OrganizationsService.php +++ b/src/Service/OrganizationsService.php @@ -5,7 +5,7 @@ namespace App\Service; use App\Entity\Apps; use App\Entity\Organizations; use App\Entity\Roles; -use App\Entity\UserOrganizatonApp; +use App\Entity\UserOrganizationApp; use App\Entity\UsersOrganizations; use App\Repository\UsersOrganizationsRepository; use App\Service\LoggerService; @@ -145,7 +145,7 @@ class OrganizationsService $adminUOs = $this->uoRepository->findBy(['organization' => $data['organization'], 'isActive' => true]); foreach ($adminUOs as $adminUO) { - $uoa = $this->entityManager->getRepository(UserOrganizatonApp::class) + $uoa = $this->entityManager->getRepository(UserOrganizationApp::class) ->findOneBy([ 'userOrganization' => $adminUO, 'role' => $roleAdmin, diff --git a/src/Service/UserOrganizationAppService.php b/src/Service/UserOrganizationAppService.php index 53fc759..fe81114 100644 --- a/src/Service/UserOrganizationAppService.php +++ b/src/Service/UserOrganizationAppService.php @@ -5,7 +5,7 @@ namespace App\Service; use App\Entity\Apps; use App\Entity\Roles; use App\Entity\User; -use App\Entity\UserOrganizatonApp; +use App\Entity\UserOrganizationApp; use App\Entity\UsersOrganizations; use App\Service\ActionService; use App\Service\LoggerService; @@ -76,9 +76,9 @@ class UserOrganizationAppService public function deactivateAllUserOrganizationsAppLinks(UsersOrganizations $userOrganization, Apps $app = null): void { if($app) { - $uoas = $this->entityManager->getRepository(UserOrganizatonApp::class)->findBy(['userOrganization' => $userOrganization, 'application' => $app, 'isActive' => true]); + $uoas = $this->entityManager->getRepository(UserOrganizationApp::class)->findBy(['userOrganization' => $userOrganization, 'application' => $app, 'isActive' => true]); } else { - $uoas = $this->entityManager->getRepository(UserOrganizatonApp::class)->findBy(['userOrganization' => $userOrganization, 'isActive' => true]); + $uoas = $this->entityManager->getRepository(UserOrganizationApp::class)->findBy(['userOrganization' => $userOrganization, 'isActive' => true]); } foreach ($uoas as $uoa) { try{ @@ -123,7 +123,7 @@ class UserOrganizationAppService ): void { // Fetch existing UserOrganizationApp links for this user and application - $uoas = $this->entityManager->getRepository(UserOrganizatonApp::class)->findBy([ + $uoas = $this->entityManager->getRepository(UserOrganizationApp::class)->findBy([ 'userOrganization' => $uo, 'application' => $application, ]); @@ -191,7 +191,7 @@ class UserOrganizationAppService $role = $this->entityManager->getRepository(Roles::class)->find($roleId); if ($role) { // Create new user-organization-application role link - $newUoa = new UserOrganizatonApp(); + $newUoa = new UserOrganizationApp(); $newUoa->setUserOrganization($uo); $newUoa->setApplication($application); $newUoa->setRole($role); @@ -224,19 +224,19 @@ class UserOrganizationAppService /** * Attribute the role Admin to the user if the user has the role Super Admin * - * @param UserOrganizatonApp $uoa + * @param UserOrganizationApp $uoa * * @return void */ - public function ensureAdminRoleForSuperAdmin(UserOrganizatonApp $uoa): void + public function ensureAdminRoleForSuperAdmin(UserOrganizationApp $uoa): void { - $uoaAdmin = $this->entityManager->getRepository(UserOrganizatonApp::class)->findOneBy([ + $uoaAdmin = $this->entityManager->getRepository(UserOrganizationApp::class)->findOneBy([ 'userOrganization' => $uoa->getUserOrganization(), 'application' => $uoa->getApplication(), 'role' => $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN']) ]); if(!$uoaAdmin) { - $uoaAdmin = new UserOrganizatonApp(); + $uoaAdmin = new UserOrganizationApp(); $uoaAdmin->setUserOrganization($uoa->getUserOrganization()); $uoaAdmin->setApplication($uoa->getApplication()); $uoaAdmin->setRole($this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN'])); @@ -260,7 +260,7 @@ class UserOrganizationAppService $uos = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['users' => $user]); $apps = []; foreach ($uos as $uo) { - $uoas = $this->entityManager->getRepository(UserOrganizatonApp::class)->findBy(['userOrganization' => $uo, 'isActive' => true]); + $uoas = $this->entityManager->getRepository(UserOrganizationApp::class)->findBy(['userOrganization' => $uo, 'isActive' => true]); foreach ($uoas as $uoa) { $app = $uoa->getApplication(); if (!in_array($app, $apps, true)) { diff --git a/src/Service/UserService.php b/src/Service/UserService.php index 2ea60c0..a6429c6 100644 --- a/src/Service/UserService.php +++ b/src/Service/UserService.php @@ -6,7 +6,7 @@ namespace App\Service; use App\Entity\Organizations; use App\Entity\Roles; use App\Entity\User; -use App\Entity\UserOrganizatonApp; +use App\Entity\UserOrganizationApp; use App\Entity\UsersOrganizations; use App\Repository\RolesRepository; use DateTimeImmutable; @@ -361,7 +361,7 @@ class UserService ->findBy(['users' => $user, 'isActive' => true]); $hasRole = false; foreach ($uos as $uo) { - $uoa = $this->entityManager->getRepository(UserOrganizatonApp::class) + $uoa = $this->entityManager->getRepository(UserOrganizationApp::class) ->findBy([ 'userOrganization' => $uo, 'isActive' => true, diff --git a/tests/Controller/OrganizationController.php b/tests/Controller/OrganizationController.php index bb5a446..a0fb562 100644 --- a/tests/Controller/OrganizationController.php +++ b/tests/Controller/OrganizationController.php @@ -5,7 +5,7 @@ namespace App\Tests\Controller; use App\Entity\Apps; use App\Entity\Organizations; use App\Entity\Roles; -use App\Entity\UserOrganizatonApp; +use App\Entity\UserOrganizationApp; use App\Entity\UsersOrganizations; use App\Service\AwsService; use App\Tests\Functional\AbstractFunctional; @@ -348,10 +348,10 @@ class OrganizationController extends AbstractFunctional self::assertCount(1, $this->entityManager->getRepository(Apps::class)->findAll()); self::assertCount(1, $this->entityManager->getRepository(Roles::class)->findAll()); self::assertCount(1, $this->entityManager->getRepository(UsersOrganizations::class)->findAll()); - self::assertCount(1, $this->entityManager->getRepository(UserOrganizatonApp::class)->findAll()); + self::assertCount(1, $this->entityManager->getRepository(UserOrganizationApp::class)->findAll()); self::assertTrue($this->entityManager->getRepository(Organizations::class)->find($organization->getId())->isDeleted()); self::assertFalse($this->entityManager->getRepository(UsersOrganizations::class)->find($uoLink->getId())->isActive()); - self::assertFalse($this->entityManager->getRepository(UserOrganizatonApp::class)->find($uoaLink->getId())->isActive()); + self::assertFalse($this->entityManager->getRepository(UserOrganizationApp::class)->find($uoaLink->getId())->isActive()); self::assertSelectorNotExists('#tabulator-org'); } diff --git a/tests/Controller/UserController.php b/tests/Controller/UserController.php index 147fe3e..c51322f 100644 --- a/tests/Controller/UserController.php +++ b/tests/Controller/UserController.php @@ -11,7 +11,7 @@ use App\Entity\Apps; use App\Entity\Roles; use App\Entity\Organizations; use App\Entity\UsersOrganizations; -use App\Entity\UserOrganizatonApp; +use App\Entity\UserOrganizationApp; use App\Tests\Functional\AbstractFunctional; use Symfony\Component\HttpFoundation\File\UploadedFile; use function Symfony\Component\DependencyInjection\Loader\Configurator\param; @@ -542,7 +542,7 @@ class UserController extends AbstractFunctional $this->client->followRedirect(); self::assertCount(2, $this->entityManager->getRepository(User::class)->findAll()); self::assertCount(1, $this->entityManager->getRepository(UsersOrganizations::class)->findAll()); - self::assertCount(1, $this->entityManager->getRepository(UserOrganizatonApp::class)->findAll()); + self::assertCount(1, $this->entityManager->getRepository(UserOrganizationApp::class)->findAll()); } #[Test] diff --git a/tests/Functional/AbstractFunctional.php b/tests/Functional/AbstractFunctional.php index 4820d47..11e3f76 100644 --- a/tests/Functional/AbstractFunctional.php +++ b/tests/Functional/AbstractFunctional.php @@ -7,7 +7,7 @@ use App\Entity\Notification; use App\Entity\Organizations; use App\Entity\Roles; use App\Entity\User; -use App\Entity\UserOrganizatonApp; +use App\Entity\UserOrganizationApp; use App\Entity\UsersOrganizations; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\KernelBrowser; @@ -85,8 +85,8 @@ abstract class AbstractFunctional extends WebTestCase return $uo; } - protected function createUOALink(UsersOrganizations $uo, Apps $app, Roles $role): UserOrganizatonApp{ - $uoa = new UserOrganizatonApp(); + protected function createUOALink(UsersOrganizations $uo, Apps $app, Roles $role): UserOrganizationApp{ + $uoa = new UserOrganizationApp(); $uoa->setUserOrganization($uo); $uoa->setApplication($app); $uoa->setRole($role); diff --git a/tests/Service/OrganizationsServiceTest.php b/tests/Service/OrganizationsServiceTest.php index 80c264c..166f545 100644 --- a/tests/Service/OrganizationsServiceTest.php +++ b/tests/Service/OrganizationsServiceTest.php @@ -6,7 +6,7 @@ use App\Entity\Apps; use App\Entity\Organizations; use App\Entity\Roles; use App\Entity\User; -use App\Entity\UserOrganizatonApp; +use App\Entity\UserOrganizationApp; use App\Entity\UsersOrganizations; use App\Repository\UsersOrganizationsRepository; use App\Service\AwsService; @@ -186,7 +186,7 @@ class OrganizationsServiceTest extends TestCase $adminRole->setName('ADMIN'); // 4. Setup UOA Logic (Proof that user is Admin of an App) - $uoa = new UserOrganizatonApp(); + $uoa = new UserOrganizationApp(); $this->setEntityId($uoa, 777); $uoa->setUserOrganization($adminUO); $uoa->setRole($adminRole); @@ -209,7 +209,7 @@ class OrganizationsServiceTest extends TestCase $this->entityManager->method('getRepository')->willReturnMap([ [Roles::class, $rolesRepo], - [UserOrganizatonApp::class, $uoaRepo], + [UserOrganizationApp::class, $uoaRepo], ]); // 6. Expectations @@ -246,7 +246,7 @@ class OrganizationsServiceTest extends TestCase $roleAdmin = new Roles(); - $uoa = new UserOrganizatonApp(); // active admin link + $uoa = new UserOrganizationApp(); // active admin link // Mocks setup $rolesRepo = $this->createMock(EntityRepository::class); @@ -259,7 +259,7 @@ class OrganizationsServiceTest extends TestCase $this->entityManager->method('getRepository')->willReturnMap([ [Roles::class, $rolesRepo], - [UserOrganizatonApp::class, $uoaRepo], + [UserOrganizationApp::class, $uoaRepo], ]); // Expectations: Notification service should NEVER be called @@ -294,7 +294,7 @@ class OrganizationsServiceTest extends TestCase $this->entityManager->method('getRepository')->willReturnMap([ [Roles::class, $rolesRepo], - [UserOrganizatonApp::class, $uoaRepo], + [UserOrganizationApp::class, $uoaRepo], ]); // 4. Expectations: ensure NOTHING happens @@ -326,7 +326,7 @@ class OrganizationsServiceTest extends TestCase $adminRole = new Roles(); $adminRole->setName('ADMIN'); - $uoa = new UserOrganizatonApp(); + $uoa = new UserOrganizationApp(); $uoa->setUserOrganization($adminUO); $uoa->setRole($adminRole); $uoa->setIsActive(true); @@ -342,7 +342,7 @@ class OrganizationsServiceTest extends TestCase $this->entityManager->method('getRepository')->willReturnMap([ [Roles::class, $rolesRepo], - [UserOrganizatonApp::class, $uoaRepo], + [UserOrganizationApp::class, $uoaRepo], ]); // 5. Dynamic Expectations diff --git a/tests/Service/UserOrganizationAppServiceTest.php b/tests/Service/UserOrganizationAppServiceTest.php index 49dbf19..0a0e7aa 100644 --- a/tests/Service/UserOrganizationAppServiceTest.php +++ b/tests/Service/UserOrganizationAppServiceTest.php @@ -6,7 +6,7 @@ use App\Entity\Apps; use App\Entity\Organizations; use App\Entity\Roles; use App\Entity\User; -use App\Entity\UserOrganizatonApp; +use App\Entity\UserOrganizationApp; use App\Entity\UsersOrganizations; use App\Service\ActionService; use App\Service\LoggerService; @@ -78,7 +78,7 @@ class UserOrganizationAppServiceTest extends TestCase $uo = new UsersOrganizations(); $this->setEntityId($uo, 99); - $uoa = new UserOrganizatonApp(); + $uoa = new UserOrganizationApp(); $this->setEntityId($uoa, 500); $uoa->setApplication($app1); $uoa->setRole($role); @@ -121,7 +121,7 @@ class UserOrganizationAppServiceTest extends TestCase $role = new Roles(); $this->setEntityId($role, 10); - $uoa = new UserOrganizatonApp(); + $uoa = new UserOrganizationApp(); $this->setEntityId($uoa, 555); $uoa->setApplication($app); $uoa->setRole($role); @@ -159,7 +159,7 @@ class UserOrganizationAppServiceTest extends TestCase $app = new Apps(); $this->setEntityId($app, 1); $role = new Roles(); $this->setEntityId($role, 1); - $realUoa = new UserOrganizatonApp(); + $realUoa = new UserOrganizationApp(); $this->setEntityId($realUoa, 100); $realUoa->setApplication($app); $realUoa->setRole($role); @@ -209,12 +209,12 @@ class UserOrganizationAppServiceTest extends TestCase $roleRepo->method('find')->with($roleId)->willReturn($role); $this->entityManager->method('getRepository')->willReturnMap([ - [UserOrganizatonApp::class, $uoaRepo], + [UserOrganizationApp::class, $uoaRepo], [Roles::class, $roleRepo], ]); // Expect creation - $this->entityManager->expects($this->once())->method('persist')->with($this->isInstanceOf(UserOrganizatonApp::class)); + $this->entityManager->expects($this->once())->method('persist')->with($this->isInstanceOf(UserOrganizationApp::class)); $this->entityManager->expects($this->once())->method('flush'); $this->actionService->expects($this->once())->method('createAction'); @@ -239,7 +239,7 @@ class UserOrganizationAppServiceTest extends TestCase $role = new Roles(); $this->setEntityId($role, 30); $role->setName('VIEWER'); - $existingUoa = new UserOrganizatonApp(); + $existingUoa = new UserOrganizationApp(); $this->setEntityId($existingUoa, 999); $existingUoa->setRole($role); $existingUoa->setApplication($app); @@ -251,7 +251,7 @@ class UserOrganizationAppServiceTest extends TestCase $uoaRepo->method('findBy')->willReturn([$existingUoa]); $this->entityManager->method('getRepository')->willReturnMap([ - [UserOrganizatonApp::class, $uoaRepo], + [UserOrganizationApp::class, $uoaRepo], ]); // We pass empty array [] as selected roles -> expect deactivation @@ -296,7 +296,7 @@ class UserOrganizationAppServiceTest extends TestCase $roleRepo->method('findOneBy')->with(['name' => 'ADMIN'])->willReturn($adminRole); $this->entityManager->method('getRepository')->willReturnMap([ - [UserOrganizatonApp::class, $uoaRepo], + [UserOrganizationApp::class, $uoaRepo], [Roles::class, $roleRepo], ]); @@ -312,7 +312,7 @@ class UserOrganizationAppServiceTest extends TestCase // - The new ADMIN link (automatically created) $this->entityManager->expects($this->exactly(2)) ->method('persist') - ->with($this->isInstanceOf(UserOrganizatonApp::class)); + ->with($this->isInstanceOf(UserOrganizationApp::class)); // Run $this->service->syncRolesForUserOrganizationApp($uo, $app, ['100'], $actingUser); diff --git a/tests/Service/UserServiceTest.php b/tests/Service/UserServiceTest.php index 8387158..40be86e 100644 --- a/tests/Service/UserServiceTest.php +++ b/tests/Service/UserServiceTest.php @@ -5,7 +5,7 @@ namespace App\Tests\Service; use App\Entity\Organizations; use App\Entity\Roles; use App\Entity\User; -use App\Entity\UserOrganizatonApp; +use App\Entity\UserOrganizationApp; use App\Entity\UsersOrganizations; use App\Event\UserCreatedEvent; use App\Service\ActionService; @@ -328,7 +328,7 @@ class UserServiceTest extends TestCase // 4. UserOrganizatonApp mock (The link checking if they are admin active) $uoaRepo = $this->createMock(EntityRepository::class); - $uoa = new UserOrganizatonApp(); + $uoa = new UserOrganizationApp(); $uoaRepo->method('findOneBy')->willReturn($uoa); // Returns an object, so true // Configure EntityManager to return these repos based on class @@ -336,7 +336,7 @@ class UserServiceTest extends TestCase [User::class, $userRepo], [UsersOrganizations::class, $uoRepo], [Roles::class, $rolesRepo], - [UserOrganizatonApp::class, $uoaRepo], + [UserOrganizationApp::class, $uoaRepo], ]); $result = $this->userService->isAdminOfOrganization($org); From a893c09fcf0844ee35e6001bc1eb69f4746537ee Mon Sep 17 00:00:00 2001 From: Charles Date: Tue, 17 Feb 2026 11:02:47 +0100 Subject: [PATCH 02/19] changed creating logic to modal --- assets/controllers/base_controller.js | 27 +++ assets/controllers/project_controller.js | 11 +- assets/controllers/user_controller.js | 39 +++- src/Controller/UserController.php | 244 +++++++++++++++-------- src/Service/UserService.php | 34 +++- templates/organization/show.html.twig | 72 ++++++- 6 files changed, 320 insertions(+), 107 deletions(-) create mode 100644 assets/controllers/base_controller.js diff --git a/assets/controllers/base_controller.js b/assets/controllers/base_controller.js new file mode 100644 index 0000000..379f069 --- /dev/null +++ b/assets/controllers/base_controller.js @@ -0,0 +1,27 @@ +import { Controller } from "@hotwired/stimulus"; + +export default class extends Controller { + async fetchAndRenderApplications(targetElement) { + try { + const response = await fetch('/application/data/all'); + const apps = await response.json(); + + targetElement.innerHTML = apps.map(app => ` +
+
+ + +
+
+ `).join(''); + + return apps; + } catch (error) { + targetElement.innerHTML = '
Erreur de chargement.
'; + console.error("App load error:", error); + } + } +} \ No newline at end of file diff --git a/assets/controllers/project_controller.js b/assets/controllers/project_controller.js index bcf2e12..e7bf00f 100644 --- a/assets/controllers/project_controller.js +++ b/assets/controllers/project_controller.js @@ -2,9 +2,10 @@ import {Controller} from '@hotwired/stimulus'; import { Modal } from "bootstrap"; import {TabulatorFull as Tabulator} from 'tabulator-tables'; import {eyeIconLink, pencilIcon, TABULATOR_FR_LANG, trashIcon} from "../js/global.js"; +import base_controller from "./base_controller.js"; -export default class extends Controller { +export default class extends base_controller { static values = { listProject : Boolean, orgId: Number, @@ -175,13 +176,13 @@ export default class extends Controller { this.currentProjectId = projectId; this.modal.show(); + this.nameInputTarget.disabled = true; this.formTitleTarget.textContent = "Modifier le projet"; try { // 1. Ensure checkboxes are loaded first - await this.loadApplications(); - + const apps = await this.fetchAndRenderApplications(this.appListTarget); // 2. Fetch the project data const response = await fetch(`/project/data/${projectId}`); const project = await response.json(); @@ -203,13 +204,13 @@ export default class extends Controller { } } // Update your openCreateModal to reset the state - openCreateModal() { + async openCreateModal() { this.currentProjectId = null; this.modal.show(); this.nameInputTarget.disabled = false; this.nameInputTarget.value = ""; this.formTitleTarget.textContent = "Nouveau Projet"; - this.loadApplications(); + await this.fetchAndRenderApplications(); } async deleteProject(event) { diff --git a/assets/controllers/user_controller.js b/assets/controllers/user_controller.js index e7834b0..4a71920 100644 --- a/assets/controllers/user_controller.js +++ b/assets/controllers/user_controller.js @@ -10,9 +10,10 @@ import { trashIconForm } from "../js/global.js"; import { Modal } from "bootstrap"; +import base_controller from "./base_controller.js"; -export default class extends Controller { +export default class extends base_controller { static values = { rolesArray: Array, selectedRoleIds: Array, @@ -26,7 +27,7 @@ export default class extends Controller { orgId: Number } - static targets = ["select", "statusButton", "modal", "userSelect"]; + static targets = ["select", "statusButton", "modal", "userSelect", "appList"]; connect() { this.roleSelect(); @@ -1018,4 +1019,38 @@ export default class extends Controller { alert("Une erreur réseau est survenue."); } } + + async openNewUserModal() { + this.modal.show(); + // Call the shared logic and pass the target + await this.fetchAndRenderApplications(this.appListTarget); + } + + async submitNewUser(event) { + event.preventDefault(); + const form = event.currentTarget; + const formData = new FormData(form); + + try { + const response = await fetch('/user/new/ajax', { // Adjust path if prefix is different + method: 'POST', + body: formData, + headers: { + 'X-Requested-With': 'XMLHttpRequest' + } + }); + + const result = await response.json(); + + if (response.ok) { + this.modal.hide(); + form.reset(); // Clear the form + location.reload(); + } else { + alert("Erreur: " + (result.error || "Une erreur est survenue lors de la création.")); + } + } catch (error) { + alert("Erreur réseau."); + } + } } \ No newline at end of file diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index d7cf277..5efcdc2 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -195,88 +195,88 @@ class UserController extends AbstractController } - #[Route('/new', name: 'new', methods: ['GET', 'POST'])] - public function new(Request $request): Response - { - $this->denyAccessUnlessGranted('ROLE_USER'); - try { - $actingUser =$this->getUser(); - - $user = new User(); - $form = $this->createForm(UserForm::class, $user); - $form->handleRequest($request); - - $orgId = $request->query->get('organizationId') ?? $request->request->get('organizationId'); - if ($orgId) { - $org = $this->organizationRepository->find($orgId); - if (!$org) { - $this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getUserIdentifier()); - $this->addFlash('danger', "L'organisation n'existe pas."); - throw $this->createNotFoundException(self::NOT_FOUND); - } - if (!$this->isGranted('ROLE_ADMIN') && !$this->userService->isAdminOfOrganization($org)) { - $this->loggerService->logAccessDenied($actingUser->getUserIdentifier()); - $this->addFlash('danger', "Accès non autorisé."); - throw $this->createAccessDeniedException(self::ACCESS_DENIED); - } - } else{ - $this->loggerService->logAccessDenied($actingUser->getUserIdentifier()); - $this->addFlash('danger', "Accès non autorisé."); - throw $this->createAccessDeniedException(self::ACCESS_DENIED); - } - - if ($form->isSubmitted() && $form->isValid()) { - $existingUser = $this->userRepository->findOneBy(['email' => $user->getEmail()]); - - // Case : User exists -> link him to given organization if not already linked, else error message - if ($existingUser && $org) { - $this->userService->addExistingUserToOrganization( - $existingUser, - $org, - ); - - if ($this->isGranted('ROLE_ADMIN')) { - $this->loggerService->logSuperAdmin( - $existingUser->getId(), - $actingUser->getUserIdentifier(), - "Super Admin linked user to organization", - $org->getId(), - ); - } - $this->addFlash('success', 'Utilisateur ajouté avec succès à l\'organisation. '); - return $this->redirectToRoute('organization_show', ['id' => $orgId]); - } - - // Case : user doesn't already exist - - $picture = $form->get('pictureUrl')->getData(); - $this->userService->createNewUser($user, $actingUser, $picture); - - $this->userService->linkUserToOrganization( - $user, - $org, - ); - $this->addFlash('success', 'Nouvel utilisateur créé et ajouté à l\'organisation avec succès. '); - return $this->redirectToRoute('organization_show', ['id' => $orgId]); - } - - return $this->render('user/new.html.twig', [ - 'user' => $user, - 'form' => $form->createView(), - 'organizationId' => $orgId, - ]); - - } catch (\Exception $e) { - $this->errorLogger->critical($e->getMessage()); - - if ($orgId) { - $this->addFlash('danger', 'Une erreur est survenue lors de la création de l\'utilisateur pour l\'organisation .'); - return $this->redirectToRoute('organization_show', ['id' => $orgId]); - } - $this->addFlash('danger', 'Une erreur est survenue lors de la création de l\'utilisateur.'); - return $this->redirectToRoute('user_index'); - } - } +// #[Route('/new', name: 'new', methods: ['GET', 'POST'])] +// public function new(Request $request): Response +// { +// $this->denyAccessUnlessGranted('ROLE_USER'); +// try { +// $actingUser =$this->getUser(); +// +// $user = new User(); +// $form = $this->createForm(UserForm::class, $user); +// $form->handleRequest($request); +// +// $orgId = $request->query->get('organizationId') ?? $request->request->get('organizationId'); +// if ($orgId) { +// $org = $this->organizationRepository->find($orgId); +// if (!$org) { +// $this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getUserIdentifier()); +// $this->addFlash('danger', "L'organisation n'existe pas."); +// throw $this->createNotFoundException(self::NOT_FOUND); +// } +// if (!$this->isGranted('ROLE_ADMIN') && !$this->userService->isAdminOfOrganization($org)) { +// $this->loggerService->logAccessDenied($actingUser->getUserIdentifier()); +// $this->addFlash('danger', "Accès non autorisé."); +// throw $this->createAccessDeniedException(self::ACCESS_DENIED); +// } +// } else{ +// $this->loggerService->logAccessDenied($actingUser->getUserIdentifier()); +// $this->addFlash('danger', "Accès non autorisé."); +// throw $this->createAccessDeniedException(self::ACCESS_DENIED); +// } +// +// if ($form->isSubmitted() && $form->isValid()) { +// $existingUser = $this->userRepository->findOneBy(['email' => $user->getEmail()]); +// +// // Case : User exists -> link him to given organization if not already linked, else error message +// if ($existingUser && $org) { +// $this->userService->addExistingUserToOrganization( +// $existingUser, +// $org, +// ); +// +// if ($this->isGranted('ROLE_ADMIN')) { +// $this->loggerService->logSuperAdmin( +// $existingUser->getId(), +// $actingUser->getUserIdentifier(), +// "Super Admin linked user to organization", +// $org->getId(), +// ); +// } +// $this->addFlash('success', 'Utilisateur ajouté avec succès à l\'organisation. '); +// return $this->redirectToRoute('organization_show', ['id' => $orgId]); +// } +// +// // Case : user doesn't already exist +// +// $picture = $form->get('pictureUrl')->getData(); +// $this->userService->createNewUser($user, $actingUser, $picture); +// +// $this->userService->linkUserToOrganization( +// $user, +// $org, +// ); +// $this->addFlash('success', 'Nouvel utilisateur créé et ajouté à l\'organisation avec succès. '); +// return $this->redirectToRoute('organization_show', ['id' => $orgId]); +// } +// +// return $this->render('user/new.html.twig', [ +// 'user' => $user, +// 'form' => $form->createView(), +// 'organizationId' => $orgId, +// ]); +// +// } catch (\Exception $e) { +// $this->errorLogger->critical($e->getMessage()); +// +// if ($orgId) { +// $this->addFlash('danger', 'Une erreur est survenue lors de la création de l\'utilisateur pour l\'organisation .'); +// return $this->redirectToRoute('organization_show', ['id' => $orgId]); +// } +// $this->addFlash('danger', 'Une erreur est survenue lors de la création de l\'utilisateur.'); +// return $this->redirectToRoute('user_index'); +// } +// } /** * Endpoint to activate/deactivate a user (soft delete) @@ -832,6 +832,86 @@ class UserController extends AbstractController throw $this->createNotFoundException(self::NOT_FOUND); } + #[Route('/new/ajax', name: 'new_ajax', methods: ['POST'])] + public function newUserAjax(Request $request): JsonResponse + { + $this->denyAccessUnlessGranted('ROLE_USER'); + $actingUser = $this->getUser(); + try { + $data = $request->request->all(); + $orgId = $data['organizationId'] ?? null; + $selectedApps = $data['applications'] ?? []; + + //unset data that are not part of the User entity to avoid form errors + unset($data['organizationId'], $data['applications']); + $user = new User(); + + $form = $this->createForm(UserForm::class, $user, [ + 'csrf_protection' => false, + 'allow_extra_fields' => true, + ]); + + $form->submit($data, false); + if (!$orgId) { + return $this->json(['error' => 'ID Organisation manquant.'], 400); + } + + $org = $this->organizationRepository->find($orgId); + if (!$org) { + return $this->json(['error' => "L'organisation n'existe pas."], 404); + } + + // 3. Permissions Check + if (!$this->isGranted('ROLE_ADMIN') && !$this->userService->isAdminOfOrganization($org)) { + $this->loggerService->logAccessDenied($actingUser->getUserIdentifier()); + return $this->json(['error' => "Accès non autorisé."], 403); + } + + if ($form->isSubmitted() && $form->isValid()) { + $email = $user->getEmail(); + $existingUser = $this->userRepository->findOneBy(['email' => $email]); + + // CASE A: User exists -> Add to org + if ($existingUser) { + // Check if already in org to avoid logic errors or duplicate logs + $this->userService->addExistingUserToOrganization($existingUser, $org, $selectedApps); + + if ($this->isGranted('ROLE_ADMIN')) { + $this->loggerService->logSuperAdmin( + $existingUser->getId(), + $actingUser->getUserIdentifier(), + "Super Admin linked user to organization via AJAX", + $org->getId(), + ); + } + + return $this->json([ + 'success' => true, + 'message' => 'Utilisateur existant ajouté à l\'organisation.' + ]); + } + + // CASE B: New User -> Create + // Fetch picture from $request->files since it's a multipart request + $picture = $request->files->get('pictureUrl'); + + $this->userService->createNewUser($user, $actingUser, $picture); + $this->userService->linkUserToOrganization($user, $org, $selectedApps); + + return $this->json([ + 'success' => true, + 'message' => 'Nouvel utilisateur créé et ajouté.' + ]); + } + + // If form is invalid, return the specific errors + return $this->json(['error' => 'Données de formulaire invalides.'], 400); + + } catch (\Exception $e) { + $this->errorLogger->critical("AJAX User Creation Error: " . $e->getMessage()); + return $this->json(['error' => 'Une erreur interne est survenue.'], 500); + } + } } diff --git a/src/Service/UserService.php b/src/Service/UserService.php index a6429c6..dbe6e6b 100644 --- a/src/Service/UserService.php +++ b/src/Service/UserService.php @@ -3,6 +3,7 @@ namespace App\Service; +use App\Entity\Apps; use App\Entity\Organizations; use App\Entity\Roles; use App\Entity\User; @@ -533,17 +534,17 @@ class UserService * * @param User $user * @param Organizations $organization - * @return void + * @param array $selectedApps + * @return int + * @throws Exception */ - public function handleExistingUser(User $user, Organizations $organization): int + public function reactivateUser(User $user, Organizations $organization, array $selectedApps): int { if (!$user->isActive()) { $user->setIsActive(true); $this->entityManager->persist($user); } - $uo = $this->linkUserToOrganization($user, $organization); - - return $uo->getId(); + return $this->linkUserToOrganization($user, $organization, $selectedApps)->getId(); } /** @@ -554,6 +555,8 @@ class UserService * Handle picture if provided * * @param User $user + * @param $picture + * @param bool $setPassword * @return void */ public function formatUserData(User $user, $picture, bool $setPassword = false): void @@ -589,11 +592,12 @@ class UserService public function addExistingUserToOrganization( User $existingUser, Organizations $org, + array $selectedApps ): int { + $actingUser = $this->getUserByIdentifier($this->security->getUser()->getUserIdentifier()); try { - $uoId = $this->handleExistingUser($existingUser, $org); - $actingUser = $this->getUserByIdentifier($this->security->getUser()->getUserIdentifier()); + $uoId = $this->reactivateUser($existingUser, $org, $selectedApps); $this->loggerService->logExistingUserAddedToOrg( $existingUser->getId(), $org->getId(), @@ -647,6 +651,7 @@ class UserService public function linkUserToOrganization( User $user, Organizations $org, + array $selectedApps ): UsersOrganizations { $actingUser = $this->getUserByIdentifier($this->security->getUser()->getUserIdentifier()); @@ -660,6 +665,7 @@ class UserService $uo->setRole($roleUser); $uo->setModifiedAt(new \DateTimeImmutable('now')); $this->entityManager->persist($uo); + $this->linkUOToApps($uo, $selectedApps); $this->entityManager->flush(); $this->loggerService->logUserOrganizationLinkCreated( @@ -731,4 +737,18 @@ class UserService } } + private function linkUOToApps(UsersOrganizations $uo, array $selectedApps):void + { + foreach ($selectedApps as $appId){ + $uoa = new UserOrganizationApp(); + $uoa->setUserOrganization($uo); + $app = $this->entityManager->getRepository(Apps::class)->find($appId); + if ($app) { + $uoa->setApplication($app); + $this->entityManager->persist($uoa); + } + } + $this->entityManager->flush(); + } + } diff --git a/templates/organization/show.html.twig b/templates/organization/show.html.twig index 60ce8e1..a54ce55 100644 --- a/templates/organization/show.html.twig +++ b/templates/organization/show.html.twig @@ -52,20 +52,70 @@ {# User tables #}
-
+
+
-

- Nouveaux utilisateurs -

- Ajouter un utilisateur +

Nouveaux utilisateurs

+ {# Button to trigger modal #} +
-
+
+
+ + {# New User Modal #} +
From c2ea41f0a1374dc5ef741f0868192a07e5b3f692 Mon Sep 17 00:00:00 2001 From: Charles Date: Tue, 17 Feb 2026 11:21:52 +0100 Subject: [PATCH 03/19] format enty data --- assets/controllers/user_controller.js | 2 ++ src/Service/UserService.php | 2 +- templates/organization/show.html.twig | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/assets/controllers/user_controller.js b/assets/controllers/user_controller.js index 4a71920..25ce4bf 100644 --- a/assets/controllers/user_controller.js +++ b/assets/controllers/user_controller.js @@ -1030,6 +1030,8 @@ export default class extends base_controller { event.preventDefault(); const form = event.currentTarget; const formData = new FormData(form); + const ucSurname = formData.get('surname').toUpperCase(); + formData.set('surname', ucSurname); try { const response = await fetch('/user/new/ajax', { // Adjust path if prefix is different diff --git a/src/Service/UserService.php b/src/Service/UserService.php index dbe6e6b..efd96b6 100644 --- a/src/Service/UserService.php +++ b/src/Service/UserService.php @@ -563,7 +563,7 @@ class UserService { // capitalize name and surname $user->setName(ucfirst(strtolower($user->getName()))); - $user->setSurname(ucfirst(strtolower($user->getSurname()))); + $user->setSurname(strtoupper($user->getSurname())); // trim strings $user->setName(trim($user->getName())); diff --git a/templates/organization/show.html.twig b/templates/organization/show.html.twig index a54ce55..450190f 100644 --- a/templates/organization/show.html.twig +++ b/templates/organization/show.html.twig @@ -95,7 +95,7 @@
- +
From b9b0efd6c6ca8a59304ab2a6ea30817087113c2b Mon Sep 17 00:00:00 2001 From: Charles Date: Tue, 17 Feb 2026 14:32:33 +0100 Subject: [PATCH 04/19] enabled ajax function to edit user --- assets/controllers/user_controller.js | 67 ++++- src/Controller/UserController.php | 88 +++++++ src/Entity/UserOrganizationApp.php | 4 +- ....php => UserOrganizationAppRepository.php} | 2 +- src/Service/UserOrganizationAppService.php | 248 +++++++----------- templates/user/show.html.twig | 111 +------- templates/user/userInformation.html.twig | 69 ++++- 7 files changed, 318 insertions(+), 271 deletions(-) rename src/Repository/{UserOrganizatonAppRepository.php => UserOrganizationAppRepository.php} (97%) diff --git a/assets/controllers/user_controller.js b/assets/controllers/user_controller.js index 25ce4bf..864edb6 100644 --- a/assets/controllers/user_controller.js +++ b/assets/controllers/user_controller.js @@ -27,7 +27,7 @@ export default class extends base_controller { orgId: Number } - static targets = ["select", "statusButton", "modal", "userSelect", "appList"]; + static targets = ["select", "statusButton", "modal", "userSelect", "appList", "emailInput", "phoneInput", "nameInput", "surnameInput"]; connect() { this.roleSelect(); @@ -1055,4 +1055,69 @@ export default class extends base_controller { alert("Erreur réseau."); } } + + async openEditUserModal(event) { + const userId = event.currentTarget.dataset.id; + this.currentUserId = userId; + this.modal.show(); + + try { + // 1. Fetch all available apps using your shared base method + await this.fetchAndRenderApplications(this.appListTarget); + + // 2. Fetch specific user data WITH the orgId query parameter + // We use this.orgIdValue which is mapped to data-user-org-id-value + const response = await fetch(`/user/data/${userId}?orgId=${this.orgIdValue}`); + + if (!response.ok) throw new Error('Failed to fetch user data'); + + const user = await response.json(); + + // 3. Fill text inputs + this.emailInputTarget.value = user.email; + this.phoneInputTarget.value = user.phoneNumber || ''; + this.nameInputTarget.value = user.name; + this.surnameInputTarget.value = user.surname; + + // 4. Check the application boxes + const checkboxes = this.appListTarget.querySelectorAll('input[type="checkbox"]'); + + // Ensure we handle IDs as strings or numbers consistently + const activeAppIds = user.applicationIds.map(id => id.toString()); + + checkboxes.forEach(cb => { + cb.checked = activeAppIds.includes(cb.value.toString()); + }); + + } catch (error) { + console.error("Erreur lors du chargement des données utilisateur:", error); + alert("Impossible de charger les informations de l'utilisateur."); + } + } + + async submitEditUser(event) { + event.preventDefault(); + const formData = new FormData(event.target); + + // Force Uppercase on Surname as requested + formData.set('surname', formData.get('surname').toUpperCase()); + + try { + const response = await fetch(`/user/edit/${this.currentUserId}/ajax`, { + method: 'POST', + body: formData, + headers: { 'X-Requested-With': 'XMLHttpRequest' } + }); + + if (response.ok) { + this.modal.hide(); + location.reload(); + } else { + const result = await response.json(); + alert(result.error || "Erreur lors de la mise à jour."); + } + } catch (error) { + alert("Erreur réseau."); + } + } } \ No newline at end of file diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 5efcdc2..e197bf6 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -913,5 +913,93 @@ class UserController extends AbstractController return $this->json(['error' => 'Une erreur interne est survenue.'], 500); } } + + #[Route('/data/{id}', name: 'user_data_json', methods: ['GET'])] + public function userData(User $user, Request $request): JsonResponse { + $orgId = $request->query->get('orgId'); + $org = $this->organizationRepository->find($orgId); + if (!$org) { + $this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $this->getUser()->getUserIdentifier()); + return $this->json(['error' => "L'organisation n'existe pas."], 404); + } + $uo = $this->uoRepository->findOneBy(['users' => $user, 'organization' => $org]); + $apps = $this->userOrganizationAppService->getUserApplicationByOrganization($uo); + return $this->json([ + 'email' => $user->getEmail(), + 'name' => $user->getName(), + 'surname' => $user->getSurname(), + 'phoneNumber' => $user->getPhoneNumber(), + 'applicationIds' => array_map(fn($app) => $app->getId(), $apps), + ]); + } + + #[Route('/edit/{id}/ajax', name: 'edit_ajax', methods: ['POST'])] + public function editAjax(int $id, Request $request): JsonResponse + { + $this->denyAccessUnlessGranted('ROLE_USER'); + $actingUser = $this->getUser(); + + $user = $this->userRepository->find($id); + if (!$user) { + return $this->json(['error' => "L'utilisateur n'existe pas."], 404); + } + + try { + if (!$this->userService->isAdminOfUser($user)) { + return $this->json(['error' => "Accès non autorisé."], 403); + } + + $data = $request->request->all(); + $orgId = $data['organizationId'] ?? null; + $selectedApps = $data['applications'] ?? []; + + // 1. Clean data for the form (remove non-entity fields) + unset($data['organizationId'], $data['applications']); + + $form = $this->createForm(UserForm::class, $user, [ + 'csrf_protection' => false, + 'allow_extra_fields' => true, + ]); + + $form->submit($data, false); + + if ($form->isSubmitted() && $form->isValid()) { + // 2. Handle User Info & Picture + $picture = $request->files->get('pictureUrl'); + $this->userService->formatUserData($user, $picture); + $user->setModifiedAt(new \DateTimeImmutable('now')); + + $this->entityManager->persist($user); + $this->entityManager->flush(); + + // 3. Handle Organization-specific Application Sync + if ($orgId) { + $org = $this->organizationRepository->find($orgId); + if ($org) { + // Logic to sync applications for THIS specific organization + $uo = $this->uoRepository->findOneBy(['users' => $user, 'organization' => $org]); + $this->userOrganizationAppService->syncUserApplicationsByOrganization($uo, $selectedApps); + + // Create Action Log + $this->actionService->createAction("Edit user information", $actingUser, $org, $user->getUserIdentifier()); + } + } + + // 4. Logging + $this->loggerService->logUserAction($user->getId(), $actingUser->getUserIdentifier(), 'User information edited via AJAX'); + if ($this->isGranted('ROLE_SUPER_ADMIN')) { + $this->loggerService->logSuperAdmin($user->getId(), $actingUser->getUserIdentifier(), "Super Admin edited user via AJAX"); + } + + return $this->json(['success' => true, 'message' => 'Informations modifiées avec succès.']); + } + + return $this->json(['error' => 'Données de formulaire invalides.'], 400); + + } catch (\Exception $e) { + $this->errorLogger->critical($e->getMessage()); + return $this->json(['error' => 'Une erreur est survenue lors de la modification.'], 500); + } + } } diff --git a/src/Entity/UserOrganizationApp.php b/src/Entity/UserOrganizationApp.php index d4a6a35..d83513b 100644 --- a/src/Entity/UserOrganizationApp.php +++ b/src/Entity/UserOrganizationApp.php @@ -2,10 +2,10 @@ namespace App\Entity; -use App\Repository\UserOrganizatonAppRepository; +use App\Repository\UserOrganizationAppRepository; use Doctrine\ORM\Mapping as ORM; -#[ORM\Entity(repositoryClass: UserOrganizatonAppRepository::class)] +#[ORM\Entity(repositoryClass: UserOrganizationAppRepository::class)] class UserOrganizationApp { #[ORM\Id] diff --git a/src/Repository/UserOrganizatonAppRepository.php b/src/Repository/UserOrganizationAppRepository.php similarity index 97% rename from src/Repository/UserOrganizatonAppRepository.php rename to src/Repository/UserOrganizationAppRepository.php index 0942b4d..d1627c9 100644 --- a/src/Repository/UserOrganizatonAppRepository.php +++ b/src/Repository/UserOrganizationAppRepository.php @@ -10,7 +10,7 @@ use Doctrine\Persistence\ManagerRegistry; /** * @extends ServiceEntityRepository */ -class UserOrganizatonAppRepository extends ServiceEntityRepository +class UserOrganizationAppRepository extends ServiceEntityRepository { public function __construct(ManagerRegistry $registry) { diff --git a/src/Service/UserOrganizationAppService.php b/src/Service/UserOrganizationAppService.php index fe81114..40166f1 100644 --- a/src/Service/UserOrganizationAppService.php +++ b/src/Service/UserOrganizationAppService.php @@ -7,6 +7,8 @@ use App\Entity\Roles; use App\Entity\User; use App\Entity\UserOrganizationApp; use App\Entity\UsersOrganizations; +use App\Repository\UserOrganizationAppRepository; +use App\Repository\UsersOrganizationsRepository; use App\Service\ActionService; use App\Service\LoggerService; use App\Service\UserService; @@ -16,7 +18,15 @@ use Symfony\Bundle\SecurityBundle\Security; class UserOrganizationAppService { - public function __construct(private readonly EntityManagerInterface $entityManager, private readonly ActionService $actionService, private readonly Security $security, private readonly UserService $userService, private readonly LoggerInterface $logger, private readonly LoggerService $loggerService) + + public function __construct(private readonly EntityManagerInterface $entityManager, + private readonly ActionService $actionService, + private readonly Security $security, + private readonly UserService $userService, + private readonly LoggerInterface $logger, + private readonly LoggerService $loggerService, + private readonly UsersOrganizationsRepository $usersOrganizationsRepository, + private readonly UserOrganizationAppRepository $uoaRepository) { } @@ -76,9 +86,9 @@ class UserOrganizationAppService public function deactivateAllUserOrganizationsAppLinks(UsersOrganizations $userOrganization, Apps $app = null): void { if($app) { - $uoas = $this->entityManager->getRepository(UserOrganizationApp::class)->findBy(['userOrganization' => $userOrganization, 'application' => $app, 'isActive' => true]); + $uoas = $this->uoaRepository->findBy(['userOrganization' => $userOrganization, 'application' => $app, 'isActive' => true]); } else { - $uoas = $this->entityManager->getRepository(UserOrganizationApp::class)->findBy(['userOrganization' => $userOrganization, 'isActive' => true]); + $uoas = $this->uoaRepository->findBy(['userOrganization' => $userOrganization, 'isActive' => true]); } foreach ($uoas as $uoa) { try{ @@ -98,156 +108,6 @@ class UserOrganizationAppService } } - /** - * Synchronizes user roles for a specific application within an organization. - * - * This method handles the complete lifecycle of user-application role assignments: - * - Activates/deactivates existing role links based on selection - * - Creates new role assignments for newly selected roles - * - Updates the user's global Symfony security roles when ADMIN/SUPER_ADMIN roles are assigned - * - * @param UsersOrganizations $uo The user-organization relationship - * @param Apps $application The target application - * @param array $selectedRoleIds Array of role IDs that should be active for this user-app combination - * @param User $actingUser The user performing this action (for audit logging) - * - * @return void - * - * @throws \Exception If role entities cannot be found or persisted - */ - public function syncRolesForUserOrganizationApp( - UsersOrganizations $uo, - Apps $application, - array $selectedRoleIds, - User $actingUser - ): void { - - // Fetch existing UserOrganizationApp links for this user and application - $uoas = $this->entityManager->getRepository(UserOrganizationApp::class)->findBy([ - 'userOrganization' => $uo, - 'application' => $application, - ]); - - $currentRoleIds = []; - // Process existing role links - activate or deactivate based on selection - foreach ($uoas as $uoa) { - $roleId = $uoa->getRole()->getId(); - $currentRoleIds[] = $roleId; - $roleName = $uoa->getRole()->getName(); - - if (in_array((string) $roleId, $selectedRoleIds, true)) { - // Role is selected - ensure it's active - if (!$uoa->isActive()) { - $uoa->setIsActive(true); - $this->entityManager->persist($uoa); - $this->loggerService->logOrganizationInformation( - $uo->getOrganization()->getId(), - $actingUser->getId(), - "Re-activated role '$roleName' for user '{$uo->getUsers()->getId()}' in application '{$application->getName()} with UOA ID {$uoa->getId()}'" - ); - $this->actionService->createAction( - "Re-activate user role for application", - $actingUser, - $uo->getOrganization(), - "App: {$application->getName()}, Role: $roleName for user {$uo->getUsers()->getUserIdentifier()}" - ); - // Sync Admins roles to user's global Symfony security roles - if (in_array($roleName, ['ADMIN', 'SUPER ADMIN'], true)) { - $this->userService->syncUserRoles($uo->getUsers(), $roleName, true); - } - // Ensure ADMIN role is assigned if SUPER ADMIN is activated - if ($roleName === 'SUPER ADMIN') { - $this->ensureAdminRoleForSuperAdmin($uoa); - } - } - } else { - // Role is not selected - ensure it's inactive - if ($uoa->isActive()) { - $uoa->setIsActive(false); - $this->entityManager->persist($uoa); - $this->loggerService->logOrganizationInformation( - $uo->getOrganization()->getId(), - $actingUser->getId(), - "Deactivated role '$roleName' for user '{$uo->getUsers()->getId()}' in application '{$application->getName()}' with UOA ID {$uoa->getId()}'" - ); - $this->actionService->createAction( - "Deactivate user role for application", - $actingUser, - $uo->getOrganization(), - "App: {$application->getName()}, Role: $roleName for user {$uo->getUsers()->getUserIdentifier()}" - ); - // Sync Admins roles to user's global Symfony security roles - if (in_array($roleName, ['ADMIN', 'SUPER ADMIN'], true)) { - $this->userService->syncUserRoles($uo->getUsers(), $roleName, false); - } - - } - } - } - - // Create new role assignments for roles that don't exist yet - foreach ($selectedRoleIds as $roleId) { - if (!in_array($roleId, $currentRoleIds)) { - $role = $this->entityManager->getRepository(Roles::class)->find($roleId); - if ($role) { - // Create new user-organization-application role link - $newUoa = new UserOrganizationApp(); - $newUoa->setUserOrganization($uo); - $newUoa->setApplication($application); - $newUoa->setRole($role); - $newUoa->setIsActive(true); - - // Sync Admins roles to user's global Symfony security roles - if (in_array($role->getName(), ['ADMIN', 'SUPER ADMIN'], true)) { - $this->userService->syncUserRoles($uo->getUsers(), $role->getName(), true); - } - // Ensure ADMIN role is assigned if SUPER ADMIN is activated - if ($role->getName() === 'SUPER ADMIN') { - $this->ensureAdminRoleForSuperAdmin($newUoa); - } - $this->entityManager->persist($newUoa); - $this->loggerService->logOrganizationInformation( - $uo->getOrganization()->getId(), - $actingUser->getId(), - "Created new role '{$role->getName()}' for user '{$uo->getUsers()->getId()}' in application '{$application->getName()}' with UOA ID {$newUoa->getId()}'" - ); - $this->actionService->createAction("New user role for application", - $actingUser, - $uo->getOrganization(), - "App: {$application->getName()}, Role: {$role->getName()} for user {$uo->getUsers()->getUserIdentifier()}"); - } - } - } - $this->entityManager->flush(); - } - - /** - * Attribute the role Admin to the user if the user has the role Super Admin - * - * @param UserOrganizationApp $uoa - * - * @return void - */ - public function ensureAdminRoleForSuperAdmin(UserOrganizationApp $uoa): void - { - $uoaAdmin = $this->entityManager->getRepository(UserOrganizationApp::class)->findOneBy([ - 'userOrganization' => $uoa->getUserOrganization(), - 'application' => $uoa->getApplication(), - 'role' => $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN']) - ]); - if(!$uoaAdmin) { - $uoaAdmin = new UserOrganizationApp(); - $uoaAdmin->setUserOrganization($uoa->getUserOrganization()); - $uoaAdmin->setApplication($uoa->getApplication()); - $uoaAdmin->setRole($this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN'])); - $uoaAdmin->setIsActive(true); - $this->entityManager->persist($uoaAdmin); - } - // If the ADMIN role link exists but is inactive, activate it - if ($uoaAdmin && !$uoaAdmin->isActive()) { - $uoaAdmin->setIsActive(true); - } - } /** * Get users applications links for a given user @@ -257,10 +117,10 @@ class UserOrganizationAppService */ public function getUserApplications(User $user): array { - $uos = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['users' => $user]); + $uos = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['users' => $user, 'isActive' => true]); $apps = []; foreach ($uos as $uo) { - $uoas = $this->entityManager->getRepository(UserOrganizationApp::class)->findBy(['userOrganization' => $uo, 'isActive' => true]); + $uoas = $this->uoaRepository->findBy(['userOrganization' => $uo, 'isActive' => true]); foreach ($uoas as $uoa) { $app = $uoa->getApplication(); if (!in_array($app, $apps, true)) { @@ -270,4 +130,82 @@ class UserOrganizationAppService } return $apps; } + + public function getUserApplicationByOrganization(UsersOrganizations $uo): array + { + $uoas = $this->uoaRepository->findBy(['userOrganization' => $uo, 'isActive' => true]); + $apps = []; + foreach ($uoas as $uoa) { + $app = $uoa->getApplication(); + if (!in_array($app, $apps, true)) { + $apps[] = $app; + } + } + return $apps; + } + + public function syncUserApplicationsByOrganization(UsersOrganizations $uo, array $selectedApps): void + { + // 1. Get all currently active applications for this specific User-Organization link + $currentUolas = $this->uoaRepository->findBy([ + 'userOrganization' => $uo, + 'isActive' => true + ]); + + // Track which app IDs are currently active in the DB + $currentAppIds = array_map(fn($uoa) => $uoa->getApplication()->getId(), $currentUolas); + + // 2. REMOVAL: Deactivate apps that are in the DB but NOT in the new selection + foreach ($currentUolas as $uoa) { + $appId = $uoa->getApplication()->getId(); + if (!in_array($appId, $selectedApps)) { + $uoa->setIsActive(false); + $this->actionService->createAction( + "Deactivate UOA link", + $uo->getUsers(), + $uo->getOrganization(), + "App: " . $uoa->getApplication()->getName() + ); + } + } + + // 3. ADDITION / REACTIVATION: Handle the selected apps + foreach ($selectedApps as $appId) { + $app = $this->entityManager->getRepository(Apps::class)->find($appId); + if (!$app) continue; + + // Check if a record (active or inactive) already exists + $existingUOA = $this->uoaRepository->findOneBy([ + 'userOrganization' => $uo, + 'application' => $app + ]); + + if (!$existingUOA) { + // Create new if it never existed + $newUOA = new UserOrganizationApp(); + $newUOA->setUserOrganization($uo); + $newUOA->setApplication($app); + $newUOA->setIsActive(true); + $this->entityManager->persist($newUOA); + + $this->actionService->createAction( + "Activate UOA link", + $uo->getUsers(), + $uo->getOrganization(), + "App: " . $app->getName() + ); + } elseif (!$existingUOA->isActive()) { + // Reactivate if it was previously disabled + $existingUOA->setIsActive(true); + $this->actionService->createAction( + "Reactivate UOA link", + $uo->getUsers(), + $uo->getOrganization(), + "App: " . $app->getName() + ); + } + } + + $this->entityManager->flush(); + } } diff --git a/templates/user/show.html.twig b/templates/user/show.html.twig index 924a9bd..e79bde0 100644 --- a/templates/user/show.html.twig +++ b/templates/user/show.html.twig @@ -33,111 +33,18 @@ {% include 'user/userInformation.html.twig' %} -
-
-
-

Information d'organisation

-
-
-
+{#
#} +{#
#} +{#
#} +{#

Information d'organisation

#} +{#
#} +{#
#} +{#
#} {# TODO: dynamic number of project#} -

Projet : 69 projets vous sont attribués

-
- - -{#
#} -{#
#} -{# {% for app in apps %}#} -{#
#} -{#
#} -{#
#} -{# {% if app.logoMiniUrl %}#} -{# Logo {{ app.name }}#} -{# {% endif %}#} -{#
#} -{#

{{ app.name|title }}

#} -{#
#} -{#
#} - -{#
#} -{#
#} -{#

#} -{# Description :#} -{# {{ app.descriptionSmall|default('Aucune description disponible.')|raw }}#} -{#

#} -{#
#} - -{# #}{# find appGroup once, used in both editable and read-only branches #} -{# {% set appGroup = data.uoas[app.id]|default(null) %}#} - -{# {% if canEdit %}#} -{#
#} -{#
#} -{# #} -{#
#} -{# {% if appGroup %}#} -{# {% for role in data.rolesArray %}#} -{# #} -{# #} -{# {% endfor %}#} -{# {% else %}#} -{#

Aucun rôle défini pour cette application.

#} -{# {% endif %}#} -{#
#} -{# #} -{#
#} -{#
#} -{# {% else %}#} -{#
#} -{# #} -{#
#} -{# {% if appGroup %}#} -{# {% for role in data.rolesArray %}#} -{# #} -{# #} -{# {% endfor %}#} -{# {% else %}#} -{#

Aucun rôle défini pour cette application.

#} -{# {% endif %}#} -{#
#} -{#
#} -{# {% endif %}#} -{#
#} -{#
#} -{#
#} -{# {% endfor %}#} -{#
#} +{#

Projet : 69 projets vous sont attribués

#} {#
#} -
+{#
#}
diff --git a/templates/user/userInformation.html.twig b/templates/user/userInformation.html.twig index ed47e03..4444a28 100644 --- a/templates/user/userInformation.html.twig +++ b/templates/user/userInformation.html.twig @@ -1,31 +1,80 @@ {% block body %} -
+
+
{% if user.pictureUrl is not empty %} - user + user {% endif %} -
+

{{ user.surname|capitalize }} {{ user.name|capitalize }}

- Modifier + {# Trigger the edit modal with the user ID #} +
-

Email: {{ user.email }}

-

Dernière connection: {{ user.lastConnection|date('d/m/Y') }} - à {{ user.lastConnection|date('H:m:s') }}

+

Dernière connection: {{ user.lastConnection|date('d/m/Y') }} à {{ user.lastConnection|date('H:m') }}

Compte crée le: {{ user.createdAt|date('d/m/Y') }}

Numéro de téléphone: {{ user.phoneNumber ? user.phoneNumber : 'Non renseigné' }}

+ + {# Reusable Edit Modal #} +
- {% endblock %} From e50bb0402a38428c3f3f17d18c28a645d228b759 Mon Sep 17 00:00:00 2001 From: Charles Date: Wed, 18 Feb 2026 11:35:44 +0100 Subject: [PATCH 05/19] Set up api calls --- .env | 1 + assets/controllers/project_controller.js | 11 ++--- config/packages/security.yaml | 7 ++- config/services.yaml | 5 ++ src/Controller/ProjectController.php | 6 ++- .../api/Security/SecurityController.php | 22 +++++++++ src/Entity/AccessToken.php | 5 ++ src/EventSubscriber/LoginSubscriber.php | 30 +++++++++--- src/Repository/AccessTokenRepository.php | 3 +- src/Service/SSO/ProjectService.php | 49 +++++++++++++++++++ 10 files changed, 122 insertions(+), 17 deletions(-) create mode 100644 src/Controller/api/Security/SecurityController.php create mode 100644 src/Service/SSO/ProjectService.php diff --git a/.env b/.env index 625e7ee..87ddb29 100644 --- a/.env +++ b/.env @@ -48,6 +48,7 @@ OAUTH_PRIVATE_KEY=%kernel.project_dir%/config/jwt/private.key OAUTH_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.key OAUTH_PASSPHRASE=8170ea18d2e3e05b5c7ae0672a754bf4 OAUTH_ENCRYPTION_KEY=f1b7c279f7992205a0df45e295d07066 +OAUTH_SSO_SECRET='sso-own-secret' ###< league/oauth2-server-bundle ### ###> nelmio/cors-bundle ### diff --git a/assets/controllers/project_controller.js b/assets/controllers/project_controller.js index e7bf00f..bcf2e12 100644 --- a/assets/controllers/project_controller.js +++ b/assets/controllers/project_controller.js @@ -2,10 +2,9 @@ import {Controller} from '@hotwired/stimulus'; import { Modal } from "bootstrap"; import {TabulatorFull as Tabulator} from 'tabulator-tables'; import {eyeIconLink, pencilIcon, TABULATOR_FR_LANG, trashIcon} from "../js/global.js"; -import base_controller from "./base_controller.js"; -export default class extends base_controller { +export default class extends Controller { static values = { listProject : Boolean, orgId: Number, @@ -176,13 +175,13 @@ export default class extends base_controller { this.currentProjectId = projectId; this.modal.show(); - this.nameInputTarget.disabled = true; this.formTitleTarget.textContent = "Modifier le projet"; try { // 1. Ensure checkboxes are loaded first - const apps = await this.fetchAndRenderApplications(this.appListTarget); + await this.loadApplications(); + // 2. Fetch the project data const response = await fetch(`/project/data/${projectId}`); const project = await response.json(); @@ -204,13 +203,13 @@ export default class extends base_controller { } } // Update your openCreateModal to reset the state - async openCreateModal() { + openCreateModal() { this.currentProjectId = null; this.modal.show(); this.nameInputTarget.disabled = false; this.nameInputTarget.value = ""; this.formTitleTarget.textContent = "Nouveau Projet"; - await this.fetchAndRenderApplications(); + this.loadApplications(); } async deleteProject(event) { diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 58afd30..8dae615 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -19,6 +19,10 @@ security: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false + api_token_validation: + pattern: ^/api/validate-token + stateless: true + oauth2: true oauth_userinfo: pattern: ^/oauth2/userinfo stateless: true @@ -65,6 +69,7 @@ security: # Note: Only the *first* access control that matches will be used access_control: - { path: ^/login, roles: PUBLIC_ACCESS } + - { path: ^/api/validate-token, roles: PUBLIC_ACCESS } - { path: ^/password_setup, roles: PUBLIC_ACCESS } - { path: ^/password_reset, roles: PUBLIC_ACCESS } - { path: ^/sso_logout, roles: IS_AUTHENTICATED_FULLY } @@ -76,8 +81,6 @@ security: - { path: ^/oauth2/userinfo, roles: IS_AUTHENTICATED_FULLY } - { path: ^/, roles: ROLE_USER } - - when@test: security: password_hashers: diff --git a/config/services.yaml b/config/services.yaml index b6113ee..7d3bb06 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -10,6 +10,7 @@ parameters: app_url: '%env(APP_URL)%' mercure_secret: '%env(MERCURE_JWT_SECRET)%' logos_directory: '%kernel.project_dir%/public/uploads/logos' + oauth_sso_secret: '%env(OAUTH_SSO_SECRET)%' services: # default configuration for services in *this* file @@ -28,6 +29,10 @@ services: App\MessageHandler\NotificationMessageHandler: arguments: $appUrl: '%app_url%' + App\Service\SSO\ProjectService: + arguments: + $appUrl: '%app_url%' + $clientSecret: '%oauth_sso_secret%' App\EventSubscriber\: resource: '../src/EventSubscriber/' tags: ['kernel.event_subscriber'] diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php index f6511b0..f1a9fa8 100644 --- a/src/Controller/ProjectController.php +++ b/src/Controller/ProjectController.php @@ -8,6 +8,7 @@ use App\Repository\AppsRepository; use App\Repository\OrganizationsRepository; use App\Repository\ProjectRepository; use App\Service\ProjectService; +use App\Service\SSO\ProjectService as SSOProjectService; use App\Service\UserService; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -25,7 +26,9 @@ final class ProjectController extends AbstractController private readonly OrganizationsRepository $organizationsRepository, private readonly ProjectRepository $projectRepository, private readonly ProjectService $projectService, - private readonly UserService $userService, private readonly AppsRepository $appsRepository) + private readonly AppsRepository $appsRepository, + private readonly SSOProjectService $SSOProjectService, + ) { } @@ -61,6 +64,7 @@ final class ProjectController extends AbstractController $project->setOrganization($org); $project->setApplications($data['applications']); $this->entityManager->persist($project); + $this->SSOProjectService->createRemoteProject('http://api.solutions-easy.moi', $project); $this->entityManager->flush(); return new JsonResponse(['message' => 'Project created successfully'], Response::HTTP_CREATED); } diff --git a/src/Controller/api/Security/SecurityController.php b/src/Controller/api/Security/SecurityController.php new file mode 100644 index 0000000..ed95b28 --- /dev/null +++ b/src/Controller/api/Security/SecurityController.php @@ -0,0 +1,22 @@ +getUser(); + + return $this->json([ + 'valid' => true, + 'email' => ($user instanceof \App\Entity\User) ? $user->getUserIdentifier() : null, + 'scopes' => $this->container->get('security.token_storage')->getToken()->getScopes(), + ]); + } +} \ No newline at end of file diff --git a/src/Entity/AccessToken.php b/src/Entity/AccessToken.php index ff51556..5936145 100644 --- a/src/Entity/AccessToken.php +++ b/src/Entity/AccessToken.php @@ -32,4 +32,9 @@ final class AccessToken implements AccessTokenEntityInterface ->getToken($this->jwtConfiguration->signer(), $this->jwtConfiguration->signingKey()); } + public function setUserIdentifier(?string $userIdentifier): void + { + $this->userIdentifier = $userIdentifier; + } + } \ No newline at end of file diff --git a/src/EventSubscriber/LoginSubscriber.php b/src/EventSubscriber/LoginSubscriber.php index 5438de6..3c3febc 100644 --- a/src/EventSubscriber/LoginSubscriber.php +++ b/src/EventSubscriber/LoginSubscriber.php @@ -28,19 +28,37 @@ class LoginSubscriber implements EventSubscriberInterface public function onLoginSuccess(LoginSuccessEvent $event): void { - $user = $event->getUser(); - if($user) { - $user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $user->getUserIdentifier()]); + $passportUser = $event->getUser(); + + // 1. Check if we have a user at all + if (!$passportUser) { + return; + } + + // 2. IMPORTANT: Check if this is a real User entity from your DB. + // If it's a Machine/Client login, it will be an instance of + // League\Bundle\OAuth2ServerBundle\Security\User\ClientCredentialsUser + if (!$passportUser instanceof \App\Entity\User) { + // It's a machine (M2M), so we don't track "last connection" or create manual tokens + return; + } + + // Now we know it's a real human user + $user = $this->entityManager->getRepository(User::class)->findOneBy([ + 'email' => $passportUser->getUserIdentifier() + ]); + + if ($user) { $user->setLastConnection(new \DateTime('now', new \DateTimeZone('Europe/Paris'))); $easySolution = $this->entityManager->getRepository(Client::class)->findOneBy(['name' => 'EasySolution']); - if($easySolution) { + if ($easySolution) { $accessToken = new AccessToken( - identifier: bin2hex(random_bytes(40)), // Generate unique identifier + identifier: bin2hex(random_bytes(40)), expiry: new \DateTimeImmutable('+1 hour', new \DateTimeZone('Europe/Paris')), client: $easySolution, userIdentifier: $user->getUserIdentifier(), - scopes: ['email profile openid apps:easySolutions'] // Empty array if no specific scopes needed + scopes: ['email', 'profile', 'openid', 'apps:easySolutions'] ); $this->entityManager->persist($user); $this->entityManager->persist($accessToken); diff --git a/src/Repository/AccessTokenRepository.php b/src/Repository/AccessTokenRepository.php index 6ac6134..103262b 100644 --- a/src/Repository/AccessTokenRepository.php +++ b/src/Repository/AccessTokenRepository.php @@ -25,8 +25,7 @@ final class AccessTokenRepository implements AccessTokenRepositoryInterface /** @var int|string|null $userIdentifier */ $accessToken = new AccessTokenEntity(); $accessToken->setClient($clientEntity); - $accessToken->setUserIdentifier($userIdentifier); - + $accessToken->setUserIdentifier($userIdentifier ?? $clientEntity->getIdentifier()); foreach ($scopes as $scope) { $accessToken->addScope($scope); } diff --git a/src/Service/SSO/ProjectService.php b/src/Service/SSO/ProjectService.php new file mode 100644 index 0000000..60586ed --- /dev/null +++ b/src/Service/SSO/ProjectService.php @@ -0,0 +1,49 @@ +httpClient->request('POST', $this->appUrl . 'token', [ + 'auth_basic' => ['afc7b28b95b61aeeeae8eaed94c5cfe1', $this->clientSecret], // ID and Secret go here + 'body' => [ + 'grant_type' => 'client_credentials', +// 'scope' => 'project_sync' + ], + ]); +// if (400 === $tokenResponse->getStatusCode() || 500 === $tokenResponse->getStatusCode()) { +// // This will print the actual OAuth2 error (e.g., "invalid_scope" or "unsupported_grant_type") +// dd($tokenResponse->getContent(false)); +// } + $accessToken = $tokenResponse->toArray()['access_token']; +// data must match easy check database + $projectJson = [ + 'id' => $project->getId(), + 'projet' => $project->getName(), + 'entity_id' => 3, + 'bdd' => $project->getBddName(), + 'isactive' => $project->isActive(), + ]; + + // 2. Call the Client Application's Webhook/API + $this->httpClient->request('POST', $clientAppUrl . '/api/v1/project/create', [ + 'headers' => ['Authorization' => 'Bearer ' . $accessToken], + 'json' => $projectJson + ]); + } + +} \ No newline at end of file From 4b92e83f1571eea7456b3a165a30e2ce7dc2ace9 Mon Sep 17 00:00:00 2001 From: Charles Date: Wed, 18 Feb 2026 12:13:36 +0100 Subject: [PATCH 06/19] dynamic sso client data --- src/Service/SSO/ProjectService.php | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/Service/SSO/ProjectService.php b/src/Service/SSO/ProjectService.php index 60586ed..106f8ca 100644 --- a/src/Service/SSO/ProjectService.php +++ b/src/Service/SSO/ProjectService.php @@ -4,31 +4,30 @@ namespace App\Service\SSO; use App\Entity\Project; +use Doctrine\ORM\EntityManagerInterface; +use League\Bundle\OAuth2ServerBundle\Model\Client; use Symfony\Contracts\HttpClient\HttpClientInterface; class ProjectService { public function __construct(private readonly HttpClientInterface $httpClient, - private string $appUrl, - private string $clientSecret) + private string $appUrl, + private string $clientIdentifier, private readonly EntityManagerInterface $entityManager) { } // Inside your SSO Server Service public function createRemoteProject(string $clientAppUrl, Project $project): void { - // 1. Get a token for "ourselves" + // 1. Get a token for "ourselves" -> on en a besoin parce que c'est du M2M. + $portalClient = $this->entityManager->getRepository(Client::class)->findOneBy(['identifier' => $this->clientIdentifier]); $tokenResponse = $this->httpClient->request('POST', $this->appUrl . 'token', [ - 'auth_basic' => ['afc7b28b95b61aeeeae8eaed94c5cfe1', $this->clientSecret], // ID and Secret go here + 'auth_basic' => [$portalClient->getIdentifier(),$portalClient->getSecret()], // ID and Secret go here 'body' => [ 'grant_type' => 'client_credentials', -// 'scope' => 'project_sync' ], ]); -// if (400 === $tokenResponse->getStatusCode() || 500 === $tokenResponse->getStatusCode()) { -// // This will print the actual OAuth2 error (e.g., "invalid_scope" or "unsupported_grant_type") -// dd($tokenResponse->getContent(false)); -// } + $accessToken = $tokenResponse->toArray()['access_token']; // data must match easy check database $projectJson = [ From e941363ca6d47005e116c0f6177364cf4d388d28 Mon Sep 17 00:00:00 2001 From: Charles Date: Wed, 18 Feb 2026 12:13:44 +0100 Subject: [PATCH 07/19] dynamic sso client data --- .env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env b/.env index 87ddb29..2931bca 100644 --- a/.env +++ b/.env @@ -48,7 +48,7 @@ OAUTH_PRIVATE_KEY=%kernel.project_dir%/config/jwt/private.key OAUTH_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.key OAUTH_PASSPHRASE=8170ea18d2e3e05b5c7ae0672a754bf4 OAUTH_ENCRYPTION_KEY=f1b7c279f7992205a0df45e295d07066 -OAUTH_SSO_SECRET='sso-own-secret' +OAUTH_SSO_IDENTIFIER='sso-own-identifier' ###< league/oauth2-server-bundle ### ###> nelmio/cors-bundle ### From 782ca27b5ed3f10b50b386400a4194d49e571d24 Mon Sep 17 00:00:00 2001 From: Charles Date: Wed, 18 Feb 2026 12:13:50 +0100 Subject: [PATCH 08/19] dynamic sso client data --- config/services.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/services.yaml b/config/services.yaml index 7d3bb06..146d0f0 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -10,7 +10,7 @@ parameters: app_url: '%env(APP_URL)%' mercure_secret: '%env(MERCURE_JWT_SECRET)%' logos_directory: '%kernel.project_dir%/public/uploads/logos' - oauth_sso_secret: '%env(OAUTH_SSO_SECRET)%' + oauth_sso_identifier: '%env(OAUTH_SSO_IDENTIFIER)%' services: # default configuration for services in *this* file @@ -32,7 +32,7 @@ services: App\Service\SSO\ProjectService: arguments: $appUrl: '%app_url%' - $clientSecret: '%oauth_sso_secret%' + $clientIdentifier: '%oauth_sso_identifier%' App\EventSubscriber\: resource: '../src/EventSubscriber/' tags: ['kernel.event_subscriber'] From c0a8a9ab821010fcc1065f734676cab12a2b8bc3 Mon Sep 17 00:00:00 2001 From: Charles Date: Wed, 18 Feb 2026 15:38:52 +0100 Subject: [PATCH 09/19] set up edit for project in api --- assets/controllers/project_controller.js | 40 +++++--- assets/icons/bi/calendar.svg | 1 + assets/icons/bi/input-cursor-text.svg | 1 + assets/icons/bi/trash3.svg | 1 + .../picture-in-picture-outline-rounded.svg | 1 + migrations/Version20260218111608.php | 32 +++++++ migrations/Version20260218111821.php | 36 ++++++++ src/Controller/ProjectController.php | 91 ++++++++++++++----- src/Entity/Project.php | 62 ++++++++++++- src/Service/ProjectService.php | 45 ++++++++- src/Service/SSO/ProjectService.php | 55 ++++++++--- templates/organization/show.html.twig | 85 +++++++++++++---- 12 files changed, 383 insertions(+), 67 deletions(-) create mode 100644 assets/icons/bi/calendar.svg create mode 100644 assets/icons/bi/input-cursor-text.svg create mode 100644 assets/icons/bi/trash3.svg create mode 100644 assets/icons/material-symbols/picture-in-picture-outline-rounded.svg create mode 100644 migrations/Version20260218111608.php create mode 100644 migrations/Version20260218111821.php diff --git a/assets/controllers/project_controller.js b/assets/controllers/project_controller.js index bcf2e12..6b8cc40 100644 --- a/assets/controllers/project_controller.js +++ b/assets/controllers/project_controller.js @@ -10,7 +10,7 @@ export default class extends Controller { orgId: Number, admin: Boolean } - static targets = ["modal", "appList", "nameInput", "formTitle"]; + static targets = ["modal", "appList", "nameInput", "formTitle", "timestampSelect", "deletionSelect"]; connect(){ if(this.listProjectValue){ this.table(); @@ -137,35 +137,44 @@ export default class extends Controller { async submitForm(event) { event.preventDefault(); - const formData = new FormData(event.target); + const form = event.target; + const formData = new FormData(form); // This automatically picks up the 'logo' file - const payload = { - organizationId: this.orgIdValue, - applications: formData.getAll('applications[]') - }; - - // Only include name if it wasn't disabled (new projects) - if (!this.nameInputTarget.disabled) { - payload.name = formData.get('name'); + // 1. Validate File Format + const logoFile = formData.get('logo'); + if (logoFile && logoFile.size > 0) { + const allowedTypes = ['image/jpeg', 'image/png', 'image/jpg']; + if (!allowedTypes.includes(logoFile.type)) { + alert("Format invalide. Veuillez utiliser uniquement des fichiers PNG ou JPG."); + return; // Stop submission + } } + // 2. Prepare for Multipart sending + // Since we are using FormData, we don't need JSON.stringify or 'Content-Type': 'application/json' + // We add the extra fields to the formData object + formData.append('organizationId', this.orgIdValue); + const url = this.currentProjectId ? `/project/edit/${this.currentProjectId}/ajax` : `/project/new/ajax`; const response = await fetch(url, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) + // IMPORTANT: Do NOT set Content-Type header when sending FormData with files + // The browser will set 'multipart/form-data' and the boundary automatically + body: formData }); if (response.ok) { this.modal.hide(); - // Use Tabulator's setData() instead of reload() for better UX if possible location.reload(); } else { + const result = await response.json(); if (response.status === 409) { - alert("Un projet avec ce nom existe déjà. Veuillez choisir un nom différent."); + alert("Un projet avec ce nom existe déjà."); + } else { + alert(result.error || "Une erreur est survenue."); } } } @@ -188,6 +197,9 @@ export default class extends Controller { // 3. Set the name this.nameInputTarget.value = project.name; + console.log(project); + this.timestampSelectTarget.value = project.timestampPrecision; + this.deletionSelectTarget.value = project.deletionAllowed; // 4. Check the boxes // We look for all checkboxes inside our appList target diff --git a/assets/icons/bi/calendar.svg b/assets/icons/bi/calendar.svg new file mode 100644 index 0000000..01b8c56 --- /dev/null +++ b/assets/icons/bi/calendar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/bi/input-cursor-text.svg b/assets/icons/bi/input-cursor-text.svg new file mode 100644 index 0000000..f2e716b --- /dev/null +++ b/assets/icons/bi/input-cursor-text.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/bi/trash3.svg b/assets/icons/bi/trash3.svg new file mode 100644 index 0000000..de8e5aa --- /dev/null +++ b/assets/icons/bi/trash3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/material-symbols/picture-in-picture-outline-rounded.svg b/assets/icons/material-symbols/picture-in-picture-outline-rounded.svg new file mode 100644 index 0000000..d89ffef --- /dev/null +++ b/assets/icons/material-symbols/picture-in-picture-outline-rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/migrations/Version20260218111608.php b/migrations/Version20260218111608.php new file mode 100644 index 0000000..9796e97 --- /dev/null +++ b/migrations/Version20260218111608.php @@ -0,0 +1,32 @@ +addSql('ALTER TABLE project ADD logo VARCHAR(255) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('ALTER TABLE project DROP logo'); + } +} diff --git a/migrations/Version20260218111821.php b/migrations/Version20260218111821.php new file mode 100644 index 0000000..04631cd --- /dev/null +++ b/migrations/Version20260218111821.php @@ -0,0 +1,36 @@ +addSql('ALTER TABLE project ADD timestamp_precision VARCHAR(10) DEFAULT NULL'); + $this->addSql('ALTER TABLE project ADD deletion_allowed BOOLEAN DEFAULT NULL'); + $this->addSql('ALTER TABLE project ADD audits_enabled BOOLEAN DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('ALTER TABLE project DROP timestamp_precision'); + $this->addSql('ALTER TABLE project DROP deletion_allowed'); + $this->addSql('ALTER TABLE project DROP audits_enabled'); + } +} diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php index f1a9fa8..fd98427 100644 --- a/src/Controller/ProjectController.php +++ b/src/Controller/ProjectController.php @@ -44,39 +44,66 @@ final class ProjectController extends AbstractController public function new(Request $request): JsonResponse { $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN'); - $data = json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR); - if (!$data) { - return new JsonResponse(['error' => 'Invalid JSON'], Response::HTTP_BAD_REQUEST); - } $org = $this->organizationsRepository->findOneBy(['id' => $data['organizationId']]); - if(!$org) { - return new JsonResponse(['error' => 'Organization not found'], Response::HTTP_NOT_FOUND); + + $orgId = $request->request->get('organizationId'); + $name = $request->request->get('name'); + $applications = $request->request->all('applications') ?? []; // Expects applications[] from JS + + $org = $this->organizationsRepository->find($orgId); + if (!$org) { + return new JsonResponse(['error' => 'Organization not found'], 404); } - $sanitizedDbName = $this->projectService->getProjectDbName($data['name'], $org->getProjectPrefix()); - if($this->projectRepository->findOneBy(['bddName' => $sanitizedDbName])) { - return new JsonResponse(['error' => 'A project with the same name already exists'], Response::HTTP_CONFLICT); + + // 2. Handle File Upload + $logoFile = $request->files->get('logo'); + $logoPath = null; + + + + // 3. Project Creation + $sanitizedDbName = $this->projectService->getProjectDbName($name, $org->getProjectPrefix()); + if ($this->projectRepository->findOneBy(['bddName' => $sanitizedDbName])) { + return new JsonResponse(['error' => 'A project with the same name already exists'], 409); } - if(!$this->projectService->isApplicationArrayValid($data['applications'])) { - return new JsonResponse(['error' => 'Invalid applications array'], Response::HTTP_BAD_REQUEST); + if ($logoFile) { + $logoPath = $this->projectService->handleLogoUpload($logoFile, $sanitizedDbName); } $project = new Project(); - $project->setName($data['name']); + $project->setName($name); $project->setBddName($sanitizedDbName); $project->setOrganization($org); - $project->setApplications($data['applications']); + $project->setApplications($applications); + + $project->setTimestampPrecision($request->request->get('timestamp')); + $project->setDeletionAllowed($request->request->getBoolean('deletion')); + + if ($logoPath) { + $project->setLogo($logoPath); + } + $this->entityManager->persist($project); - $this->SSOProjectService->createRemoteProject('http://api.solutions-easy.moi', $project); - $this->entityManager->flush(); - return new JsonResponse(['message' => 'Project created successfully'], Response::HTTP_CREATED); + $this->entityManager->flush(); //On met le flush avant parce qu'on a besoin de l'ID du projet pour la création distante. + //Oui ducoup c'est chiant parce que le projet est crée même s'il y a une erreur API, mais OH ffs at that point. + // Remote creation logic + try { + $this->SSOProjectService->createRemoteProject('http://api.solutions-easy.moi', $project); + } catch (\Exception $e) { + return new JsonResponse(['error' => 'Remote creation failed: ' . $e->getMessage()], 500); + } + + + + return new JsonResponse(['message' => 'Project created successfully'], 201); } #[Route(path:'/edit/{id}/ajax', name: '_edit', methods: ['POST'])] public function edit(Request $request, int $id): JsonResponse { $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN'); - $data = json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR); - if (!$data) { - return new JsonResponse(['error' => 'Invalid JSON'], Response::HTTP_BAD_REQUEST); - } $org = $this->organizationsRepository->findOneBy(['id' => $data['organizationId']]); + $orgId = $request->request->get('organizationId'); + $applications = $request->request->all('applications') ?? []; // Expects applications[] from JS + + $org = $this->organizationsRepository->findOneBy(['id' => $orgId]); if(!$org) { return new JsonResponse(['error' => 'Organization not found'], Response::HTTP_NOT_FOUND); } @@ -84,9 +111,29 @@ final class ProjectController extends AbstractController if(!$project) { return new JsonResponse(['error' => 'Project not found'], Response::HTTP_NOT_FOUND); } - $project->setApplications($data['applications']); + $logoFile = $request->files->get('logo'); + $logoPath = null; + + if ($logoFile) { + $logoPath = $this->projectService->handleLogoUpload($logoFile, $project->getBddName()); + } + + $project->setApplications($applications); $project->setModifiedAt(new \DateTimeImmutable()); + $project->setTimestampPrecision($request->request->get('timestamp')); + $project->setDeletionAllowed($request->request->getBoolean('deletion')); + if ($logoPath) { + $project->setLogo($logoPath); + } + $this->entityManager->persist($project); + // Remote editing logic + try { + $this->SSOProjectService->editRemoteProject('http://api.solutions-easy.moi', $project); + } catch (\Exception $e) { + return new JsonResponse(['error' => 'Remote creation failed: ' . $e->getMessage()], 500); + } + $this->entityManager->flush(); return new JsonResponse(['message' => 'Project updated successfully'], Response::HTTP_OK); } @@ -148,6 +195,8 @@ final class ProjectController extends AbstractController 'id' => $project->getId(), 'name' => ucfirst($project->getName()), 'applications' => $project->getApplications(), + 'timestampPrecision'=> $project->getTimestampPrecision(), + 'deletionAllowed' => $project->isDeletionAllowed(), ]); } diff --git a/src/Entity/Project.php b/src/Entity/Project.php index 1512564..d19607a 100644 --- a/src/Entity/Project.php +++ b/src/Entity/Project.php @@ -35,9 +35,21 @@ class Project #[ORM\Column] private ?bool $isDeleted = null; - #[ORM\Column(length: 255, nullable: true)] + #[ORM\Column(length: 255)] private ?string $bddName = null; + #[ORM\Column(length: 255, nullable: true)] + private ?string $logo = null; + + #[ORM\Column(length: 10)] + private ?string $timestampPrecision = null; + + #[ORM\Column()] + private ?bool $deletionAllowed = null; + + #[ORM\Column(nullable: true)] + private ?bool $auditsEnabled = null; + public function __construct() { $this->createdAt = new \DateTimeImmutable(); @@ -146,4 +158,52 @@ class Project return $this; } + + public function getLogo(): ?string + { + return $this->logo; + } + + public function setLogo(?string $logo): static + { + $this->logo = $logo; + + return $this; + } + + public function getTimestampPrecision(): ?string + { + return $this->timestampPrecision; + } + + public function setTimestampPrecision(?string $timestampPrecision): static + { + $this->timestampPrecision = $timestampPrecision; + + return $this; + } + + public function isDeletionAllowed(): ?bool + { + return $this->deletionAllowed; + } + + public function setDeletionAllowed(?bool $deletionAllowed): static + { + $this->deletionAllowed = $deletionAllowed; + + return $this; + } + + public function isAuditsEnabled(): ?bool + { + return $this->auditsEnabled; + } + + public function setAuditsEnabled(?bool $auditsEnabled): static + { + $this->auditsEnabled = $auditsEnabled; + + return $this; + } } diff --git a/src/Service/ProjectService.php b/src/Service/ProjectService.php index 11e8d4f..9663c15 100644 --- a/src/Service/ProjectService.php +++ b/src/Service/ProjectService.php @@ -3,12 +3,16 @@ namespace App\Service; use App\Repository\AppsRepository; +use App\Service\LoggerService; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\HttpFoundation\File\Exception\FileException; use Symfony\Component\String\Slugger\AsciiSlugger; class ProjectService{ - public function __construct(private readonly AppsRepository $appsRepository) + + public function __construct(private readonly AppsRepository $appsRepository, private readonly Security $security, private readonly LoggerService $loggerService) { } @@ -40,4 +44,43 @@ class ProjectService{ } return true; } + + public function handleLogoUpload($logoFile, $projectBddName): ?string + { + // 1. Define the destination directory (adjust path as needed, e.g., 'public/uploads/profile_pictures') + $destinationDir = 'uploads/project_logos'; + + // 2. Create the directory if it doesn't exist + if (!file_exists($destinationDir)) { + // 0755 is the standard permission (Owner: read/write/exec, Others: read/exec) + if (!mkdir($destinationDir, 0755, true) && !is_dir($destinationDir)) { + throw new \RuntimeException(sprintf('Directory "%s" was not created', $destinationDir)); + } + } + + $extension = $logoFile->guessExtension(); + + // Sanitize the filename to remove special characters/spaces to prevent filesystem errors + $customFilename = $projectBddName . '.' . $extension; + + try { + // 4. Move the file to the destination directory + $logoFile->move($destinationDir, $customFilename); + + // 5. Update the user entity with the relative path + // Ensure you store the path relative to your public folder usually + return $destinationDir . '/' . $customFilename; + + } catch (\Exception $e) { + // 6. Log the critical error as requested + $this->loggerService->logError('File upload failed',[ + 'target_user_id' => $this->security->getUser()->getId(), + 'message' => $e->getMessage(), + 'file_name' => $customFilename, + ]); + + // Optional: Re-throw the exception if you want the controller/user to know the upload failed + throw new FileException('File upload failed.'); + } + } } diff --git a/src/Service/SSO/ProjectService.php b/src/Service/SSO/ProjectService.php index 106f8ca..77df55d 100644 --- a/src/Service/SSO/ProjectService.php +++ b/src/Service/SSO/ProjectService.php @@ -7,6 +7,7 @@ use App\Entity\Project; use Doctrine\ORM\EntityManagerInterface; use League\Bundle\OAuth2ServerBundle\Model\Client; use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; class ProjectService { @@ -20,23 +21,12 @@ class ProjectService public function createRemoteProject(string $clientAppUrl, Project $project): void { // 1. Get a token for "ourselves" -> on en a besoin parce que c'est du M2M. - $portalClient = $this->entityManager->getRepository(Client::class)->findOneBy(['identifier' => $this->clientIdentifier]); - $tokenResponse = $this->httpClient->request('POST', $this->appUrl . 'token', [ - 'auth_basic' => [$portalClient->getIdentifier(),$portalClient->getSecret()], // ID and Secret go here - 'body' => [ - 'grant_type' => 'client_credentials', - ], - ]); + $tokenResponse = $this->getTokenResponse(); $accessToken = $tokenResponse->toArray()['access_token']; // data must match easy check database - $projectJson = [ - 'id' => $project->getId(), - 'projet' => $project->getName(), - 'entity_id' => 3, - 'bdd' => $project->getBddName(), - 'isactive' => $project->isActive(), - ]; + $projectJson = $this->getTokenResponse($project); + // 2. Call the Client Application's Webhook/API $this->httpClient->request('POST', $clientAppUrl . '/api/v1/project/create', [ @@ -45,4 +35,41 @@ class ProjectService ]); } + public function editRemoteProject(string $clientAppUrl, Project $project): void + { + $tokenResponse = $this->getTokenResponse(); + + $accessToken = $tokenResponse->toArray()['access_token']; +// data must match easy check database + $projectJson = $this->getProjectToJson($project); + // 2. Call the Client Application's Webhook/API + $this->httpClient->request('PUT', $clientAppUrl . '/api/v1/project/edit/'. $project->getId(), [ + 'headers' => ['Authorization' => 'Bearer ' . $accessToken], + 'json' => $projectJson + ]); + } + + + public function getTokenResponse(): ResponseInterface{ + $portalClient = $this->entityManager->getRepository(Client::class)->findOneBy(['identifier' => $this->clientIdentifier]); + return $this->httpClient->request('POST', $this->appUrl . 'token', [ + 'auth_basic' => [$portalClient->getIdentifier(),$portalClient->getSecret()], // ID and Secret go here + 'body' => [ + 'grant_type' => 'client_credentials', + ], + ]); + } + + public function getProjectToJson(Project $project): array { + return [ + 'id' => $project->getId(), + 'projet' => $project->getName(), + 'entity_id' => 3, + 'bdd' => $project->getBddName(), + 'isactive' => $project->isActive(), + 'logo' => $project->getLogo(), + 'timestamp'=> $project->getTimestampPrecision(), + 'deletion' => $project->isDeletionAllowed() + ]; + } } \ No newline at end of file diff --git a/templates/organization/show.html.twig b/templates/organization/show.html.twig index 450190f..b83fc9f 100644 --- a/templates/organization/show.html.twig +++ b/templates/organization/show.html.twig @@ -1,7 +1,7 @@ {% extends 'base.html.twig' %} {% block body %} - {% set isSA = is_granted('ROLE_SUPER_ADMIN')%} + {% set isSA = is_granted('ROLE_SUPER_ADMIN') %}
{% for type, messages in app.flashes %} {% for message in messages %} @@ -70,7 +70,8 @@
{# New User Modal #} -