diff --git a/assets/controllers/organization_controller.js b/assets/controllers/organization_controller.js index 225c662..a1fc967 100644 --- a/assets/controllers/organization_controller.js +++ b/assets/controllers/organization_controller.js @@ -18,7 +18,7 @@ export default class extends Controller { this.loadActivities(); setInterval(() => { this.loadActivities(); - }, 60000); // Refresh every 60 seconds + }, 300000); // Refresh every 5 minutes } if (this.tableValue && this.sadminValue) { this.table(); diff --git a/assets/controllers/project_controller.js b/assets/controllers/project_controller.js new file mode 100644 index 0000000..bcf2e12 --- /dev/null +++ b/assets/controllers/project_controller.js @@ -0,0 +1,235 @@ +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"; + + +export default class extends Controller { + static values = { + listProject : Boolean, + orgId: Number, + admin: Boolean + } + static targets = ["modal", "appList", "nameInput", "formTitle"]; + connect(){ + if(this.listProjectValue){ + this.table(); + } + this.modal = new Modal(this.modalTarget); + } + + table(){ + const columns = [ + {title: "ID ", field: "id", visible: false}, + {title: "Nom du projet ", field: "name", headerFilter: "input", widthGrow: 2, vertAlign: "middle"}, + { + title: "Applications", + field: "applications", + headerSort: false, + hozAlign: "left", + formatter: (cell) => { + const apps = cell.getValue(); + if (!apps || apps.length === 0) { + return "Aucune"; + } + + // Wrap everything in a flex container to keep them on one line + const content = apps.map(app => ` +
+ ${app.name} +
+ `).join(''); + + return `
${content}
`; + } + } + ]; + // 2. Add the conditional column if admin value is true + if (this.adminValue) { + columns.push({ + title: "Base de données", + field: "bddName", + hozAlign: "left", + }, + { + title: "Actions", + field: "id", + width: 120, + hozAlign: "center", + headerSort: false, + formatter: (cell) => { + const id = cell.getValue(); + // Return a button that Stimulus can listen to + return `
+ + +
`; + } + }); + } + + const tabulator = new Tabulator("#tabulator-projectListOrganization", { + langs: TABULATOR_FR_LANG, + locale: "fr", + ajaxURL: `/project/organization/data`, + ajaxConfig: "GET", + pagination: true, + paginationMode: "remote", + paginationSize: 15, + + ajaxParams: {orgId: this.orgIdValue}, + ajaxResponse: (url, params, response) => response, + paginationDataSent: {page: "page", size: "size"}, + paginationDataReceived: {last_page: "last_page"}, + + ajaxSorting: true, + ajaxFiltering: true, + filterMode: "remote", + + ajaxURLGenerator: function(url, config, params) { + let queryParams = new URLSearchParams(); + queryParams.append('orgId', params.orgId); + queryParams.append('page', params.page || 1); + queryParams.append('size', params.size || 15); + + // Add filters + if (params.filter) { + params.filter.forEach(filter => { + queryParams.append(`filter[${filter.field}]`, filter.value); + }); + } + + return `${url}?${queryParams.toString()}`; + }, + rowHeight: 60, + layout: "fitColumns", // activate French + + columns + }) + } + + + async loadApplications() { + try { + const response = await fetch('/application/data/all'); + const apps = await response.json(); + + this.appListTarget.innerHTML = apps.map(app => ` +
+
+ + +
+
+ `).join(''); + } catch (error) { + this.appListTarget.innerHTML = '
Erreur de chargement.
'; + } + } + + async submitForm(event) { + event.preventDefault(); + const formData = new FormData(event.target); + + 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'); + } + + 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) + }); + + if (response.ok) { + this.modal.hide(); + // Use Tabulator's setData() instead of reload() for better UX if possible + location.reload(); + } else { + if (response.status === 409) { + alert("Un projet avec ce nom existe déjà. Veuillez choisir un nom différent."); + } + } + } + + async openEditModal(event) { + const projectId = event.currentTarget.dataset.id; + 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(); + + // 2. Fetch the project data + const response = await fetch(`/project/data/${projectId}`); + const project = await response.json(); + + // 3. Set the name + this.nameInputTarget.value = project.name; + + // 4. Check the boxes + // We look for all checkboxes inside our appList target + const checkboxes = this.appListTarget.querySelectorAll('input[type="checkbox"]'); + + checkboxes.forEach(cb => { + cb.checked = project.applications.includes(cb.value); + }); + + } catch (error) { + console.error("Error loading project data", error); + alert("Erreur lors de la récupération des données du projet."); + } + } +// Update your openCreateModal to reset the state + openCreateModal() { + this.currentProjectId = null; + this.modal.show(); + this.nameInputTarget.disabled = false; + this.nameInputTarget.value = ""; + this.formTitleTarget.textContent = "Nouveau Projet"; + this.loadApplications(); + } + + async deleteProject(event) { + const projectId = event.currentTarget.dataset.id; + if (!confirm("Êtes-vous sûr de vouloir supprimer ce projet ?")) { + return; + } + + try { + const response = await fetch(`/project/delete/${projectId}/ajax`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }); + + if (response.ok) { + location.reload(); + } + }catch (error) { + console.error("Error deleting project", error); + alert("Erreur lors de la suppression du projet."); + } + } +} \ No newline at end of file diff --git a/assets/controllers/user_controller.js b/assets/controllers/user_controller.js index 6ae7662..2a53cad 100644 --- a/assets/controllers/user_controller.js +++ b/assets/controllers/user_controller.js @@ -18,7 +18,7 @@ export default class extends Controller { orgId: Number } - static targets = ["select"]; + static targets = ["select", "statusButton"]; connect() { this.roleSelect(); @@ -180,7 +180,7 @@ export default class extends Controller { const isActive = Boolean(statut); const actionClass = isActive ? 'deactivate-user' : 'activate-user'; - const actionTitle = isActive ? 'Désactiver' : 'Réactiver'; + const actionTitle = isActive ? 'Désactiver l\'utilisateur': 'Réactiver l\'utilisateur'; const actionColorClass = isActive ? 'color-secondary' : 'color-primary'; // SVGs @@ -678,7 +678,7 @@ export default class extends Controller { const isActive = (statut === "ACTIVE"); const actionClass = isActive ? 'deactivate-user' : 'activate-user'; - const actionTitle = isActive ? 'Désactiver' : 'Réactiver'; + const actionTitle = isActive ? 'Désactiver l\'utilisateur': 'Réactiver l\'utilisateur' ; const actionColorClass = isActive ? 'color-secondary' : 'color-primary'; // SVGs @@ -874,4 +874,48 @@ export default class extends Controller { columns }); }; + + async toggleStatus(event) { + event.preventDefault(); + + const button = this.statusButtonTarget; + const isActive = button.dataset.active === "true"; + const newStatus = isActive ? 'deactivate' : 'activate'; + const confirmMsg = isActive ? "Désactiver cet utilisateur ?" : "Réactiver cet utilisateur ?"; + + if (!confirm(confirmMsg)) return; + + try { + const formData = new FormData(); + formData.append('status', newStatus); + + const response = await fetch(`/user/activeStatus/${this.idValue}`, { + method: 'POST', + body: formData, + headers: {'X-Requested-With': 'XMLHttpRequest'} + }); + + if (response.ok) { + this.updateButtonUI(!isActive); + } else { + alert('Erreur lors de la mise à jour'); + } + } catch (err) { + alert('Erreur de connexion'); + } + } + + updateButtonUI(nowActive) { + const btn = this.statusButtonTarget; + + if (nowActive) { + btn.textContent = "Désactiver"; + btn.classList.replace("btn-success", "btn-secondary"); + btn.dataset.active = "true"; + } else { + btn.textContent = "Réactiver"; + btn.classList.replace("btn-secondary", "btn-success"); + btn.dataset.active = "false"; + } + } } \ No newline at end of file diff --git a/assets/js/global.js b/assets/js/global.js index 62f5b75..80d5c79 100644 --- a/assets/js/global.js +++ b/assets/js/global.js @@ -22,7 +22,7 @@ export const TABULATOR_FR_LANG = { }; export function eyeIconLink(url) { - return ` + return ` `; } +export function pencilIcon() { + return ` + + + + + ` +} + +export function trashIcon(url) { + return ` + + + + + ` +} + + export function deactivateUserIcon() { return ` diff --git a/assets/styles/app.css b/assets/styles/app.css index bde65b2..c04f469 100644 --- a/assets/styles/app.css +++ b/assets/styles/app.css @@ -4,10 +4,14 @@ --primary-blue-dark : #094754; --black-font: #1D1E1C; --delete : #E42E31; + --delete-dark : #aa1618; --disable : #A3A3A3; - --check : #80F20E; + --check : #5cae09; + --check-dark: #3a6e05; --secondary : #cc664c; --secondary-dark : #a5543d; + --warning : #d2b200; + --warning-dark: #c4a600; } html { @@ -103,12 +107,41 @@ body { border: var(--primary-blue-light); } +.btn-success{ + background: var(--check); + color : #FFFFFF; + border: var(--check-dark); + border-radius: 1rem; +} +.btn-success:hover{ + background: var(--check-dark); + color : #FFFFFF; + border: var(--check); +} + +.btn-warning{ + background: var(--warning); + color : #FFFFFF; + border: var(--warning-dark); + border-radius: 1rem; +} +.btn-warning:hover{ + background: var(--warning-dark); + color : #FFFFFF; + border: var(--warning); +} + .btn-danger{ background: var(--delete); color : #FFFFFF; - border: var(--delete); + border: var(--delete-dark); border-radius: 1rem; } +.btn-danger:hover{ + background: var(--delete-dark); + color : #FFFFFF; + border: var(--delete); +} .color-primary{ color: var(--primary-blue-light) !important; @@ -117,6 +150,13 @@ body { color: var(--primary-blue-dark); } +.color-delete{ + color: var(--delete) !important; +} +.color-delete-dark{ + color: var(--delete-dark); +} + .btn-secondary{ background: var(--secondary); color : #FFFFFF; diff --git a/docs/Organization.md b/docs/Organization.md new file mode 100644 index 0000000..1c6243c --- /dev/null +++ b/docs/Organization.md @@ -0,0 +1,35 @@ +# Intro +Each organization are a collection of users and projects. +Users will be able to have multiple organizations and different roles in each of them. For example, a user can be an +admin in one organization and a member in another organization. + +Each organization will have a unique slug that will consist of 4 lowercase letters and this slug will be used for the +database name of each project contained in an organization. + +## Projects +Each project will have a unique name. Each project will be associated with an organization and will have a unique slug +that will come from the organization. The project will have a JSON field that will contain the different applications it has access to + +## Organization Management +The organization management will have different features, such as creating an organization, inviting users to an organization, +managing the roles of users in an organization(admin or not), and deleting an organization. + +### CRUD Operations +- **Create Organization**: Super Admin +- **Read Organization**: Super Admin, Admin and admin of the organization +- **Update Organization**: Super Admin, Admin +- **Delete Organization**: Super Admin + +### User Management +- **Invite User**: Super Admin, Admin and admin of the organization +- **Remove User**: Super Admin, Admin and admin of the organization +- **Change User Role**: Super Admin, Admin and admin of the organization +- **List Users**: Super Admin, Admin and admin of the organization +- **Accept Invitation**: User +- **Decline Invitation**: User + +### Project Management +- **Create Project**: Super Admin +- **Read Project**: Super Admin, Admin and admin of the organization +- **Update Project**: Super Admin +- **Delete Project**: Super Admin diff --git a/docs/Role_Hierarchy.md b/docs/Role_Hierarchy.md new file mode 100644 index 0000000..2dfb715 --- /dev/null +++ b/docs/Role_Hierarchy.md @@ -0,0 +1,34 @@ +# Intro +Roles will be split into two categories: **System Roles** and **Organizations Roles**. +System roles are global and apply to the entire system, while Organizations roles are specific to individual Organizations. + +## System Roles +System roles are global and apply to the entire system. They include: +- **System Super Admin**: Has full access to all system features and settings. Can manage users, projects, organizations and applications. (SI) +- **System Admin**: Has access to most system features and settings. Can manage users, organizations, applications authorizations by projects. (BE) +- **System User**: Has limited access to system features and settings. Can view projects and applications, can manage own information, and organization where they are admin. (Others) + +### System Super Admin +Get Access to the following with the following authorisations: +- **Users**: READ, CREATE, UPDATE, DELETE +- **Projects**: READ, CREATE, UPDATE, DELETE +- **Organizations**: READ, CREATE, UPDATE, DELETE +- **Applications**: READ, UPDATE + +### System Admin +Get Access to the following with the following authorisations: +- **Users**: READ, CREATE, UPDATE, DELETE +- **Organizations**: READ, UPDATE +- **Applications**: READ + +### System User +Get Access to the following with the following authorisations: +- **Users**: READ, UPDATE (own information only), READ (organization where they are admin), CREATE ( organization where they are admin), UPDATE (organization where they are admin), DELETE (organization where they are admin) +- **Projects**: READ ( of organization they are part of) +- **Organizations**: READ +- **Applications**: READ + +## Organizations Roles +Organizations roles are specific to individual Organizations. They include: +- **Organization Admin**: Has full access to all organization features and settings. Can manage users of the organizations. +- **Organization User**: Has limited access to organization features and settings. Can view projects and applications, can manage own information \ No newline at end of file diff --git a/migrations/Version20260210131727.php b/migrations/Version20260210131727.php new file mode 100644 index 0000000..b313cf2 --- /dev/null +++ b/migrations/Version20260210131727.php @@ -0,0 +1,36 @@ +addSql('ALTER TABLE users_organizations ADD role_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE users_organizations ADD CONSTRAINT FK_4B991472D60322AC FOREIGN KEY (role_id) REFERENCES roles (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('CREATE INDEX IDX_4B991472D60322AC ON users_organizations (role_id)'); + } + + 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 users_organizations DROP CONSTRAINT FK_4B991472D60322AC'); + $this->addSql('DROP INDEX IDX_4B991472D60322AC'); + $this->addSql('ALTER TABLE users_organizations DROP role_id'); + } +} diff --git a/migrations/Version20260211142643.php b/migrations/Version20260211142643.php new file mode 100644 index 0000000..584ce6c --- /dev/null +++ b/migrations/Version20260211142643.php @@ -0,0 +1,32 @@ +addSql('ALTER TABLE organizations ADD project_prefix VARCHAR(4) 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 organizations DROP project_prefix'); + } +} diff --git a/migrations/Version20260211145606.php b/migrations/Version20260211145606.php new file mode 100644 index 0000000..e916e0f --- /dev/null +++ b/migrations/Version20260211145606.php @@ -0,0 +1,37 @@ +addSql('CREATE TABLE project (id SERIAL NOT NULL, organization_id INT NOT NULL, name VARCHAR(255) NOT NULL, applications JSON DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, modified_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, is_active BOOLEAN NOT NULL, is_deleted BOOLEAN NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_2FB3D0EE32C8A3DE ON project (organization_id)'); + $this->addSql('COMMENT ON COLUMN project.created_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN project.modified_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE project ADD CONSTRAINT FK_2FB3D0EE32C8A3DE FOREIGN KEY (organization_id) REFERENCES organizations (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + 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 CONSTRAINT FK_2FB3D0EE32C8A3DE'); + $this->addSql('DROP TABLE project'); + } +} diff --git a/migrations/Version20260216092531.php b/migrations/Version20260216092531.php new file mode 100644 index 0000000..1082423 --- /dev/null +++ b/migrations/Version20260216092531.php @@ -0,0 +1,32 @@ +addSql('ALTER TABLE project ADD bdd_name 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 bdd_name'); + } +} diff --git a/src/Controller/ActionController.php b/src/Controller/ActionController.php index 0b02473..1222496 100644 --- a/src/Controller/ActionController.php +++ b/src/Controller/ActionController.php @@ -5,6 +5,7 @@ namespace App\Controller; use App\Entity\Actions; use App\Entity\Organizations; use App\Service\ActionService; +use App\Service\UserService; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; @@ -15,21 +16,24 @@ class ActionController extends AbstractController { public function __construct( private EntityManagerInterface $entityManager, - private ActionService $actionService + private ActionService $actionService, private readonly UserService $userService ) { } #[Route('/organization/{id}/activities-ajax', name: 'app_organization_activities_ajax', methods: ['GET'])] public function fetchActivitiesAjax(Organizations $organization): JsonResponse { - $this->denyAccessUnlessGranted('ROLE_ADMIN'); - $actions = $this->entityManager->getRepository(Actions::class)->findBy( - ['Organization' => $organization], - ['date' => 'DESC'], - 10 - ); - $formattedActivities = $this->actionService->formatActivities($actions); + $this->denyAccessUnlessGranted('ROLE_USER'); + if($this->userService->isAdminOfOrganization($organization) || $this->isGranted('ROLE_ADMIN')) { + $actions = $this->entityManager->getRepository(Actions::class)->findBy( + ['Organization' => $organization], + ['date' => 'DESC'], + 10 + ); + $formattedActivities = $this->actionService->formatActivities($actions); - return new JsonResponse($formattedActivities); + return new JsonResponse($formattedActivities); + } + return new JsonResponse(['error' => 'You are not authorized to access this page.'], 403); } } diff --git a/src/Controller/ApplicationController.php b/src/Controller/ApplicationController.php index 5f86963..f273c18 100644 --- a/src/Controller/ApplicationController.php +++ b/src/Controller/ApplicationController.php @@ -51,7 +51,7 @@ class ApplicationController extends AbstractController #[Route(path: '/edit/{id}', name: 'edit', methods: ['GET', 'POST'])] public function edit(int $id, Request $request): Response{ $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN'); - $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); + $actingUser = $this->getUser(); $application = $this->entityManager->getRepository(Apps::class)->find($id); if (!$application) { $this->loggerService->logEntityNotFound('Application', [ @@ -102,95 +102,11 @@ class ApplicationController extends AbstractController } - #[Route(path: '/authorize/{id}', name: 'authorize', methods: ['POST'])] - public function authorize(int $id, Request $request): Response - { - $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN'); - $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); - try{ - $application = $this->entityManager->getRepository(Apps::class)->find($id); - if (!$application) { - $this->loggerService->logEntityNotFound('Application', [ - 'applicationId' => $id, - 'message' => "Application not found for authorization." - ], $actingUser->getId()); - throw $this->createNotFoundException("L'application n'existe pas."); - } - $orgId = $request->get('organizationId'); - - $organization = $this->entityManager->getRepository(Organizations::Class)->find($orgId); - if (!$organization) { - $this->loggerService->logEntityNotFound('Organization', [ - 'Organization_id' => $orgId, - 'message' => "Organization not found for authorization." - ], $actingUser->getId()); - throw $this->createNotFoundException("L'Organization n'existe pas."); - } - $application->addOrganization($organization); - $this->loggerService->logApplicationInformation('Application Authorized', [ - 'applicationId' => $application->getId(), - 'applicationName' => $application->getName(), - 'organizationId' => $organization->getId(), - 'message' => "Application authorized for organization." - ], $actingUser->getId()); - $this->entityManager->persist($application); - $this->entityManager->flush(); - $this->actionService->createAction("Authorization d'accès", $actingUser, $organization, $application->getName()); - return new Response('', Response::HTTP_OK); - }catch (HttpExceptionInterface $e){ - throw $e; - } catch (\Exception $e){ - $this->loggerService->logError('Application Authorization Failed', [ - 'applicationId' => $id, - 'error' => $e->getMessage(), - 'message' => "Failed to authorize application.", - 'acting_user_id' => $actingUser->getId() - ]); - return new Response('Erreur lors de l\'autorisation de l\'application.', Response::HTTP_INTERNAL_SERVER_ERROR); - } - - - } - - #[Route(path: '/revoke/{id}', name: 'revoke', methods: ['POST'])] - public function revoke(int $id, Request $request) - { - $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN'); - $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); - $application = $this->entityManager->getRepository(Apps::class)->find($id); - if (!$application) { - $this->loggerService->logEntityNotFound('Application', [ - 'applicationId' => $id, - 'message' => "Application not found for authorization removal." - ], $actingUser->getId()); - throw $this->createNotFoundException("L'application n'existe pas."); - } - $orgId = $request->get('organizationId'); - $organization = $this->entityManager->getRepository(Organizations::Class)->find($orgId); - if (!$organization) { - $this->loggerService->logEntityNotFound('Organization', [ - 'Organization_id' => $orgId, - 'message' => "Organization not found for authorization removal." - ], $actingUser->getId()); - throw $this->createNotFoundException("L'Organization n'existe pas."); - } - $application->removeOrganization($organization); - $this->loggerService->logApplicationInformation('Application Authorized removed', [ - 'applicationId' => $application->getId(), - 'applicationName' => $application->getName(), - 'organizationId' => $organization->getId(), - 'message' => "Application authorized removed for organization." - ], $actingUser->getId()); - $this->actionService->createAction("Authorization retirer", $actingUser, $organization, $application->getName()); - - return new Response('', Response::HTTP_OK); - } - #[Route(path:'/user/{id}', name: 'user', methods: ['GET'])] public function getApplicationUsers(int $id): JSONResponse { $user = $this->userRepository->find($id); - $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); + $actingUser = $this->getUser(); if (!$user) { $this->loggerService->logEntityNotFound('User', ['message'=> 'User not found for application list'], $actingUser->getId()); return new JsonResponse(['error' => 'User not found'], Response::HTTP_NOT_FOUND); @@ -211,4 +127,20 @@ class ApplicationController extends AbstractController return new JsonResponse($data, Response::HTTP_OK); } + + #[Route(path: '/data/all', name: 'data_all', methods: ['GET'])] + public function getAllApplications(): JsonResponse + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $applications = $this->entityManager->getRepository(Apps::class)->findAll(); + $data = array_map(function($app) { + return [ + 'id' => $app->getId(), + 'name' => $app->getName(), + 'subDomain' => $app->getSubDomain(), + 'logoMiniUrl' => $this->assetsManager->getUrl($app->getLogoMiniUrl()), + ]; + }, $applications); + return new JsonResponse($data, Response::HTTP_OK); + } } diff --git a/src/Controller/IndexController.php b/src/Controller/IndexController.php index 5bfd14b..ae0e8b4 100644 --- a/src/Controller/IndexController.php +++ b/src/Controller/IndexController.php @@ -2,6 +2,7 @@ namespace App\Controller; +use App\Service\UserService; use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\SecurityBundle\Security; @@ -11,10 +12,15 @@ use Symfony\Component\Routing\Attribute\Route; final class IndexController extends AbstractController { + public function __construct(private readonly UserService $userService) + { + } + #[Route('/', name: 'app_index')] public function index(): Response { - if ($this->isGranted('ROLE_ADMIN')) { + + if ($this->isGranted('ROLE_ADMIN') || ($this->isGranted('ROLE_USER') && $this->userService->isAdminInAnyOrganization($this->getUser()))) { return $this->redirectToRoute('organization_index'); } diff --git a/src/Controller/MercureController.php b/src/Controller/MercureController.php index 096871b..93548ae 100644 --- a/src/Controller/MercureController.php +++ b/src/Controller/MercureController.php @@ -20,7 +20,7 @@ class MercureController extends AbstractController public function getMercureToken(Request $request): JsonResponse { $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); - $user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); + $user =$this->getUser(); $domain = $request->getSchemeAndHttpHost(); diff --git a/src/Controller/NotificationController.php b/src/Controller/NotificationController.php index 9195577..9d32d6c 100644 --- a/src/Controller/NotificationController.php +++ b/src/Controller/NotificationController.php @@ -29,7 +29,7 @@ class NotificationController extends AbstractController public function index(): JsonResponse { $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN'); - $user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); + $user =$this->getUser(); $notifications = $this->notificationRepository->findRecentByUser($user, 50); $unreadCount = $this->notificationRepository->countUnreadByUser($user); @@ -44,7 +44,7 @@ class NotificationController extends AbstractController public function unread(): JsonResponse { $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); - $user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); + $user =$this->getUser(); $notifications = $this->notificationRepository->findUnreadByUser($user); $unreadCount = count($notifications); @@ -59,7 +59,7 @@ class NotificationController extends AbstractController public function count(): JsonResponse { $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); - $user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); + $user =$this->getUser(); $unreadCount = $this->notificationRepository->countUnreadByUser($user); @@ -70,7 +70,7 @@ class NotificationController extends AbstractController public function markAsRead(int $id): JsonResponse { $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); - $user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); + $user =$this->getUser(); $notification = $this->notificationRepository->find($id); @@ -88,7 +88,7 @@ class NotificationController extends AbstractController public function markAllAsRead(): JsonResponse { $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); - $user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); + $user =$this->getUser(); $count = $this->notificationRepository->markAllAsReadForUser($user); @@ -99,7 +99,7 @@ class NotificationController extends AbstractController public function delete(int $id): JsonResponse { $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); - $user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); + $user =$this->getUser(); $notification = $this->notificationRepository->find($id); diff --git a/src/Controller/OrganizationController.php b/src/Controller/OrganizationController.php index ae7f6fc..89813ed 100644 --- a/src/Controller/OrganizationController.php +++ b/src/Controller/OrganizationController.php @@ -43,35 +43,42 @@ class OrganizationController extends AbstractController private readonly ActionService $actionService, private readonly UserOrganizationService $userOrganizationService, private readonly OrganizationsRepository $organizationsRepository, - private readonly AwsService $awsService, private readonly LoggerService $loggerService, private readonly LoggerInterface $logger) + private readonly LoggerService $loggerService) { } #[Route(path: '/', name: 'index', methods: ['GET'])] public function index(): Response { - $this->denyAccessUnlessGranted('ROLE_ADMIN'); - $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); - if($this->userService->hasAccessTo($actingUser, true)){ - $orgCount = $this->organizationsRepository->count(['isDeleted' => false]); - if(!$this->isGranted("ROLE_SUPER_ADMIN")){ - $userUO = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['users' => $actingUser, 'isActive' => true]); - $uoAdmin = 0; - foreach($userUO as $u){ - if($this->userService->isAdminOfOrganization($u->getOrganization())){ - $uoAdmin++; - } - } - if($uoAdmin === 1){ - return $this->redirectToRoute('organization_show', ['id' => $userUO[0]->getOrganization()->getId()]); - } - } - return $this->render('organization/index.html.twig', [ - 'hasOrganizations' => $orgCount > 0 - ]); + $this->denyAccessUnlessGranted('ROLE_USER'); + $actingUser = $this->getUser(); + + // 1. Super Admin Case: Just show the list + if ($this->isGranted("ROLE_ADMIN")) { + return $this->render('organization/index.html.twig', ['hasOrganizations' => true]); } - $this->loggerService->logAccessDenied($actingUser->getId()); - throw new AccessDeniedHttpException('Access denied'); + + // 2. Organization Admin Case: Get their specific orgs + $orgs = $this->userOrganizationService->getAdminOrganizationsForUser($actingUser); + + // If exactly one org, jump straight to it + if (count($orgs) === 1) { + return $this->redirectToRoute('organization_show', ['id' => $orgs[0]->getId()]); + } + + // If multiple orgs, show the list + if (count($orgs) > 1) { + return $this->render('organization/index.html.twig', ['hasOrganizations' => true]); + } + + // 3. Fallback: No access/No orgs found + $this->loggerService->logEntityNotFound('Organization', [ + 'user_id' => $actingUser->getUserIdentifier(), + 'message' => 'No admin organizations found' + ], $actingUser->getUserIdentifier()); + + $this->addFlash('danger', 'Erreur, aucune organisation trouvée.'); + return $this->redirectToRoute('app_index'); } @@ -79,7 +86,7 @@ class OrganizationController extends AbstractController public function new(Request $request): Response { $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN'); - $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); + $actingUser = $this->getUser(); if ($request->isMethod('POST')) { $organization = new Organizations(); $form = $this->createForm(OrganizationForm::class, $organization); @@ -90,16 +97,17 @@ class OrganizationController extends AbstractController $this->organizationsService->handleLogo($organization, $logoFile); } try { + $organization->setProjectPrefix($this->organizationsService->generateUniqueProjectPrefix()); $this->entityManager->persist($organization); $this->entityManager->flush(); - $this->loggerService->logOrganizationInformation($organization->getId(), $actingUser->getId(), "Organization Created"); - $this->loggerService->logSuperAdmin($actingUser->getId(), $actingUser->getId(), "Organization Created", $organization->getId()); + $this->loggerService->logOrganizationInformation($organization->getId(), $actingUser->getUserIdentifier(), "Organization Created"); + $this->loggerService->logSuperAdmin($actingUser->getUserIdentifier(), $actingUser->getUserIdentifier(), "Organization Created", $organization->getId()); $this->actionService->createAction("Create Organization", $actingUser, $organization, $organization->getName()); $this->addFlash('success', 'Organisation crée avec succès.'); return $this->redirectToRoute('organization_index'); } catch (Exception $e) { - $this->addFlash('error', 'Erreur lors de la création de l\'organization'); - $this->loggerService->logError('Error creating organization', ['acting_user_id' => $actingUser->getId(), 'error' => $e->getMessage()]); + $this->addFlash('danger', 'Erreur lors de la création de l\'organization'); + $this->loggerService->logError('Error creating organization', ['acting_user_id' => $actingUser->getUserIdentifier(), 'error' => $e->getMessage()]); } } return $this->render('organization/new.html.twig', [ @@ -117,40 +125,17 @@ class OrganizationController extends AbstractController public function edit(Request $request, $id): Response { $this->denyAccessUnlessGranted('ROLE_ADMIN'); - $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); + $actingUser = $this->getUser(); $organization = $this->organizationsRepository->find($id); if (!$organization) { $this->loggerService->logEntityNotFound('Organization', [ 'org_id' => $id, - 'message' => 'Organization not found for edit'], $actingUser->getId() + 'message' => 'Organization not found for edit'], $actingUser->getUserIdentifier() ); - $this->addFlash('error', 'Erreur, l\'organization est introuvable.'); + $this->addFlash('danger', 'Erreur, l\'organization est introuvable.'); return $this->redirectToRoute('organization_index'); } - if (!$this->isGranted("ROLE_SUPER_ADMIN")) { - //check if the user is admin of the organization - $uo = $this->entityManager->getRepository(UsersOrganizations::class)->findOneBy(['users' => $actingUser, 'organization' => $organization]); - if (!$uo) { - $this->loggerService->logEntityNotFound('UO link', [ - 'user_id' => $actingUser->getId(), - 'org_id' => $organization->getId(), - 'message' => 'UO link not found for edit organization' - ], $actingUser->getId()); - $this->addFlash('error', 'Erreur, accès refusé.'); - return $this->redirectToRoute('organization_index'); - } - $roleAdmin = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN']); - $uoaAdmin = $this->entityManager->getRepository(UserOrganizatonApp::class)->findOneBy(['userOrganization' => $uo, 'role' => $roleAdmin]); - if (!$uoaAdmin) { - $this->loggerService->logEntityNotFound('UOA link', [ - 'uo_id' => $uo->getId(), - 'role_id' => $roleAdmin->getId(), - 'message' => 'UOA link not found for edit organization, user is not admin of organization' - ], $actingUser->getId()); - $this->addFlash('error', 'Erreur, accès refusé.'); - return $this->redirectToRoute('organization_index'); - } - } + $form = $this->createForm(OrganizationForm::class, $organization); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { @@ -161,16 +146,16 @@ class OrganizationController extends AbstractController try { $this->entityManager->persist($organization); $this->entityManager->flush(); - $this->loggerService->logOrganizationInformation($organization->getId(), $actingUser->getId(), "Organization Edited"); + $this->loggerService->logOrganizationInformation($organization->getId(), $actingUser->getUserIdentifier(), "Organization Edited"); if ($this->isGranted("ROLE_SUPER_ADMIN")) { - $this->loggerService->logSuperAdmin($actingUser->getId(), $actingUser->getId(), "Organization Edited", $organization->getId()); + $this->loggerService->logSuperAdmin($actingUser->getUserIdentifier(), $actingUser->getUserIdentifier(), "Organization Edited", $organization->getId()); } $this->actionService->createAction("Edit Organization", $actingUser, $organization, $organization->getName()); $this->addFlash('success', 'Organisation modifiée avec succès.'); return $this->redirectToRoute('organization_index'); }catch (Exception $e) { - $this->addFlash('error', 'Erreur lors de la modification de l\'organization'); - $this->loggerService->logError('Error editing organization', ['acting_user_id' => $actingUser->getId(), 'error' => $e->getMessage()]); + $this->addFlash('danger', 'Erreur lors de la modification de l\'organization'); + $this->loggerService->logError('Error editing organization', ['acting_user_id' => $actingUser->getUserIdentifier(), 'error' => $e->getMessage()]); } } return $this->render('organization/edit.html.twig', [ @@ -182,24 +167,25 @@ class OrganizationController extends AbstractController #[Route(path: '/view/{id}', name: 'show', methods: ['GET'])] public function view($id): Response { - $this->denyAccessUnlessGranted('ROLE_ADMIN'); + $this->denyAccessUnlessGranted('ROLE_USER'); $organization = $this->organizationsRepository->find($id); - $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); + $actingUser = $this->getUser(); if (!$organization) { $this->loggerService->logEntityNotFound('Organization', [ 'org_id' => $id, 'message' => 'Organization not found for view' - ], $actingUser->getId()); - $this->addFlash('error', 'Erreur, l\'organization est introuvable.'); + ], $actingUser->getUserIdentifier()); + $this->addFlash('danger', 'Erreur, l\'organization est introuvable.'); return $this->redirectToRoute('organization_index'); } //check if the user is admin of the organization - if (!$this->userService->isAdminOfOrganization($organization) && !$this->isGranted("ROLE_SUPER_ADMIN")) { - $this->loggerService->logAccessDenied($actingUser->getId()); - $this->addFlash('error', 'Erreur, accès refusé.'); + if (!$this->userService->isAdminOfOrganization($organization) && !$this->isGranted("ROLE_ADMIN")) { + $this->loggerService->logAccessDenied($actingUser->getUserIdentifier()); + $this->addFlash('danger', 'Erreur, accès refusé.'); throw new AccessDeniedHttpException('Access denied'); } + //TODO: add project to the response $allApps = $this->entityManager->getRepository(Apps::class)->findAll(); // appsAll $orgApps = $organization->getApps()->toArray(); // apps @@ -219,15 +205,15 @@ class OrganizationController extends AbstractController #[Route(path: '/delete/{id}', name: 'delete', methods: ['POST'])] public function delete($id): Response { - $this->denyAccessUnlessGranted("ROLE_ADMIN"); - $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); + $this->denyAccessUnlessGranted("ROLE_SUPER_ADMIN"); + $actingUser = $this->getUser(); $organization = $this->organizationsRepository->find($id); if (!$organization) { $this->loggerService->logEntityNotFound('Organization', [ 'org_id' => $id, 'message' => 'Organization not found for delete' - ], $actingUser->getId()); - $this->addFlash('error', 'Erreur, l\'organization est introuvable.'); + ], $actingUser->getUserIdentifier()); + $this->addFlash('danger', 'Erreur, l\'organization est introuvable.'); throw $this->createNotFoundException(self::NOT_FOUND); } try { @@ -240,14 +226,14 @@ class OrganizationController extends AbstractController $this->entityManager->persist($organization); $this->actionService->createAction("Delete Organization", $actingUser, $organization, $organization->getName()); $this->entityManager->flush(); - $this->loggerService->logOrganizationInformation($organization->getId(), $actingUser->getId(),'Organization Deleted'); + $this->loggerService->logOrganizationInformation($organization->getId(), $actingUser->getUserIdentifier(),'Organization Deleted'); if ($this->isGranted("ROLE_SUPER_ADMIN")) { - $this->loggerService->logSuperAdmin($actingUser->getId(), $actingUser->getId(),'Organization Deleted', $organization->getId()); + $this->loggerService->logSuperAdmin($actingUser->getUserIdentifier(), $actingUser->getUserIdentifier(),'Organization Deleted', $organization->getId()); } $this->addFlash('success', 'Organisation supprimée avec succès.'); }catch (\Exception $e){ - $this->loggerService->logError($actingUser->getId(), ['message' => 'Error deleting organization: '.$e->getMessage()]); - $this->addFlash('error', 'Erreur lors de la suppression de l\'organization.'); + $this->loggerService->logError($actingUser->getUserIdentifier(), ['message' => 'Error deleting organization: '.$e->getMessage()]); + $this->addFlash('danger', 'Erreur lors de la suppression de l\'organization.'); } return $this->redirectToRoute('organization_index'); @@ -257,22 +243,21 @@ class OrganizationController extends AbstractController public function deactivate($id): Response { $this->denyAccessUnlessGranted("ROLE_SUPER_ADMIN"); - $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); + $actingUser = $this->getUser(); $organization = $this->organizationsRepository->find($id); if (!$organization) { $this->loggerService->logEntityNotFound('Organization', [ 'org_id' => $id, 'message' => 'Organization not found for deactivate' - ], $actingUser->getId()); - $this->addFlash('error', 'Erreur, l\'organization est introuvable.'); + ], $actingUser->getUserIdentifier()); + $this->addFlash('danger', 'Erreur, l\'organization est introuvable.'); throw $this->createNotFoundException(self::NOT_FOUND); } $organization->setIsActive(false); -// $this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, null, $organization); $this->entityManager->persist($organization); $this->actionService->createAction("Deactivate Organization", $actingUser, $organization, $organization->getName()); - $this->loggerService->logSuperAdmin($actingUser->getId(), $actingUser->getId(),'Organization deactivated', $organization->getId()); + $this->loggerService->logSuperAdmin($actingUser->getUserIdentifier(), $actingUser->getUserIdentifier(),'Organization deactivated', $organization->getId()); $this->addFlash('success', 'Organisation désactivé avec succès.'); return $this->redirectToRoute('organization_index'); } @@ -281,20 +266,20 @@ class OrganizationController extends AbstractController public function activate($id): Response { $this->denyAccessUnlessGranted("ROLE_SUPER_ADMIN"); - $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); + $actingUser = $this->getUser(); $organization = $this->organizationsRepository->find($id); if (!$organization) { $this->loggerService->logEntityNotFound('Organization', [ 'org_id' => $id, 'message' => 'Organization not found for activate' - ], $actingUser->getId()); - $this->addFlash('error', 'Erreur, l\'organization est introuvable.'); + ], $actingUser->getUserIdentifier()); + $this->addFlash('danger', 'Erreur, l\'organization est introuvable.'); throw $this->createNotFoundException(self::NOT_FOUND); } $organization->setIsActive(true); $this->entityManager->persist($organization); - $this->loggerService->logOrganizationInformation($organization->getId(), $actingUser->getId(),'Organization Activated'); - $this->loggerService->logSuperAdmin($actingUser->getId(), $actingUser->getId(),'Organization Activated', $organization->getId()); + $this->loggerService->logOrganizationInformation($organization->getId(), $actingUser->getUserIdentifier(),'Organization Activated'); + $this->loggerService->logSuperAdmin($actingUser->getUserIdentifier(), $actingUser->getUserIdentifier(),'Organization Activated', $organization->getId()); $this->actionService->createAction("Activate Organization", $actingUser, $organization, $organization->getName()); $this->addFlash('success', 'Organisation activée avec succès.'); return $this->redirectToRoute('organization_index'); @@ -304,56 +289,23 @@ class OrganizationController extends AbstractController #[Route(path: '/data', name: 'data', methods: ['GET'])] public function data(Request $request): JsonResponse { - $this->denyAccessUnlessGranted('ROLE_ADMIN'); - - - $page = max(1, (int)$request->query->get('page', 1)); - $size = max(1, (int)$request->query->get('size', 10)); + $this->denyAccessUnlessGranted('ROLE_USER'); + $page = max(1, $request->query->getInt('page', 1)); + $size = max(1, $request->query->getInt('size', 10)); $filters = $request->query->all('filter'); + // Fetch paginated results + $paginator = $this->organizationsRepository->findAdmissibleOrganizations( + $this->getUser(), + $this->isGranted('ROLE_ADMIN'), // Super Admin check + $page, + $size, + $filters + ); - $qb = $this->organizationsRepository->createQueryBuilder('o') - ->where('o.isDeleted = :del')->setParameter('del', false); + $total = count($paginator); - if (!empty($filters['name'])) { - $qb->andWhere('o.name LIKE :name') - ->setParameter('name', '%' . $filters['name'] . '%'); - } - if (!empty($filters['email'])) { - $qb->andWhere('o.email LIKE :email') - ->setParameter('email', '%' . $filters['email'] . '%'); - } - if (!$this->isGranted('ROLE_SUPER_ADMIN')) { - $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); - $uo = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['users' => $actingUser]); - - $allowedOrgIds = []; - foreach ($uo as $item) { - if ($this->userService->isAdminOfOrganization($item->getOrganization())) { - $allowedOrgIds[] = $item->getOrganization()->getId(); - } - } - - // If user has no organizations, ensure query returns nothing (or handle typically) - if (empty($allowedOrgIds)) { - $qb->andWhere('1 = 0'); // Force empty result - } else { - $qb->andWhere('o.id IN (:orgIds)') - ->setParameter('orgIds', $allowedOrgIds); - } - } - - - // Count total - $countQb = clone $qb; - $total = (int)$countQb->select('COUNT(o.id)')->getQuery()->getSingleScalarResult(); - - // Pagination - $offset = ($page - 1) * $size; - $rows = $qb->setFirstResult($offset)->setMaxResults($size)->getQuery()->getResult(); - - // Map to array $data = array_map(function (Organizations $org) { return [ 'id' => $org->getId(), @@ -363,17 +315,12 @@ class OrganizationController extends AbstractController 'active' => $org->isActive(), 'showUrl' => $this->generateUrl('organization_show', ['id' => $org->getId()]), ]; - }, $rows); - - $lastPage = (int)ceil($total / $size); + }, iterator_to_array($paginator)); return $this->json([ 'data' => $data, - 'last_page' => $lastPage, - 'total' => $total, // optional, useful for debugging + 'last_page' => (int)ceil($total / $size), + 'total' => $total, ]); } - - - } diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php new file mode 100644 index 0000000..f6511b0 --- /dev/null +++ b/src/Controller/ProjectController.php @@ -0,0 +1,162 @@ +render('project/index.html.twig', [ + 'controller_name' => 'ProjectController', + ]); + } + + #[Route('/new/ajax', name: '_new', methods: ['POST'])] + 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); + } + $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); + } + if(!$this->projectService->isApplicationArrayValid($data['applications'])) { + return new JsonResponse(['error' => 'Invalid applications array'], Response::HTTP_BAD_REQUEST); + } + $project = new Project(); + $project->setName($data['name']); + $project->setBddName($sanitizedDbName); + $project->setOrganization($org); + $project->setApplications($data['applications']); + $this->entityManager->persist($project); + $this->entityManager->flush(); + return new JsonResponse(['message' => 'Project created successfully'], Response::HTTP_CREATED); + } + + #[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']]); + if(!$org) { + return new JsonResponse(['error' => 'Organization not found'], Response::HTTP_NOT_FOUND); + } + $project = $this->projectRepository->findOneBy(['id' => $id]); + if(!$project) { + return new JsonResponse(['error' => 'Project not found'], Response::HTTP_NOT_FOUND); + } + $project->setApplications($data['applications']); + $project->setModifiedAt(new \DateTimeImmutable()); + $this->entityManager->persist($project); + $this->entityManager->flush(); + return new JsonResponse(['message' => 'Project updated successfully'], Response::HTTP_OK); + } + + #[Route('/organization/data', name: '_organization_data', methods: ['GET'])] + public function organizationData(Request $request, Packages $assetPackage): JsonResponse { + $this->denyAccessUnlessGranted('ROLE_USER'); + + $page = $request->query->getInt('page', 1); + $size = $request->query->getInt('size', 15); + $filters = $request->query->all('filter'); + $orgId = $request->query->get('orgId'); + + $org = $this->organizationsRepository->findOneBy(['id' => $orgId, 'isDeleted' => false]); + if(!$org) { + return new JsonResponse(['error' => 'Organization not found'], Response::HTTP_NOT_FOUND); + } + + $paginator = $this->projectRepository->findProjectByOrganization($orgId, $page, $size, $filters); + $total = count($paginator); + + $data = array_map(function (Project $project) use ($assetPackage) { + // Map ONLY the applications linked to THIS specific project + $projectApps = array_map(function($appId) use ($assetPackage) { + // Note: If $project->getApplications() returns IDs, we need to find the entities. + // If your Project entity has a ManyToMany relationship, use $project->getApps() instead. + $appEntity = $this->appsRepository->find($appId); + return $appEntity ? [ + 'id' => $appEntity->getId(), + 'name' => $appEntity->getName(), + 'logoMiniUrl' => $assetPackage->getUrl($appEntity->getLogoMiniUrl()), + ] : null; + }, $project->getApplications() ?? []); + + return [ + 'id' => $project->getId(), + 'name' => ucfirst($project->getName()), + 'applications' => array_filter($projectApps), // Remove nulls + 'bddName' => $project->getBddName(), + 'isActive' => $project->isActive(), + ]; + }, iterator_to_array($paginator)); + + return $this->json([ + 'data' => $data, + 'total' => $total, + 'last_page' => (int)ceil($total / $size), + ]); + } + + #[Route(path: '/data/{id}', name: '_project_data', methods: ['GET'])] + public function projectData(Request $request, int $id): JsonResponse{ + $this->denyAccessUnlessGranted('ROLE_USER'); + $project = $this->projectRepository->findOneBy(['id' => $id]); + if(!$project) { + return new JsonResponse(['error' => 'Project not found'], Response::HTTP_NOT_FOUND); + } + return new JsonResponse([ + 'id' => $project->getId(), + 'name' => ucfirst($project->getName()), + 'applications' => $project->getApplications(), + ]); + } + + #[Route(path: '/delete/{id}/ajax', name: '_delete', methods: ['POST'])] + public function delete(int $id): JsonResponse { + $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN'); + $project = $this->projectRepository->findOneBy(['id' => $id]); + if(!$project) { + return new JsonResponse(['error' => 'Project not found'], Response::HTTP_NOT_FOUND); + } + $project->setIsDeleted(true); + $this->entityManager->persist($project); + $this->entityManager->flush(); + return new JsonResponse(['message' => 'Project deleted successfully'], Response::HTTP_OK); + } +} diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 9434546..0619640 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -61,7 +61,16 @@ class UserController extends AbstractController ) { } - +//TODO: Move mailing/notification logic to event listeners/subscribers for better separation of concerns and to avoid bloating the controller with non-controller logic. Keep in mind the potential for circular dependencies and design accordingly (e.g. using interfaces or decoupled events). + #[Route(path: '/', name: 'index', methods: ['GET'])] + public function index(): Response + { + $this->denyAccessUnlessGranted('ROLE_ADMIN'); + $totalUsers = $this->userRepository->count(['isDeleted' => false, 'isActive' => true]); + return $this->render('user/index.html.twig', [ + 'users' => $totalUsers + ]); + } #[Route('/view/{id}', name: 'show', methods: ['GET'])] public function view(int $id, Request $request): Response @@ -70,128 +79,42 @@ class UserController extends AbstractController $this->denyAccessUnlessGranted('ROLE_USER'); // Utilisateur courant (acting user) via UserService - $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); - - // Vérification des droits d'accès supplémentaires - + $actingUser = $this->getUser(); // Chargement de l'utilisateur cible à afficher $user = $this->userRepository->find($id); if (!$user) { - $this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getId()); - $this->addFlash('error', "L'utilisateur demandé n'existe pas."); + $this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getUserIdentifier()); + $this->addFlash('danger', "L'utilisateur demandé n'existe pas."); throw $this->createNotFoundException(self::NOT_FOUND); } - if (!$this->userService->hasAccessTo($user)) { - $this->loggerService->logAccessDenied($actingUser->getId()); - $this->addFlash('error', "L'utilisateur demandé n'existe pas."); + //if hasAccessTo is false, turn to true and deny access + if (!$this->userService->isAdminOfUser($user) && !$this->isGranted('ROLE_ADMIN')) { + $this->loggerService->logAccessDenied($actingUser->getUserIdentifier()); + $this->addFlash('danger', "Vous n'avez pas accès à cette information."); throw new AccessDeniedHttpException (self::ACCESS_DENIED); } try { // Paramètre optionnel de contexte organisationnel $orgId = $request->query->get('organizationId'); - - // Liste de toutes les applications (pour créer des groupes même si vides) - $apps = $this->appsRepository->findAll(); - - // Initialisations pour la résolution des UsersOrganizations (UO) - $singleUo = null; - $uoActive = null; - - // get uo or uoS based on orgId if ($orgId) { - // Contexte organisation précis : récupérer l'organisation et les liens UO - $organization = $this->organizationRepository->findBy(['id' => $orgId]); - $uoList = $this->uoRepository->findBy([ - 'users' => $user, - 'organization' => $organization, - 'isActive' => true, - ]); - - if (!$uoList) { - $this->loggerService->logEntityNotFound('UsersOrganization', [ - 'user_id' => $user->getId(), - 'organization_id' => $orgId], - $actingUser->getId()); - $this->addFlash('error', "L'utilisateur n'est pas actif dans cette organisation."); - throw $this->createNotFoundException(self::NOT_FOUND); - } - - // Si contexte org donné, on retient la première UO (singleUo) - $singleUo = $uoList[0]; - $data["singleUo"] = $singleUo; - $uoActive = $singleUo->isActive(); +// TODO: afficher les projets de l'organisation } else { - // Pas de contexte org : récupérer toutes les UO actives de l'utilisateur - $uoList = $this->uoRepository->findBy([ - 'users' => $user, - 'isActive' => true, - ]); - if (!$uoList) { - $data['rolesArray'] = $this->userService->getRolesArrayForUser($actingUser, true); - return $this->render('user/show.html.twig', [ - 'user' => $user, - 'organizationId' => $orgId ?? null, - 'uoActive' => $uoActive ?? null, - 'apps' => $apps ?? [], - 'data' => $data ?? [], - 'canEdit' => false, - ]); - } + // Afficher tous les projets de l'utilisateur } - // Charger les liens UserOrganizationApp (UOA) actifs pour les UO trouvées - // Load user-organization-app roles (can be empty) - $uoa = $this->entityManager - ->getRepository(UserOrganizatonApp::class) - ->findBy([ - 'userOrganization' => $uoList, - 'isActive' => true, - ]); - // Group UOA by app and ensure every app has a group - $data['uoas'] = $this->userOrganizationAppService - ->groupUserOrganizationAppsByApplication( - $uoa, - $apps, - $singleUo ? $singleUo->getId() : null - ); - - //Build roles based on user permissions. - //Admin can't see or edit a super admin user - if ($this->isGranted('ROLE_SUPER_ADMIN')) { - $data['rolesArray'] = $this->rolesRepository->findAll(); - } elseif (!$orgId) { - $data['rolesArray'] = $this->userService->getRolesArrayForUser($actingUser, true); - } else { - $data['rolesArray'] = $this->userService->getRolesArrayForUser($actingUser); - } - - // ------------------------------------------------------------------- - - // Calcul du flag de modification : utilisateur admin ET exactement 1 UO - if (empty($uoa) || !$orgId){ - $canEdit = false; - }else{ - $canEdit = $this->userService->canEditRolesCheck($actingUser, $user, $this->isGranted('ROLE_ADMIN'), $singleUo, $organization); - } - - } catch (\Exception $e) { $this->loggerService->logError('error while loading user information', [ 'target_user_id' => $id, - 'acting_user_id' => $actingUser->getId(), + 'acting_user_id' => $actingUser->getUserIdentifier(), 'error' => $e->getMessage(), ]); - $this->addFlash('error', 'Une erreur est survenue lors du chargement des informations utilisateur.'); + $this->addFlash('danger', 'Une erreur est survenue lors du chargement des informations utilisateur.'); $referer = $request->headers->get('referer'); return $this->redirect($referer ?? $this->generateUrl('app_index')); } return $this->render('user/show.html.twig', [ 'user' => $user, 'organizationId' => $orgId ?? null, - 'uoActive' => $uoActive ?? null, - 'apps' => $apps ?? [], - 'data' => $data ?? [], - 'canEdit' => $canEdit ?? false, ]); } @@ -199,15 +122,15 @@ class UserController extends AbstractController public function edit(int $id, Request $request): Response { $this->denyAccessUnlessGranted('ROLE_USER'); - $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); + $actingUser = $this->getUser(); $user = $this->userRepository->find($id); if (!$user) { - $this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getId()); - $this->addFlash('error', "L'utilisateur demandé n'existe pas."); + $this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getUserIdentifier()); + $this->addFlash('danger', "L'utilisateur demandé n'existe pas."); throw $this->createNotFoundException(self::NOT_FOUND); } try { - if ($this->userService->hasAccessTo($user)) { + if ($this->userService->isAdminOfUser($user)) { $form = $this->createForm(UserForm::class, $user); $form->handleRequest($request); @@ -221,31 +144,31 @@ class UserController extends AbstractController $this->entityManager->flush(); //log and action - $this->loggerService->logUserAction($user->getId(), $actingUser->getId(), 'User information edited'); + $this->loggerService->logUserAction($user->getId(), $actingUser->getUserIdentifier(), 'User information edited'); $orgId = $request->get('organizationId'); if ($orgId) { $org = $this->organizationRepository->find($orgId); if ($org) { $this->actionService->createAction("Edit user information", $actingUser, $org, $user->getUserIdentifier()); - $this->loggerService->logUserAction($user->getId(), $actingUser->getId(), 'User information edited'); + $this->loggerService->logUserAction($user->getId(), $actingUser->getUserIdentifier(), 'User information edited'); if ($this->isGranted('ROLE_SUPER_ADMIN')) { $this->loggerService->logSuperAdmin( $user->getId(), - $actingUser->getId(), + $actingUser->getUserIdentifier(), "Super Admin accessed user edit page", ); } $this->addFlash('success', 'Information modifié avec success.'); return $this->redirectToRoute('user_show', ['id' => $user->getId(), 'organizationId' => $orgId]); } - $this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getId()); - $this->addFlash('error', "L'organisation n'existe pas."); + $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_SUPER_ADMIN')) { $this->loggerService->logSuperAdmin( $user->getId(), - $actingUser->getId(), + $actingUser->getUserIdentifier(), "Super Admin accessed user edit page", ); } @@ -260,11 +183,11 @@ class UserController extends AbstractController 'organizationId' => $request->get('organizationId') ]); } - $this->loggerService->logAccessDenied($actingUser->getId()); - $this->addFlash('error', "Accès non autorisé."); + $this->loggerService->logAccessDenied($actingUser->getUserIdentifier()); + $this->addFlash('danger', "Accès non autorisé."); throw $this->createAccessDeniedException(self::ACCESS_DENIED); } catch (\Exception $e) { - $this->addFlash('error', 'Une erreur est survenue lors de la modification des informations utilisateur.'); + $this->addFlash('danger', 'Une erreur est survenue lors de la modification des informations utilisateur.'); $this->errorLogger->critical($e->getMessage()); } // Default deny access. shouldn't reach here normally. @@ -275,52 +198,47 @@ class UserController extends AbstractController #[Route('/new', name: 'new', methods: ['GET', 'POST'])] public function new(Request $request): Response { - $this->denyAccessUnlessGranted('ROLE_ADMIN'); + $this->denyAccessUnlessGranted('ROLE_USER'); try { - $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); - if (!$this->userService->hasAccessTo($actingUser)) { - $this->loggerService->logAccessDenied($actingUser->getId()); - throw $this->createAccessDeniedException(self::ACCESS_DENIED); - } + $actingUser =$this->getUser(); $user = new User(); $form = $this->createForm(UserForm::class, $user); $form->handleRequest($request); - $orgId = $request->get('organizationId'); + $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->getId()); - $this->addFlash('error', "L'organisation n'existe pas."); + $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->isGranted('ROLE_SUPER_ADMIN')) { - $this->loggerService->logAccessDenied($actingUser->getId()); - $this->addFlash('error', "Accès non autorisé."); + 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); } - } elseif ($this->isGranted('ROLE_ADMIN')) { - $this->loggerService->logAccessDenied($actingUser->getId()); - $this->addFlash('error', "Accès non autorisé."); + } 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 + has organization context + // Case : User exists -> link him to given organization if not already linked, else error message if ($existingUser && $org) { $this->userService->addExistingUserToOrganization( $existingUser, $org, - $actingUser, ); - if ($this->isGranted('ROLE_SUPER_ADMIN')) { + if ($this->isGranted('ROLE_ADMIN')) { $this->loggerService->logSuperAdmin( $existingUser->getId(), - $actingUser->getId(), + $actingUser->getUserIdentifier(), "Super Admin linked user to organization", $org->getId(), ); @@ -329,61 +247,17 @@ class UserController extends AbstractController return $this->redirectToRoute('organization_show', ['id' => $orgId]); } - //Code semi-mort : On ne peut plus créer un utilisateur sans organisation - // Case : User exists but NO organization context -> throw error on email field. - -// if ($existingUser) { -// $this->loggerService->logError('Attempt to create user with existing email without organization', [ -// 'target_user_email' => $user->getid(), -// 'acting_user_id' => $actingUser->getId(), -// ]); -// -// $form->get('email')->addError( -// new \Symfony\Component\Form\FormError( -// 'This email is already in use. Add the user to an organization instead.' -// ) -// ); -// -// return $this->render('user/new.html.twig', [ -// 'user' => $user, -// 'form' => $form->createView(), -// 'organizationId' => $orgId, -// ]); -// } + // Case : user doesn't already exist $picture = $form->get('pictureUrl')->getData(); $this->userService->createNewUser($user, $actingUser, $picture); - if ($this->isGranted('ROLE_SUPER_ADMIN')) { - $this->loggerService->logSuperAdmin( - $user->getId(), - $actingUser->getId(), - "Super Admin created new user", - - ); - } - - // Case : Organization provided and user doesn't already exist - if ($orgId) { - $this->userService->linkUserToOrganization( - $user, - $org, - $actingUser, - ); - - if ($this->isGranted('ROLE_SUPER_ADMIN')) { - $this->loggerService->logSuperAdmin( - $user->getId(), - $actingUser->getId(), - "Super Admin linked user to organization during creation", - $org->getId() - ); - } - $this->addFlash('success', 'Nouvel utilisateur créé et ajouté à l\'organisation avec succès. '); - return $this->redirectToRoute('organization_show', ['id' => $orgId]); - } - $this->addFlash('success', 'Nouvel utilisateur créé avec succès. '); - return $this->redirectToRoute('user_index'); + $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', [ @@ -396,38 +270,32 @@ class UserController extends AbstractController $this->errorLogger->critical($e->getMessage()); if ($orgId) { - $this->addFlash('error', 'Une erreur est survenue lors de la création de l\'utilisateur pour l\'organisation .'); + $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('error', 'Une erreur est survenue lors de la création de l\'utilisateur.'); + $this->addFlash('danger', 'Une erreur est survenue lors de la création de l\'utilisateur.'); return $this->redirectToRoute('user_index'); } } - - #[Route('/activeStatus/{id}', name: 'active_status', methods: ['GET', 'POST'])] + /** + * Endpoint to activate/deactivate a user (soft delete) + * If deactivating, also deactivate all org links and revoke tokens + */ + #[Route('/activeStatus/{id}', name: 'active_status', methods: ['POST'])] public function activeStatus(int $id, Request $request): JsonResponse { $this->denyAccessUnlessGranted('ROLE_ADMIN'); - $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); - $status = $request->get('status'); + $actingUser =$this->getUser(); + $status = $request->request->get('status'); try { - // Access control - if (!$this->userService->hasAccessTo($actingUser, true)) { - $this->loggerService->logAccessDenied($actingUser->getId()); - throw $this->createAccessDeniedException(self::ACCESS_DENIED); - } - - // Load target user $user = $this->userRepository->find($id); if (!$user) { - $this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getId()); - + $this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getUserIdentifier()); throw $this->createNotFoundException(self::NOT_FOUND); } - // Deactivate if ($status === 'deactivate') { $user->setIsActive(false); @@ -440,12 +308,12 @@ class UserController extends AbstractController $user->setModifiedAt(new \DateTimeImmutable('now')); $this->entityManager->persist($user); $this->entityManager->flush(); - $this->loggerService->logUserAction($user->getId(), $actingUser->getId(), 'User deactivated'); + $this->loggerService->logUserAction($user->getId(), $actingUser->getUserIdentifier(), 'User deactivated'); if ($this->isGranted('ROLE_SUPER_ADMIN')) { $this->loggerService->logSuperAdmin( $user->getId(), - $actingUser->getId(), + $actingUser->getUserIdentifier(), 'Super admin deactivated user' ); } @@ -461,13 +329,13 @@ class UserController extends AbstractController $user->setModifiedAt(new \DateTimeImmutable('now')); $this->entityManager->persist($user); $this->entityManager->flush(); - $this->loggerService->logUserAction($user->getId(), $actingUser->getId(), 'User activated'); + $this->loggerService->logUserAction($user->getId(), $actingUser->getUserIdentifier(), 'User activated'); if ($this->isGranted('ROLE_SUPER_ADMIN')) { $this->loggerService->logSuperAdmin( $user->getId(), - $actingUser->getId(), + $actingUser->getUserIdentifier(), 'Super admin activated user' ); } @@ -498,32 +366,33 @@ class UserController extends AbstractController } } - #[Route('/organization/activateStatus/{id}', name: 'activate_organization', methods: ['GET', 'POST'])] + #[Route('/organization/activateStatus/{id}', name: 'activate_organization', methods: ['POST'])] public function activateStatusOrganization(int $id, Request $request): JsonResponse { - $this->denyAccessUnlessGranted('ROLE_ADMIN'); - $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); + $this->denyAccessUnlessGranted('ROLE_USER'); + $actingUser = $this->getUser(); try { - if ($this->userService->hasAccessTo($actingUser, true)) { - $orgId = $request->get('organizationId'); + $user = $this->userRepository->find($id); + if (!$user) { + $this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getUserIdentifier()); + throw $this->createNotFoundException(self::NOT_FOUND); + } + if ($this->userService->isAdminOfUser($user)) { + $orgId = $request->request->get('organizationId'); $org = $this->organizationRepository->find($orgId); if (!$org) { - $this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getId()); - throw $this->createNotFoundException(self::NOT_FOUND); - } - $user = $this->userRepository->find($id); - if (!$user) { - $this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getId()); + $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' => $user->getId(), - 'organization_id' => $org->getId()], $actingUser->getId()); + 'organization_id' => $org->getId()], $actingUser->getUserIdentifier()); throw $this->createNotFoundException(self::NOT_FOUND); } - $status = $request->get('status'); + $status = $request->request->get('status'); if ($status === 'deactivate') { $uo->setIsActive(false); $this->userOrganizationAppService->deactivateAllUserOrganizationsAppLinks($uo); @@ -532,7 +401,7 @@ class UserController extends AbstractController $data = ['user' => $user, 'organization' => $org]; $this->organizationsService->notifyOrganizationAdmins($data, "USER_DEACTIVATED"); - $this->loggerService->logOrganizationInformation($org->getId(), $actingUser->getId(), "UO link deactivated with uo id : {$uo->getId()}"); + $this->loggerService->logOrganizationInformation($org->getId(), $actingUser->getUserIdentifier(), "UO link deactivated with uo id : {$uo->getId()}"); $this->actionService->createAction("Deactivate user in organization", $actingUser, $org, $org->getName() . " for user " . $user->getUserIdentifier()); return new JsonResponse(['status' => 'deactivated'], Response::HTTP_OK); } @@ -540,7 +409,7 @@ class UserController extends AbstractController $uo->setIsActive(true); $this->entityManager->persist($uo); $this->entityManager->flush(); - $this->loggerService->logOrganizationInformation($orgId, $actingUser->getId(), "UO link activated with uo id : {$uo->getId()}"); + $this->loggerService->logOrganizationInformation($orgId, $actingUser->getUserIdentifier(), "UO link activated with uo id : {$uo->getId()}"); $this->actionService->createAction("Activate user in organization", $actingUser, $org, $org->getName() . " for user " . $user->getUserIdentifier()); $data = ['user' => $user, 'organization' => $org]; @@ -566,14 +435,14 @@ class UserController extends AbstractController { $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN'); - $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); + $actingUser = $this->getUser(); try { $user = $this->userRepository->find($id); if (!$user) { // Security/audit log for missing user - $this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getId()); - $this->addFlash('error', "L'utilisateur demandé n'existe pas."); + $this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getUserIdentifier()); + $this->addFlash('danger', "L'utilisateur demandé n'existe pas."); throw $this->createNotFoundException(self::NOT_FOUND); } @@ -585,7 +454,7 @@ class UserController extends AbstractController $user->setModifiedAt(new \DateTimeImmutable('now')); // Deactivate all org links $this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, $user); - $this->loggerService->logOrganizationInformation($user->getId(), $actingUser->getId(), 'All user organization links deactivated'); + $this->loggerService->logOrganizationInformation($user->getId(), $actingUser->getUserIdentifier(), 'All user organization links deactivated'); // Revoke tokens if connected if ($this->userService->isUserConnected($user->getUserIdentifier())) { @@ -595,13 +464,13 @@ class UserController extends AbstractController $this->entityManager->flush(); // User management log - $this->loggerService->logUserAction($user->getId(), $actingUser->getId(), 'User deleted'); + $this->loggerService->logUserAction($user->getId(), $actingUser->getUserIdentifier(), 'User deleted'); // Super admin log (standardized style) if ($this->isGranted('ROLE_SUPER_ADMIN')) { $this->loggerService->logSuperAdmin( $user->getId(), - $actingUser->getId(), + $actingUser->getUserIdentifier(), 'Super admin deleted user' ); } @@ -636,68 +505,11 @@ class UserController extends AbstractController if ($e instanceof NotFoundHttpException) { throw $e; // keep 404 semantics } - $this->addFlash('error', 'Erreur lors de la suppression de l\'utilisateur\.'); + $this->addFlash('danger', 'Erreur lors de la suppression de l\'utilisateur\.'); return $this->redirectToRoute('user_index'); } } - #[Route(path: '/application/roles/{id}', name: 'application_role', methods: ['GET', 'POST'])] - public function applicationRole(int $id, Request $request): Response - { - $this->denyAccessUnlessGranted("ROLE_ADMIN"); - $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); - - if ($this->userService->hasAccessTo($actingUser, true)) { - $uo = $this->entityManager->getRepository(UsersOrganizations::class)->find($id); - if (!$uo) { - $this->loggerService->logEntityNotFound('UsersOrganization', ['id' => $id], $actingUser->getId()); - $this->addFlash('error', "La liaison utilisateur-organisation n'existe pas."); - throw new NotFoundHttpException("UserOrganization not found"); - } - $application = $this->entityManager->getRepository(Apps::class)->find($request->get('appId')); - if (!$application) { - $this->loggerService->logEntityNotFound('Application', ['id' => $request->get('appId')], $actingUser->getId()); - $this->addFlash('error', "L'application demandée n'existe pas."); - throw $this->createNotFoundException(self::NOT_FOUND); - } - - $selectedRolesIds = $request->get('roles', []); - $roleUser = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'USER']); - if (!$roleUser) { - $this->loggerService->logEntityNotFound('Role', ['name' => 'USER'], $actingUser->getId()); - $this->addFlash('error', "Le role de l'utilisateur n'existe pas."); - throw $this->createNotFoundException('User role not found'); - } - - if (!empty($selectedRolesIds)) { - // Si le role User n'est pas sélectionné, on désactive tous les liens (affiché comme 'accès' dans l'UI) - if (!in_array((string)$roleUser->getId(), $selectedRolesIds, true)) { - $this->userOrganizationAppService->deactivateAllUserOrganizationsAppLinks($uo, $application); - } else { - $this->userOrganizationAppService->syncRolesForUserOrganizationApp( - $uo, - $application, - $selectedRolesIds, - $actingUser - ); - } - - } else { - $this->userOrganizationAppService->deactivateAllUserOrganizationsAppLinks($uo, $application); - } - - $user = $uo->getUsers(); - $this->addFlash('success', 'Rôles mis à jour avec succès.'); - return $this->redirectToRoute('user_show', [ - 'user' => $user, - 'id' => $user->getId(), - 'organizationId' => $uo->getOrganization()->getId() - ]); - } - - throw $this->createAccessDeniedException(); - } - /* * AJAX endpoint for user listing with pagination * Get all the users that aren´t deleted and are active @@ -707,40 +519,14 @@ class UserController extends AbstractController { $this->denyAccessUnlessGranted("ROLE_ADMIN"); - $page = max(1, (int)$request->query->get('page', 1)); - $size = max(1, (int)$request->query->get('size', 10)); - - // Get filter parameters + $page = max(1, $request->query->getInt('page', 1)); + $size = max(1, $request->query->getInt('size', 10)); $filters = $request->query->all('filter', []); - $repo = $this->userRepository; + // Call the repository + $paginator = $this->userRepository->findActiveUsersForTabulator($page, $size, $filters); + $total = count($paginator); - // Base query - $qb = $repo->createQueryBuilder('u') - ->where('u.isDeleted = :del')->setParameter('del', false); - - // Apply filters - if (!empty($filters['name'])) { - $qb->andWhere('u.surname LIKE :name') - ->setParameter('name', '%' . $filters['name'] . '%'); - } - if (!empty($filters['prenom'])) { - $qb->andWhere('u.name LIKE :prenom') - ->setParameter('prenom', '%' . $filters['prenom'] . '%'); - } - if (!empty($filters['email'])) { - $qb->andWhere('u.email LIKE :email') - ->setParameter('email', '%' . $filters['email'] . '%'); - } - - $countQb = clone $qb; - $total = (int)$countQb->select('COUNT(u.id)')->getQuery()->getSingleScalarResult(); - - // Pagination - $offset = ($page - 1) * $size; - $rows = $qb->setFirstResult($offset)->setMaxResults($size)->getQuery()->getResult(); - - // Map to array $data = array_map(function (User $user) { return [ 'id' => $user->getId(), @@ -752,33 +538,15 @@ class UserController extends AbstractController 'showUrl' => $this->generateUrl('user_show', ['id' => $user->getId()]), 'statut' => $user->isActive(), ]; - }, $rows); - $lastPage = (int)ceil($total / $size); + }, iterator_to_array($paginator)); return $this->json([ 'data' => $data, - 'last_page' => $lastPage, + 'last_page' => (int)ceil($total / $size), 'total' => $total, ]); } - #[Route(path: '/', name: 'index', methods: ['GET'])] - public function index(): Response - { - $this->isGranted('ROLE_SUPER_ADMIN'); - $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); - if ($this->userService->hasAccessTo($actingUser, true) && $this->isGranted("ROLE_ADMIN")) { - $totalUsers = $this->userRepository->count(['isDeleted' => false, 'isActive' => true]); - return $this->render('user/index.html.twig', [ - 'users' => $totalUsers - ]); - } - - //shouldn't be reached normally - $this->loggerService->logAccessDenied($actingUser->getId()); - throw $this->createAccessDeniedException(self::ACCESS_DENIED); - } - /* * AJAX endpoint for new users listing * Get the 5 most recently created users for an organization @@ -786,10 +554,16 @@ class UserController extends AbstractController #[Route(path: '/data/new', name: 'dataNew', methods: ['GET'])] public function dataNew(Request $request): JsonResponse { - $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); - if ($this->userService->hasAccessTo($actingUser, true) && $this->isGranted("ROLE_ADMIN")) { - $orgId = $request->query->get('orgId'); - $uos = $this->uoRepository->findBy(['organization' => $orgId, 'statut' => ["ACCEPTED", "INVITED"]], + $actingUser = $this->getUser(); + $orgId = $request->query->get('orgId'); + $org = $this->organizationRepository->find($orgId); + if (!$org) { + $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, 'statut' => ["ACCEPTED", "INVITED"]], orderBy: ['createdAt' => 'DESC'], limit: 5); @@ -822,10 +596,15 @@ class UserController extends AbstractController #[Route(path: '/data/admin', name: 'dataAdmin', methods: ['GET'])] public function dataAdmin(Request $request): JsonResponse { - $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); - if ($this->userService->hasAccessTo($actingUser, true) && $this->isGranted("ROLE_ADMIN")) { - $orgId = $request->query->get('orgId'); - $uos = $this->uoRepository->findBy(['organization' => $orgId]); + $actingUser = $this->getUser(); + $orgId = $request->query->get('orgId'); + $org = $this->organizationRepository->find($orgId); + if (!$org) { + $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]); $roleAdmin = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN']); $users = []; foreach ($uos as $uo) { @@ -863,76 +642,54 @@ class UserController extends AbstractController #[Route(path: '/data/organization', name: 'dataUserOrganization', methods: ['GET'])] public function dataUserOrganization(Request $request): JsonResponse { - $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); + $actingUser = $this->getUser(); + $orgId = $request->query->get('orgId'); - if ($this->userService->hasAccessTo($actingUser, true) && $this->isGranted("ROLE_ADMIN")) { - $orgId = $request->query->get('orgId'); - $page = max(1, (int)$request->query->get('page', 1)); - $size = max(1, (int)$request->query->get('size', 10)); - - $filters = $request->query->all('filter') ?? []; - - $repo = $this->uoRepository; - - // Base query - $qb = $repo->createQueryBuilder('uo') - ->join('uo.users', 'u') - ->where('uo.organization = :orgId') - ->setParameter('orgId', $orgId); - - // Apply filters - if (!empty($filters['name'])) { - $qb->andWhere('u.surname LIKE :name') - ->setParameter('name', '%' . $filters['name'] . '%'); - } - if (!empty($filters['prenom'])) { - $qb->andWhere('u.name LIKE :prenom') - ->setParameter('prenom', '%' . $filters['prenom'] . '%'); - } - if (!empty($filters['email'])) { - $qb->andWhere('u.email LIKE :email') - ->setParameter('email', '%' . $filters['email'] . '%'); - } - - $countQb = clone $qb; - $total = (int)$countQb->select('COUNT(uo.id)')->getQuery()->getSingleScalarResult(); - - $qb->orderBy('uo.isActive', 'DESC') - ->addOrderBy('CASE WHEN uo.statut = :invited THEN 0 ELSE 1 END', 'ASC') - ->setParameter('invited', 'INVITED'); - - $offset = ($page - 1) * $size; - $rows = $qb->setFirstResult($offset)->setMaxResults($size)->getQuery()->getResult(); - $data = $this->userService->formatStatutForOrganizations($rows); - - $lastPage = (int)ceil($total / $size); - - return $this->json([ - 'data' => $data, - 'last_page' => $lastPage, - 'total' => $total, - ]); + $org = $this->organizationRepository->find($orgId); + if (!$org) { + $this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getUserIdentifier()); + throw $this->createNotFoundException(self::NOT_FOUND); } - throw $this->createAccessDeniedException(self::ACCESS_DENIED); + // Security Check + if (!$this->isGranted("ROLE_ADMIN") && !$this->userService->isAdminOfOrganization($org)) { + throw $this->createAccessDeniedException(self::ACCESS_DENIED); + } + // Params extraction + $page = max(1, $request->query->getInt('page', 1)); + $size = max(1, $request->query->getInt('size', 10)); + $filters = $request->query->all('filter') ?? []; + + // Get paginated results from Repository + $paginator = $this->uoRepository->findByOrganizationWithFilters($org, $page, $size, $filters); + $total = count($paginator); + + // Format the data using your existing service method + $data = $this->userService->formatStatutForOrganizations(iterator_to_array($paginator)); + + return $this->json([ + 'data' => $data, + 'last_page' => (int)ceil($total / $size), + 'total' => $total, + ]); } #[Route(path: '/organization/resend-invitation/{userId}', name: 'resend_invitation', methods: ['POST'])] public function resendInvitation(int $userId, Request $request): JsonResponse { - $this->denyAccessUnlessGranted("ROLE_ADMIN"); - $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); - if ($this->userService->hasAccessTo($actingUser, true)) { - $orgId = $request->get('organizationId'); - $org = $this->organizationRepository->find($orgId); - if (!$org) { - $this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getId()); - throw $this->createNotFoundException(self::NOT_FOUND); - } + $this->denyAccessUnlessGranted("ROLE_USER"); + $actingUser = $this->getUser(); + $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); + } + if ($this->userService->isAdminOfOrganization($org)) { $user = $this->userRepository->find($userId); if (!$user) { - $this->loggerService->logEntityNotFound('User', ['id' => $user->getId()], $actingUser->getId()); + $this->loggerService->logEntityNotFound('User', ['id' => $user->getId()], $actingUser->getUserIdentifier()); throw $this->createNotFoundException(self::NOT_FOUND); } $token = $this->userService->generatePasswordToken($user, $org->getId()); @@ -945,7 +702,7 @@ class UserController extends AbstractController if (!$uo) { $this->loggerService->logEntityNotFound('UsersOrganization', [ 'user_id' => $user->getId(), - 'organization_id' => $orgId], $actingUser->getId()); + 'organization_id' => $orgId], $actingUser->getUserIdentifier()); throw $this->createNotFoundException(self::NOT_FOUND); } $uo->setModifiedAt(new \DateTimeImmutable()); @@ -959,7 +716,7 @@ class UserController extends AbstractController $this->loggerService->logCritical('Error while resending invitation', [ 'target_user_id' => $user->getId(), 'organization_id' => $orgId, - 'acting_user_id' => $actingUser->getId(), + 'acting_user_id' => $actingUser->getUserIdentifier(), 'error' => $e->getMessage(), ]); return $this->json(['message' => 'Erreur lors de l\'envoie du mail.'], Response::HTTP_INTERNAL_SERVER_ERROR); @@ -972,8 +729,8 @@ class UserController extends AbstractController #[Route(path: '/accept-invitation', name: 'accept', methods: ['GET'])] public function acceptInvitation(Request $request): Response { - $token = $request->get('token'); - $userId = $request->get('id'); + $token = $request->query->get('token'); + $userId = $request->query->get('id'); if (!$token || !$userId) { $this->loggerService->logEntityNotFound('Token or UserId missing in accept invitation', [ diff --git a/src/Entity/Organizations.php b/src/Entity/Organizations.php index 281697c..470316d 100644 --- a/src/Entity/Organizations.php +++ b/src/Entity/Organizations.php @@ -61,12 +61,22 @@ class Organizations #[ORM\OneToMany(targetEntity: UserOrganizatonApp::class, mappedBy: 'organization')] private Collection $userOrganizatonApps; + #[ORM\Column(length: 4, nullable: true)] + private ?string $projectPrefix = null; + + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: Project::class, mappedBy: 'organization')] + private Collection $projects; + public function __construct() { $this->apps = new ArrayCollection(); $this->actions = new ArrayCollection(); $this->createdAt = new \DateTimeImmutable(); $this->userOrganizatonApps = new ArrayCollection(); + $this->projects = new ArrayCollection(); } public function getId(): ?int @@ -256,4 +266,46 @@ class Organizations return $this; } + + public function getProjectPrefix(): ?string + { + return $this->projectPrefix; + } + + public function setProjectPrefix(?string $projectPrefix): static + { + $this->projectPrefix = $projectPrefix; + + return $this; + } + + /** + * @return Collection + */ + public function getProjects(): Collection + { + return $this->projects; + } + + public function addProject(Project $project): static + { + if (!$this->projects->contains($project)) { + $this->projects->add($project); + $project->setOrganization($this); + } + + return $this; + } + + public function removeProject(Project $project): static + { + if ($this->projects->removeElement($project)) { + // set the owning side to null (unless already changed) + if ($project->getOrganization() === $this) { + $project->setOrganization(null); + } + } + + return $this; + } } diff --git a/src/Entity/Project.php b/src/Entity/Project.php new file mode 100644 index 0000000..1512564 --- /dev/null +++ b/src/Entity/Project.php @@ -0,0 +1,149 @@ +createdAt = new \DateTimeImmutable(); + $this->modifiedAt = new \DateTimeImmutable(); + $this->isActive = true; + $this->isDeleted = false; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): static + { + $this->name = $name; + + return $this; + } + + public function getOrganization(): ?Organizations + { + return $this->organization; + } + + public function setOrganization(?Organizations $organization): static + { + $this->organization = $organization; + + return $this; + } + + public function getApplications(): ?array + { + return $this->applications; + } + + public function setApplications(?array $applications): static + { + $this->applications = $applications; + + return $this; + } + + public function getCreatedAt(): ?\DateTimeImmutable + { + return $this->createdAt; + } + + public function setCreatedAt(\DateTimeImmutable $createdAt): static + { + $this->createdAt = $createdAt; + + return $this; + } + + public function getModifiedAt(): ?\DateTimeImmutable + { + return $this->modifiedAt; + } + + public function setModifiedAt(\DateTimeImmutable $modifiedAt): static + { + $this->modifiedAt = $modifiedAt; + + return $this; + } + + public function isActive(): ?bool + { + return $this->isActive; + } + + public function setIsActive(bool $isActive): static + { + $this->isActive = $isActive; + + return $this; + } + + public function isDeleted(): ?bool + { + return $this->isDeleted; + } + + public function setIsDeleted(bool $isDeleted): static + { + $this->isDeleted = $isDeleted; + + return $this; + } + + public function getBddName(): ?string + { + return $this->bddName; + } + + public function setBddName(string $bddName): static + { + $this->bddName = $bddName; + + return $this; + } +} diff --git a/src/Entity/UsersOrganizations.php b/src/Entity/UsersOrganizations.php index d524d46..efe84e0 100644 --- a/src/Entity/UsersOrganizations.php +++ b/src/Entity/UsersOrganizations.php @@ -41,6 +41,9 @@ class UsersOrganizations #[ORM\Column(nullable: true)] private ?\DateTimeImmutable $modifiedAt = null; + #[ORM\ManyToOne] + private ?Roles $role = null; + public function __construct() { $this->isActive = true; // Default value for isActive @@ -147,4 +150,16 @@ class UsersOrganizations return $this; } + + public function getRole(): ?Roles + { + return $this->role; + } + + public function setRole(?Roles $role): static + { + $this->role = $role; + + return $this; + } } diff --git a/src/EventSubscriber/UserSubscriber.php b/src/EventSubscriber/UserSubscriber.php index 3e27e14..3e559d9 100644 --- a/src/EventSubscriber/UserSubscriber.php +++ b/src/EventSubscriber/UserSubscriber.php @@ -30,26 +30,21 @@ class UserSubscriber implements EventSubscriberInterface $user = $event->getNewUser(); $actingUser = $event->getActingUser(); - // 1. Generate Token (If logic was moved here, otherwise assume UserService set it) - // If the token generation logic is still in UserService, just send the email here. - // If you moved generating the token here, do it now. - - // 2. Send Email - // Note: You might need to pass the token in the Event if it's not stored in the DB entity - // or generate a new one here if appropriate. + // 1. Send Email if ($user->getPasswordToken()) { $this->emailService->sendPasswordSetupEmail($user, $user->getPasswordToken()); } - // 3. Log the creation - $this->loggerService->logUserCreated($user->getId(), $actingUser->getId()); + // 2. Logic-based Logging (Moved from Service) + if (in_array('ROLE_ADMIN', $actingUser->getRoles(), true)) { + $this->loggerService->logSuperAdmin( + $user->getId(), + $actingUser->getId(), + "Super Admin created new user: " . $user->getUserIdentifier() + ); + } - // 4. Create the Audit Action - $this->actionService->createAction( - "Create new user", - $actingUser, - null, - $user->getUserIdentifier() - ); + // 3. General Audit Trail + $this->actionService->createAction("USER_CREATED", $actingUser, null, $user->getUserIdentifier()); } } \ No newline at end of file diff --git a/src/Repository/OrganizationsRepository.php b/src/Repository/OrganizationsRepository.php index e598d9f..87d4772 100644 --- a/src/Repository/OrganizationsRepository.php +++ b/src/Repository/OrganizationsRepository.php @@ -3,8 +3,11 @@ namespace App\Repository; use App\Entity\Organizations; +use App\Entity\User; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\ORM\Tools\Pagination\Paginator; use Doctrine\Persistence\ManagerRegistry; +use App\Entity\UsersOrganizations; /** * @extends ServiceEntityRepository @@ -16,28 +19,37 @@ class OrganizationsRepository extends ServiceEntityRepository parent::__construct($registry, Organizations::class); } - // /** - // * @return Organizations[] Returns an array of Organizations objects - // */ - // public function findByExampleField($value): array - // { - // return $this->createQueryBuilder('o') - // ->andWhere('o.exampleField = :val') - // ->setParameter('val', $value) - // ->orderBy('o.id', 'ASC') - // ->setMaxResults(10) - // ->getQuery() - // ->getResult() - // ; - // } + public function findAdmissibleOrganizations(User $user, bool $isSuperAdmin, int $page, int $size, array $filters = []): Paginator + { + $qb = $this->createQueryBuilder('o') + ->where('o.isDeleted = :del') + ->setParameter('del', false); - // public function findOneBySomeField($value): ?Organizations - // { - // return $this->createQueryBuilder('o') - // ->andWhere('o.exampleField = :val') - // ->setParameter('val', $value) - // ->getQuery() - // ->getOneOrNullResult() - // ; - // } + // 1. Security Logic: If not Super Admin, join UsersOrganizations to filter + if (!$isSuperAdmin) { + $qb->innerJoin(UsersOrganizations::class, 'uo', 'WITH', 'uo.organization = o') + ->andWhere('uo.users = :user') + ->andWhere('uo.role = :roleAdmin') + ->andWhere('uo.isActive = true') + ->setParameter('user', $user) + // You can pass the actual Role entity or the string name depending on your mapping + ->setParameter('roleAdmin', $this->_em->getRepository(\App\Entity\Roles::class)->findOneBy(['name' => 'ADMIN'])); + } + + // 2. Filters + if (!empty($filters['name'])) { + $qb->andWhere('o.name LIKE :name') + ->setParameter('name', '%' . $filters['name'] . '%'); + } + if (!empty($filters['email'])) { + $qb->andWhere('o.email LIKE :email') + ->setParameter('email', '%' . $filters['email'] . '%'); + } + + // 3. Pagination + $qb->setFirstResult(($page - 1) * $size) + ->setMaxResults($size); + + return new Paginator($qb); + } } diff --git a/src/Repository/ProjectRepository.php b/src/Repository/ProjectRepository.php new file mode 100644 index 0000000..9d1d68c --- /dev/null +++ b/src/Repository/ProjectRepository.php @@ -0,0 +1,61 @@ + + */ +class ProjectRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Project::class); + } + + // /** + // * @return Project[] Returns an array of Project objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('p') + // ->andWhere('p.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('p.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?Project + // { + // return $this->createQueryBuilder('p') + // ->andWhere('p.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } + public function findProjectByOrganization(int $organizationId, int $page, int $size, array $filters) + { + $qb = $this->createQueryBuilder('p') + ->where('p.organization = :orgId') + ->andWhere('p.isDeleted = :del') + ->setParameter('orgId', $organizationId) + ->setParameter('del', false); + + if (!empty($filters['name'])) { + $qb->andWhere('p.name LIKE :name') + ->setParameter('name', '%' . strtoLower($filters['name']) . '%'); + } + + return $qb->setFirstResult(($page - 1) * $size) + ->setMaxResults($size) + ->getQuery() + ->getResult(); + } +} diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php index 0af98d0..9cb0b71 100644 --- a/src/Repository/UserRepository.php +++ b/src/Repository/UserRepository.php @@ -4,11 +4,11 @@ namespace App\Repository; use App\Entity\User; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\ORM\Tools\Pagination\Paginator; use Doctrine\Persistence\ManagerRegistry; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; -use App\Entity\UsersOrganizations; /** * @extends ServiceEntityRepository @@ -35,21 +35,33 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader } /** - * Returns active users that are NOT in any UsersOrganizations mapping. - * Returns User entities. - * - * @return User[] + * @param int $page + * @param int $size + * @param array $filters + * @return Paginator */ - public function findUsersWithoutOrganization(): array + public function findActiveUsersForTabulator(int $page, int $size, array $filters = []): Paginator { $qb = $this->createQueryBuilder('u') - ->select('u') - ->leftJoin(UsersOrganizations::class, 'uo', 'WITH', 'uo.users = u') - ->andWhere('u.isDeleted = :uDeleted') - ->andWhere('uo.id IS NULL') - ->orderBy('u.surname', 'ASC') - ->setParameter('uDeleted', false); + ->where('u.isDeleted = :del') + ->setParameter('del', false); - return $qb->getQuery()->getResult(); + if (!empty($filters['name'])) { + $qb->andWhere('u.surname LIKE :name') + ->setParameter('name', '%' . $filters['name'] . '%'); + } + if (!empty($filters['prenom'])) { + $qb->andWhere('u.name LIKE :prenom') + ->setParameter('prenom', '%' . $filters['prenom'] . '%'); + } + if (!empty($filters['email'])) { + $qb->andWhere('u.email LIKE :email') + ->setParameter('email', '%' . $filters['email'] . '%'); + } + + $qb->setFirstResult(($page - 1) * $size) + ->setMaxResults($size); + + return new Paginator($qb); } } diff --git a/src/Repository/UsersOrganizationsRepository.php b/src/Repository/UsersOrganizationsRepository.php index c15ce1c..47d066e 100644 --- a/src/Repository/UsersOrganizationsRepository.php +++ b/src/Repository/UsersOrganizationsRepository.php @@ -6,8 +6,10 @@ use App\Entity\Organizations; use App\Entity\User; use App\Entity\UsersOrganizations; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\ORM\Tools\Pagination\Paginator; use Doctrine\Persistence\ManagerRegistry; + /** * @extends ServiceEntityRepository */ @@ -19,121 +21,64 @@ class UsersOrganizationsRepository extends ServiceEntityRepository } /** - * Returns active user-organization mappings with joined User and Organization. - * Only active and non-deleted users and organizations are included. - * - * @return UsersOrganizations[] + * Checks if an acting user has administrative rights over a target user + * based on shared organizational memberships. */ - public function findUsersWithOrganization(array $organizationIds = null): array + public function isUserAdminOfTarget(User $actingUser, User $targetUser, $adminRole): bool + { + $qb = $this->createQueryBuilder('uo_acting'); + + return (bool) $qb + ->select('COUNT(uo_acting.id)') + // We join the same table again to find the target user in the same organization + ->innerJoin( + UsersOrganizations::class, + 'uo_target', + 'WITH', + 'uo_target.organization = uo_acting.organization' + ) + ->where('uo_acting.users = :actingUser') + ->andWhere('uo_acting.role = :role') + ->andWhere('uo_acting.isActive = true') + ->andWhere('uo_target.users = :targetUser') + ->andWhere('uo_target.statut = :status') + ->setParameter('actingUser', $actingUser) + ->setParameter('targetUser', $targetUser) + ->setParameter('role', $adminRole) + ->setParameter('status', 'ACCEPTED') + ->getQuery() + ->getSingleScalarResult() > 0; + } + + public function findByOrganizationWithFilters(Organizations $org, int $page, int $size, array $filters = []): Paginator { $qb = $this->createQueryBuilder('uo') - ->addSelect('u', 'o') - ->leftJoin('uo.users', 'u') - ->leftJoin('uo.organization', 'o') - ->andWhere('u.isActive = :uActive') - ->andWhere('u.isDeleted = :uDeleted') - ->andWhere('o.isActive = :oActive') - ->andWhere('o.isDeleted = :oDeleted') - ->orderBy('o.name', 'ASC') - ->addOrderBy('u.surname', 'ASC') - ->setParameter('uActive', true) - ->setParameter('uDeleted', false) - ->setParameter('oActive', true) - ->setParameter('oDeleted', false); - if (!empty($organizationIds)) { - $qb->andWhere('o.id IN (:orgIds)') - ->setParameter('orgIds', $organizationIds); + ->innerJoin('uo.users', 'u') + ->where('uo.organization = :org') + ->setParameter('org', $org); + + // Apply filters + if (!empty($filters['name'])) { + $qb->andWhere('u.surname LIKE :name') + ->setParameter('name', '%' . $filters['name'] . '%'); + } + if (!empty($filters['prenom'])) { + $qb->andWhere('u.name LIKE :prenom') + ->setParameter('prenom', '%' . $filters['prenom'] . '%'); + } + if (!empty($filters['email'])) { + $qb->andWhere('u.email LIKE :email') + ->setParameter('email', '%' . $filters['email'] . '%'); } - return $qb->getQuery()->getResult(); - } + // Apply complex sorting + $qb->orderBy('uo.isActive', 'DESC') + ->addOrderBy("CASE WHEN uo.statut = 'INVITED' THEN 0 ELSE 1 END", 'ASC'); - /** - * Same as above, filtered by a list of organization IDs. - * - * @param int[] $organizationIds - * @return UsersOrganizations[] - */ - public function findActiveWithUserAndOrganizationByOrganizationIds(array $organizationIds): array - { - if (empty($organizationIds)) { - return []; - } + // Pagination + $qb->setFirstResult(($page - 1) * $size) + ->setMaxResults($size); - $qb = $this->createQueryBuilder('uo') - ->addSelect('u', 'o') - ->leftJoin('uo.users', 'u') - ->leftJoin('uo.organization', 'o') - ->where('uo.isActive = :uoActive') - ->andWhere('u.isActive = :uActive') - ->andWhere('u.isDeleted = :uDeleted') - ->andWhere('o.isActive = :oActive') - ->andWhere('o.isDeleted = :oDeleted') - ->andWhere('o.id IN (:orgIds)') - ->orderBy('o.name', 'ASC') - ->addOrderBy('u.surname', 'ASC') - ->setParameter('uoActive', true) - ->setParameter('uActive', true) - ->setParameter('uDeleted', false) - ->setParameter('oActive', true) - ->setParameter('oDeleted', false) - ->setParameter('orgIds', $organizationIds); - - return $qb->getQuery()->getResult(); - } - - /** - * Find 10 newest Users in an Organization. - * - * @param Organizations $organization - * @return User[] - */ - public function findNewestUO(Organizations $organization): array - { - $qb = $this->createQueryBuilder('uo') - ->select('uo', 'u') - ->leftJoin('uo.users', 'u') - ->where('uo.organization = :org') - ->andWhere('uo.isActive = :uoActive') - ->andWhere('u.isActive = :uActive') - ->andWhere('u.isDeleted = :uDeleted') - ->orderBy('u.createdAt', 'DESC') - ->setMaxResults(10) - ->setParameter('org', $organization) - ->setParameter('uoActive', true) - ->setParameter('uActive', true) - ->setParameter('uDeleted', false); - - return $qb->getQuery()->getResult(); - } - - /** - * Find all the admins of an Organization. - * limited to 10 results. - * - * @param Organizations $organization - * @return User[] - */ - public function findAdminsInOrganization(Organizations $organization): array - { - $qb = $this->createQueryBuilder('uo') - ->select('uo', 'u') - ->leftJoin('uo.users', 'u') - ->leftJoin('uo.userOrganizatonApps', 'uoa') - ->leftJoin('uoa.role', 'r') - ->where('uo.organization = :org') - ->andWhere('uo.isActive = :uoActive') - ->andWhere('u.isActive = :uActive') - ->andWhere('u.isDeleted = :uDeleted') - ->andWhere('r.name = :roleAdmin') - ->orderBy('u.surname', 'ASC') - ->setMaxResults(10) - ->setParameter('org', $organization) - ->setParameter('uoActive', true) - ->setParameter('uActive', true) - ->setParameter('uDeleted', false) - ->setParameter('roleAdmin', 'ADMIN'); - - return $qb->getQuery()->getResult(); + return new Paginator($qb); } } diff --git a/src/Service/LoggerService.php b/src/Service/LoggerService.php index a610708..75f4e86 100644 --- a/src/Service/LoggerService.php +++ b/src/Service/LoggerService.php @@ -23,7 +23,7 @@ readonly class LoggerService // User Management Logs - public function logUserCreated(int $userId, int $actingUserId): void + public function logUserCreated(int|string $userId, int|string $actingUserId): void { $this->userManagementLogger->notice("New user created: $userId", [ 'target_user_id' => $userId, @@ -34,7 +34,7 @@ readonly class LoggerService } // Organization Management Logs - public function logUserOrganizationLinkCreated(int $userId, int $orgId, int $actingUserId, ?int $uoId): void + public function logUserOrganizationLinkCreated(int|string $userId, int $orgId, int|string $actingUserId, ?int $uoId): void { $this->organizationManagementLogger->notice('User-Organization link created', [ 'target_user_id' => $userId, @@ -46,7 +46,7 @@ readonly class LoggerService ]); } - public function logExistingUserAddedToOrg(int $userId, int $orgId, int $actingUserId, int $uoId): void + public function logExistingUserAddedToOrg(int|string $userId, int $orgId, int|string $actingUserId, int $uoId): void { $this->organizationManagementLogger->notice('Existing user added to organization', [ 'target_user_id' => $userId, @@ -59,7 +59,7 @@ readonly class LoggerService } // Email Notification Logs - public function logEmailSent(int $userId, ?int $orgId, string $message): void + public function logEmailSent(int|string $userId, ?int $orgId, string $message): void { $this->emailNotificationLogger->notice($message, [ 'target_user_id' => $userId, @@ -69,7 +69,7 @@ readonly class LoggerService ]); } - public function logExistingUserNotificationSent(int $userId, int $orgId): void + public function logExistingUserNotificationSent(int|string $userId, int $orgId): void { $this->emailNotificationLogger->notice("Existing user notification email sent to $userId", [ 'target_user_id' => $userId, @@ -87,7 +87,7 @@ readonly class LoggerService ])); } - public function logSuperAdmin(int $userId, int $actingUserId, string $message, ?int $orgId = null): void + public function logSuperAdmin(int|string $userId, int|string $actingUserId, string $message, ?int $orgId = null): void { $this->adminActionsLogger->notice($message, [ 'target_user_id' => $userId, @@ -116,7 +116,7 @@ readonly class LoggerService } // Security Logs - public function logAccessDenied(?int $actingUserId): void + public function logAccessDenied(int|string $actingUserId): void { $this->securityLogger->warning('Access denied', [ 'acting_user_id' => $actingUserId, @@ -133,7 +133,7 @@ readonly class LoggerService } - public function logUserAction(int $targetId, int $actingUserId, string $message): void + public function logUserAction(int $targetId, int|string $actingUserId, string $message): void { $this->userManagementLogger->notice($message, [ 'target_user_id'=> $targetId, @@ -143,7 +143,7 @@ readonly class LoggerService ]); } - public function logAdminAction(int $targetId, int $actingUserId, int $organizationId, string $message): void + public function logAdminAction(int $targetId, int|string $actingUserId, int $organizationId, string $message): void { $this->adminActionsLogger->notice($message, [ 'target_id' => $targetId, @@ -154,7 +154,7 @@ readonly class LoggerService ]); } - public function logEntityNotFound(string $entityType, array $criteria, ?int $actingUserId): void + public function logEntityNotFound(string $entityType, array $criteria, int|string $actingUserId): void { $this->errorLogger->error('Entity not found', array_merge($criteria, [ 'entity_type' => $entityType, @@ -192,7 +192,7 @@ readonly class LoggerService ]); } - public function logOrganizationInformation(int $organizationId, int $actingUserId, string $message): void + public function logOrganizationInformation(int $organizationId, int|string $actingUserId, string $message): void { $this->organizationManagementLogger->info($message, [ 'organization_id' => $organizationId, @@ -202,7 +202,7 @@ readonly class LoggerService ]); } - public function logRoleEntityAssignment(int $userId, int $organizationId, int $roleId, int $actingUserId, string $message): void + public function logRoleEntityAssignment(int|string $userId, int $organizationId, int $roleId, int|string $actingUserId, string $message): void { $this->accessControlLogger->info($message, [ 'target_user_id' => $userId, @@ -252,7 +252,7 @@ readonly class LoggerService ])); } - public function logApplicationInformation(string $string, array $array, int $actingUser) + public function logApplicationInformation(string $string, array $array, int|string $actingUser) { $this->accessControlLogger->info($string, array_merge($array, [ 'acting_user_id' => $actingUser, diff --git a/src/Service/OrganizationsService.php b/src/Service/OrganizationsService.php index ec7c095..cfb6263 100644 --- a/src/Service/OrganizationsService.php +++ b/src/Service/OrganizationsService.php @@ -229,4 +229,19 @@ class OrganizationsService } } + /* Function that check if the project prefix was provided and if it is unique, if not it will generate a random one and check again until it is unique */ + public function generateUniqueProjectPrefix(): string{ + $prefix = $this->generateRandomPrefix(); + while ($this->entityManager->getRepository(Organizations::class)->findOneBy(['projectPrefix' => $prefix])) { + $prefix = $this->generateRandomPrefix(); + } + return $prefix; + } + + private function generateRandomPrefix(): string + { + return substr(str_shuffle('abcdefghijklmnopqrstuvwxyz'), 0, 4); + } + + } diff --git a/src/Service/ProjectService.php b/src/Service/ProjectService.php new file mode 100644 index 0000000..11e8d4f --- /dev/null +++ b/src/Service/ProjectService.php @@ -0,0 +1,43 @@ +slug($projectName, '_')->lower()->toString(); +// \d matches any digit character, equivalent to [0-9]. So, the regular expression '/\d/' will match any digit in the string. + $str = preg_replace('/\d/', '', $slug); + return $projectPrefix . '_' . $str; + } + + public function isApplicationArrayValid(array $applicationArray): bool + { + foreach ($applicationArray as $app) { + $app = (int) $app; + if (empty($app) || $app <= 0 || empty($this->appsRepository->findOneBy(['id' => $app]))) { + return false; + } + } + return true; + } +} diff --git a/src/Service/UserOrganizationService.php b/src/Service/UserOrganizationService.php index b5caca9..cbe9b43 100644 --- a/src/Service/UserOrganizationService.php +++ b/src/Service/UserOrganizationService.php @@ -6,6 +6,7 @@ use App\Entity\Actions; use App\Entity\Organizations; use App\Entity\User; use App\Entity\UsersOrganizations; +use App\Repository\RolesRepository; use App\Service\ActionService; use App\Service\LoggerService; use \App\Service\UserOrganizationAppService; @@ -20,7 +21,7 @@ readonly class UserOrganizationService { public function __construct( - private userOrganizationAppService $userOrganizationAppService, private EntityManagerInterface $entityManager, private ActionService $actionService, private LoggerService $loggerService, + private userOrganizationAppService $userOrganizationAppService, private EntityManagerInterface $entityManager, private ActionService $actionService, private LoggerService $loggerService, private RolesRepository $rolesRepository, ) { } @@ -55,6 +56,17 @@ readonly class UserOrganizationService } + public function getAdminOrganizationsForUser(User $user): array + { + $adminRole = $this->rolesRepository->findOneBy(['name' => "ADMIN"]); // Assuming 'ADMIN' is the role name for administrators + $uos = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['users' => $user, 'role' => $adminRole, 'isActive' => true]); + $adminOrgs = []; + foreach ($uos as $uo) { + $adminOrgs[] = $uo->getOrganization(); + } + return $adminOrgs; + } + } diff --git a/src/Service/UserService.php b/src/Service/UserService.php index 09108a6..2ea60c0 100644 --- a/src/Service/UserService.php +++ b/src/Service/UserService.php @@ -8,6 +8,7 @@ use App\Entity\Roles; use App\Entity\User; use App\Entity\UserOrganizatonApp; use App\Entity\UsersOrganizations; +use App\Repository\RolesRepository; use DateTimeImmutable; use DateTimeZone; use Doctrine\ORM\EntityManagerInterface; @@ -33,7 +34,7 @@ class UserService private readonly ActionService $actionService, private readonly EmailService $emailService, private readonly OrganizationsService $organizationsService, - private readonly EventDispatcherInterface $eventDispatcher + private readonly EventDispatcherInterface $eventDispatcher, private readonly RolesRepository $rolesRepository ) { @@ -48,6 +49,23 @@ class UserService return bin2hex(random_bytes(32)); } + /** Check if the user is admin in any organization. + * Return true if the user is admin in at least one organization, false otherwise. + * + * @param User $user + * @return bool + * @throws Exception + */ +// TODO: pas sur de l'utiliser, à vérifier + public function isAdminInAnyOrganization(User $user): bool + { + $roleAdmin = $this->rolesRepository->findOneBy(['name' => 'ADMIN']); + $uoAdmin = $this->entityManager->getRepository(UsersOrganizations::class)->findOneBy([ + 'users' => $user, + 'isActive' => true, + 'role'=> $roleAdmin]); + return $uoAdmin !== null; + } /** * Check if the user is currently connected. @@ -75,26 +93,30 @@ class UserService } /** - * Check if the user have the rights to access the page - * Self check can be skipped when checking access for the current user + * Determines if the currently logged-in user has permission to manage or view a target User. + * * Access is granted if: + * 1. The current user is a Super Admin. + * 2. The current user is the target user itself. + * 3. The current user is an active Admin of an organization the target user belongs to. * - * @param User $user - * @param bool $skipSelfCheck - * @return bool - * @throws Exception + * @param User $user The target User object we are checking access against. + * * @return bool True if access is permitted, false otherwise. + * @throws Exception If database or security context issues occur. */ - public function hasAccessTo(User $user, bool $skipSelfCheck = false): bool + public function hasAccessTo(User $user): bool { - if ($this->security->isGranted('ROLE_SUPER_ADMIN')) { + if ($this->security->isGranted('ROLE_ADMIN')) { return true; } - if (!$skipSelfCheck && $user->getUserIdentifier() === $this->security->getUser()->getUserIdentifier()) { +// S'il s'agit de son propre compte, on lui donne accès + if ($user->getUserIdentifier() === $this->security->getUser()->getUserIdentifier()) { return true; } $userOrganizations = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['users' => $user]); if ($userOrganizations) { foreach ($userOrganizations as $uo) { - if ($this->isAdminOfOrganization($uo->getOrganization()) && $uo->getStatut() === "ACCEPTED" && $uo->isActive()) { + //l'utilisateur doit être actif dans l'org, avoir le statut ACCEPTED (double vérif) et être admin de l'org + if ($uo->getStatut() === "ACCEPTED" && $uo->isActive() && $this->isAdminOfOrganization($uo->getOrganization())) { return true; } } @@ -103,11 +125,36 @@ class UserService } + /* Return if the current user is an admin of the target user. + * This is true if the current user is an admin of at least one organization that the target user belongs to. + * + * @param User $user + * @return bool + * @throws Exception + */ + public function isAdminOfUser(User $user): bool + { + $actingUser = $this->security->getUser(); + + if (!$actingUser instanceof User) { + return false; + } + + // Reuse the cached/fetched role + $adminRole = $this->rolesRepository->findOneBy(['name' => 'ADMIN']); + + if (!$adminRole) { + return false; + } + + return $this->entityManager + ->getRepository(UsersOrganizations::class) + ->isUserAdminOfTarget($actingUser, $user, $adminRole); + } + /** - * Check if the user is an admin of the organization - * A user is considered an admin of an organization if they have the 'ROLE_ADMIN' AND have the link to the - * entity role 'ROLE_ADMIN' in the UsersOrganizationsApp entity - * (if he is admin for any application of the organization). + * Check if the acting user is an admin of the organization + * A user is considered an admin of an organization if they have an active UsersOrganizations link with the role of ADMIN for that organization. * * @param Organizations $organizations * @return bool @@ -115,20 +162,15 @@ class UserService */ public function isAdminOfOrganization(Organizations $organizations): bool { - $actingUser = $this->getUserByIdentifier($this->security->getUser()->getUserIdentifier()); - $uo = $this->entityManager->getRepository(UsersOrganizations::class)->findOneBy(['users' => $actingUser, 'organization' => $organizations]); + $actingUser =$this->security->getUser(); $roleAdmin = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN']); - if ($uo) { - $uoa = $this->entityManager->getRepository(UserOrganizatonApp::class)->findOneBy(['userOrganization' => $uo, - 'role' => $roleAdmin, - 'isActive' => true]); - if ($uoa && $this->security->isGranted('ROLE_ADMIN')) { - return true; - } - } - return false; - } + $uoAdmin = $this->entityManager->getRepository(UsersOrganizations::class)->findOneBy(['users' => $actingUser, + 'organization' => $organizations, + 'role'=> $roleAdmin, + 'isActive' => true]); + return $uoAdmin !== null; + } /** * Get the user by their identifier. @@ -499,14 +541,7 @@ class UserService $user->setIsActive(true); $this->entityManager->persist($user); } - $uo = new UsersOrganizations(); - $uo->setUsers($user); - $uo->setOrganization($organization); - $uo->setStatut("INVITED"); - $uo->setIsActive(false); - $uo->setModifiedAt(new \DateTimeImmutable('now')); - $this->entityManager->persist($uo); - $this->entityManager->flush(); + $uo = $this->linkUserToOrganization($user, $organization); return $uo->getId(); } @@ -554,12 +589,11 @@ class UserService public function addExistingUserToOrganization( User $existingUser, Organizations $org, - User $actingUser, ): int { try { $uoId = $this->handleExistingUser($existingUser, $org); - + $actingUser = $this->getUserByIdentifier($this->security->getUser()->getUserIdentifier()); $this->loggerService->logExistingUserAddedToOrg( $existingUser->getId(), $org->getId(), @@ -593,20 +627,16 @@ class UserService try { $this->formatUserData($user, $picture, true); - // Generate token here if it's part of the user persistence flow - $token = $this->generatePasswordToken($user); $user->setisActive(false); + $this->generatePasswordToken($user); // Set token on the entity + $this->entityManager->persist($user); $this->entityManager->flush(); - + $this->eventDispatcher->dispatch(new UserCreatedEvent($user, $actingUser)); } catch (\Exception $e) { - // Error logging remains here because the event won't fire if exception occurs - $this->loggerService->logError('Error creating new user: ' . $e->getMessage(), [ - 'target_user_email' => $user->getEmail(), - 'acting_user_id' => $actingUser->getId(), - ]); + $this->loggerService->logError('Error creating user: ' . $e->getMessage()); throw $e; } } @@ -617,15 +647,17 @@ class UserService public function linkUserToOrganization( User $user, Organizations $org, - User $actingUser, ): UsersOrganizations { + $actingUser = $this->getUserByIdentifier($this->security->getUser()->getUserIdentifier()); try { + $roleUser = $this->rolesRepository->findOneBy(['name' => 'USER']); $uo = new UsersOrganizations(); $uo->setUsers($user); $uo->setOrganization($org); $uo->setStatut("INVITED"); $uo->setIsActive(false); + $uo->setRole($roleUser); $uo->setModifiedAt(new \DateTimeImmutable('now')); $this->entityManager->persist($uo); $this->entityManager->flush(); @@ -644,6 +676,15 @@ class UserService $org, "Added {$user->getUserIdentifier()} to {$org->getName()}" ); + $auRoles = $actingUser->getRoles(); + if (in_array('ROLE_ADMIN', $auRoles, true)) { + $this->loggerService->logSuperAdmin( + $user->getId(), + $actingUser->getId(), + "Admin linked user to organization during creation", + $org->getId() + ); + } $this->sendNewUserNotifications($user, $org, $actingUser); diff --git a/src/Twig/MenuExtension.php b/src/Twig/MenuExtension.php new file mode 100644 index 0000000..e3115d0 --- /dev/null +++ b/src/Twig/MenuExtension.php @@ -0,0 +1,41 @@ +security->getUser(); + + if (!$user) { + return false; + } + + // 1. If Super Admin, they see it + if ($this->security->isGranted('ROLE_ADMIN')) { + return true; + } + + return $this->userService->isAdminInAnyOrganization($user); + } +} \ No newline at end of file diff --git a/templates/elements/menu.html.twig b/templates/elements/menu.html.twig index fecb6f2..334d54f 100644 --- a/templates/elements/menu.html.twig +++ b/templates/elements/menu.html.twig @@ -3,7 +3,7 @@ {% set current_route = app.request.attributes.get('_route') %} \ No newline at end of file diff --git a/templates/organization/show.html.twig b/templates/organization/show.html.twig index a24e1b2..8605ad1 100644 --- a/templates/organization/show.html.twig +++ b/templates/organization/show.html.twig @@ -1,6 +1,7 @@ {% extends 'base.html.twig' %} {% block body %} + {% set isSA = is_granted('ROLE_SUPER_ADMIN')%}
{% for type, messages in app.flashes %} {% for message in messages %} @@ -18,7 +19,7 @@

{{ organization.name|title }} - Dashboard

- {% if is_granted("ROLE_SUPER_ADMIN") %} + {% if isSA %} Gérer l'organisation
{# APPLICATION ROW #} - {# TODO: Weird gap not going away #} -
- {% for application in applications %} -
- {% include 'application/appSmall.html.twig' with { - application: application - } %} + {# TODO:remove app acces and replace wioth project overview#} +
+
+

Mes Projets

+ {% if is_granted("ROLE_SUPER_ADMIN") %} + {# Trigger for the Modal #} + + {% endif %} +
+
+
- {% endfor %} +
+ +
{# Activities col #}
-
@@ -122,7 +164,7 @@

Activité récente

-
@@ -148,6 +190,7 @@ + {% endblock %} diff --git a/templates/project/index.html.twig b/templates/project/index.html.twig new file mode 100644 index 0000000..0749a00 --- /dev/null +++ b/templates/project/index.html.twig @@ -0,0 +1,5 @@ +{% extends 'base.html.twig' %} + +{% block body %} + +{% endblock %} diff --git a/templates/user/index.html.twig b/templates/user/index.html.twig index 2d48030..cf97b7e 100644 --- a/templates/user/index.html.twig +++ b/templates/user/index.html.twig @@ -3,7 +3,7 @@ {% block title %}User Profile{% endblock %} {% block body %} - {% if is_granted('ROLE_SUPER_ADMIN') %} + {% if is_granted('ROLE_ADMIN') %}
{% for type, messages in app.flashes %} {% for message in messages %} @@ -40,7 +40,7 @@ {% else %}
-
+

Accès limité

Vous n'avez pas les permissions nécessaires pour voir la liste des utilisateurs.

diff --git a/templates/user/show.html.twig b/templates/user/show.html.twig index adee997..924a9bd 100644 --- a/templates/user/show.html.twig +++ b/templates/user/show.html.twig @@ -3,7 +3,7 @@ {% block body %}
-
+
{% for type, messages in app.flashes %} {% for message in messages %}
@@ -11,20 +11,22 @@
{% endfor %} {% endfor %} - - {% if is_granted("ROLE_ADMIN") %}
-
-

Gestion Utilisateur

-
+

Gestion Utilisateur

- {% if is_granted("ROLE_SUPER_ADMIN") %} - Supprimer + {% if is_granted("ROLE_ADMIN") %} + + + Supprimer {% endif %}
- {% endif %}
@@ -34,103 +36,107 @@
-

Vos applications

+

Information d'organisation

- - -
-
- {% 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 %} -
+
+{# 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 %}#} +{#
#} +{#
#} +
diff --git a/templates/user/userInformation.html.twig b/templates/user/userInformation.html.twig index f74c11c..ed47e03 100644 --- a/templates/user/userInformation.html.twig +++ b/templates/user/userInformation.html.twig @@ -12,18 +12,13 @@
- {% if canEdit %} - Modifier - {% elseif user.id == app.user.id or is_granted("ROLE_SUPER_ADMIN") %} Modifier - {% endif %}
-
+

Email: {{ user.email }}

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