diff --git a/assets/controllers/user_controller.js b/assets/controllers/user_controller.js index 2a53cad..e7834b0 100644 --- a/assets/controllers/user_controller.js +++ b/assets/controllers/user_controller.js @@ -1,7 +1,15 @@ import {Controller} from '@hotwired/stimulus'; import Choices from 'choices.js'; import {TabulatorFull as Tabulator} from 'tabulator-tables'; -import {activateUserIcon, deactivateUserIcon, eyeIconLink, sendEmailIcon, TABULATOR_FR_LANG} from "../js/global.js"; +import { + activateUserIcon, + deactivateUserIcon, + eyeIconLink, + sendEmailIcon, + TABULATOR_FR_LANG, + trashIconForm +} from "../js/global.js"; +import { Modal } from "bootstrap"; export default class extends Controller { @@ -18,7 +26,7 @@ export default class extends Controller { orgId: Number } - static targets = ["select", "statusButton"]; + static targets = ["select", "statusButton", "modal", "userSelect"]; connect() { this.roleSelect(); @@ -34,6 +42,9 @@ export default class extends Controller { if (this.listOrganizationValue) { this.tableOrganization() } + if (this.hasModalTarget) { + this.modal = new Modal(this.modalTarget); + } } @@ -499,8 +510,9 @@ export default class extends Controller { headerSort: false, formatter: (cell) => { const url = cell.getValue(); + const orgId = this.orgIdValue; if (url) { - eyeIconLink(url); + return trashIconForm(url, orgId); } return ''; } @@ -918,4 +930,92 @@ export default class extends Controller { btn.dataset.active = "false"; } } + + async openAddAdminModal() { + this.modal.show(); + await this.loadAvailableUsers(); + } + + async loadAvailableUsers() { + try { + const response = await fetch(`/organization/${this.orgIdValue}/users`); + const data = await response.json(); + + if (data.users) { + this.userSelectTarget.innerHTML = '' + + data.users.map(user => ` + + `).join(''); + } + } catch (error) { + this.userSelectTarget.innerHTML = ''; + } + } + + async submitAddAdmin(event) { + event.preventDefault(); + const formData = new FormData(event.target); + const userId = formData.get('userId'); + + try { + const response = await fetch(`/user/organization/admin/${userId}`, { + method: 'POST', + body: formData, // Sends userId, status="add", and organizationId + headers: {'X-Requested-With': 'XMLHttpRequest'} + }); + + if (response.ok) { + this.modal.hide(); + // If you use Tabulator, refresh it here + // e.g., this.table.setData(); + location.reload(); + } else { + if (response.status === 409) { + alert("Cet utilisateur est déjà administrateur de l'organisation."); + return; + } + alert("Erreur lors de l'ajout de l'administrateur."); + } + } catch (error) { + alert("Une erreur est survenue."); + } + } + + async removeAdmin(event) { + // 1. Prevent any default behavior + event.preventDefault(); + + const button = event.currentTarget; + const url = button.dataset.url; + const organizationId = button.dataset.orgId; + + if (!confirm("Voulez-vous vraiment retirer les droits d'administrateur à cet utilisateur ?")) { + return; + } + + // 2. Prepare the payload (matching your previous hidden fields) + const formData = new FormData(); + formData.append('status', 'remove'); + formData.append('organizationId', organizationId); + + try { + const response = await fetch(url, { + method: 'POST', + body: formData, + headers: { + 'X-Requested-With': 'XMLHttpRequest' + } + }); + + if (response.ok) { + // 3. Success! Reload the page to see the updated list + location.reload(); + } else { + const errorData = await response.json(); + alert("Erreur: " + (errorData.error || "Impossible de supprimer les droits.")); + } + } catch (error) { + alert("Une erreur réseau est survenue."); + } + } } \ No newline at end of file diff --git a/assets/js/global.js b/assets/js/global.js index 80d5c79..42d51d4 100644 --- a/assets/js/global.js +++ b/assets/js/global.js @@ -41,7 +41,7 @@ export function pencilIcon() { ` } -export function trashIcon(url) { +export function trashIcon() { return ` @@ -50,6 +50,18 @@ export function trashIcon(url) { ` } +export function trashIconForm(url, organizationId) { + return ` + `; +} + export function deactivateUserIcon() { return ` diff --git a/src/Controller/OrganizationController.php b/src/Controller/OrganizationController.php index 89813ed..b36cc29 100644 --- a/src/Controller/OrganizationController.php +++ b/src/Controller/OrganizationController.php @@ -10,6 +10,7 @@ use App\Entity\UserOrganizatonApp; use App\Entity\UsersOrganizations; use App\Form\OrganizationForm; use App\Repository\OrganizationsRepository; +use App\Repository\UsersOrganizationsRepository; use App\Service\ActionService; use App\Service\AwsService; use App\Service\LoggerService; @@ -43,7 +44,7 @@ class OrganizationController extends AbstractController private readonly ActionService $actionService, private readonly UserOrganizationService $userOrganizationService, private readonly OrganizationsRepository $organizationsRepository, - private readonly LoggerService $loggerService) + private readonly LoggerService $loggerService, private readonly UsersOrganizationsRepository $usersOrganizationsRepository) { } @@ -323,4 +324,31 @@ class OrganizationController extends AbstractController 'total' => $total, ]); } + + #[Route(path: '/{id}/users', name: 'users', methods: ['GET'])] + public function users($id): JsonResponse{ + $this->denyAccessUnlessGranted("ROLE_USER"); + $actingUser = $this->getUser(); + $organization = $this->organizationsRepository->find($id); + if (!$organization) { + $this->loggerService->logEntityNotFound('Organization', [ + 'org_id' => $id, + 'message' => 'Organization not found for users endpoint' + ], $actingUser->getUserIdentifier()); + return $this->json(['error' => self::NOT_FOUND], Response::HTTP_NOT_FOUND); + } + if (!$this->userService->isAdminOfOrganization($organization) && !$this->isGranted("ROLE_ADMIN")) { + $this->loggerService->logAccessDenied($actingUser->getUserIdentifier()); + return $this->json(['error' => self::ACCESS_DENIED], Response::HTTP_FORBIDDEN); + } + $uos = $this->usersOrganizationsRepository->findBy(['organization' => $organization, 'isActive' => true]); + $users = array_map(function (UsersOrganizations $uo) { + $user = $uo->getUsers(); + return [ + 'id' => $user->getId(), + 'email' => $user->getEmail() + ]; + }, $uos); + return $this->json(['users' => $users]); + } } diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 0619640..64fade1 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -603,30 +603,24 @@ class UserController extends AbstractController $this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getUserIdentifier()); throw $this->createNotFoundException(self::NOT_FOUND); } - if ($this->userService->isAdminOfOrganization($org) || $this->isGranted("ROLE_ADMIN")) { - $uos = $this->uoRepository->findBy(['organization' => $org]); + if ($this->userService->isAdminOfOrganization($org) || $this->isGranted("ROLE_ADMIN")){ $roleAdmin = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN']); - $users = []; - foreach ($uos as $uo) { - if ($this->entityManager->getRepository(UserOrganizatonApp::class)->findOneBy(['userOrganization' => $uo, 'role' => $roleAdmin])) { - $users[] = $uo; - } - } - - - // Map to array (keep isConnected) - $data = array_map(function (UsersOrganizations $uo) { - $user = $uo->getUsers(); + if (!$roleAdmin) { + $this->loggerService->logEntityNotFound('Role', ['name' => 'ADMIN'], $actingUser->getUserIdentifier()); + throw $this->createNotFoundException(self::NOT_FOUND); + } + $uos = $this->uoRepository->findBy(['organization' => $org, 'role' => $roleAdmin, 'statut' => "ACCEPTED", 'isActive' => true]); + $data = array_map(function (UsersOrganizations $uos) { + $user = $uos->getUsers(); $initials = $user->getName()[0] . $user->getSurname()[0]; return [ 'pictureUrl' => $user->getPictureUrl(), 'email' => $user->getEmail(), 'isConnected' => $this->userService->isUserConnected($user->getUserIdentifier()), - 'showUrl' => $this->generateUrl('user_show', ['id' => $user->getId()]), + 'showUrl' => $this->generateUrl('user_organization_admin_status', ['id' => $user->getId()]), 'initials' => strtoupper($initials), ]; - }, $users); - + }, $uos); return $this->json([ 'data' => $data, ]); @@ -774,5 +768,70 @@ class UserController extends AbstractController } return $this->render('security/login.html.twig', ['error'=> null, 'last_username' => $user->getEmail()]); } + + #[Route(path: '/organization/admin/{id}', name: 'organization_admin_status', methods: ['POST'])] + public function organizationAdminStatus(int $id, Request $request): JsonResponse + { + $this->denyAccessUnlessGranted('ROLE_ADMIN'); + $actingUser = $this->getUser(); + $user = $this->userRepository->find($id); + if (!$user) { + $this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getUserIdentifier()); + throw $this->createNotFoundException(self::NOT_FOUND); + } + $orgId = $request->request->get('organizationId'); + $org = $this->organizationRepository->find($orgId); + if (!$org) { + $this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getUserIdentifier()); + throw $this->createNotFoundException(self::NOT_FOUND); + } + $uo = $this->uoRepository->findOneBy(['users' => $user,'organization' => $org]); + if (!$uo) { + $this->loggerService->logEntityNotFound('UsersOrganization', ['user_id' => $id, 'organization_id' => $uo->getOrganization()->getId()], $actingUser->getUserIdentifier()); + throw $this->createNotFoundException(self::NOT_FOUND); + } + $status = $request->request->get('status'); + if ($status === 'add') { + $roleAdmin = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN']); + if (!$roleAdmin) { + $this->loggerService->logEntityNotFound('Role', ['name' => 'ADMIN'], $actingUser->getUserIdentifier()); + throw $this->createNotFoundException(self::NOT_FOUND); + } + if ($uo->getRole() == $roleAdmin) { + return new JsonResponse(['error' => 'User is already admin'], Response::HTTP_CONFLICT); + } + $uo->setRole($roleAdmin); + $uo->setModifiedAt(new \DateTimeImmutable()); + $this->entityManager->persist($uo); + $this->entityManager->flush(); + $this->loggerService->logOrganizationInformation($uo->getOrganization()->getId(), $actingUser->getUserIdentifier(), "Role ADMIN added to user with uo id : {$uo->getId()}"); + $this->actionService->createAction("Grant organization admin rights", $actingUser, $uo->getOrganization(), "for user " . $uo->getUSers()->getUserIdentifier()); + return new JsonResponse(['status' => 'added'], Response::HTTP_OK); + } + + if ($status === 'remove') { + $roleUser = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'USER']); + if (!$roleUser) { + $this->loggerService->logEntityNotFound('Role', ['name' => 'ADMIN'], $actingUser->getUserIdentifier()); + throw $this->createNotFoundException(self::NOT_FOUND); + } + $uo->setRole($roleUser); + $uo->setModifiedAt(new \DateTimeImmutable()); + $this->entityManager->persist($uo); + $this->entityManager->flush(); + $this->loggerService->logOrganizationInformation($uo->getOrganization()->getId(), $actingUser->getUserIdentifier(), "Role ADMIN removed from user with uo id : {$uo->getId()}"); + $this->actionService->createAction("Revoke organization admin rights", $actingUser, $uo->getOrganization(), "for user " . $uo->getUSers()->getUserIdentifier()); + return new JsonResponse(['status' => 'removed'], Response::HTTP_OK); + } + //invalid status + $this->loggerService->logError('Invalid status provided for organizationAdminStatus', [ + 'requested_status' => $status, + 'target_user_id' => $uo->getUsers()->getId(), + 'organization_id' => $uo->getOrganization()->getId(), + ]); + throw $this->createNotFoundException(self::NOT_FOUND); + } + + } diff --git a/templates/organization/show.html.twig b/templates/organization/show.html.twig index 8605ad1..60ce8e1 100644 --- a/templates/organization/show.html.twig +++ b/templates/organization/show.html.twig @@ -69,18 +69,49 @@ -
-
-

- Administrateurs -

+
+ +
+

Administrateurs

+
+
-
+
+
+ + {# Modal for Adding Admin #} +