Merge branch 'doc' into 'develop'

Doc

See merge request easy-solutions/apps/easyportal!31
This commit is contained in:
Charles-Edouard MARGUERITE 2026-02-16 13:28:49 +00:00
commit 056325bcf3
39 changed files with 1791 additions and 997 deletions

View File

@ -18,7 +18,7 @@ export default class extends Controller {
this.loadActivities(); this.loadActivities();
setInterval(() => { setInterval(() => {
this.loadActivities(); this.loadActivities();
}, 60000); // Refresh every 60 seconds }, 300000); // Refresh every 5 minutes
} }
if (this.tableValue && this.sadminValue) { if (this.tableValue && this.sadminValue) {
this.table(); this.table();

View File

@ -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: "<b>ID</b> ", field: "id", visible: false},
{title: "<b>Nom du projet</b> ", 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 "<span class='text-muted' style='font-size: 0.8rem;'>Aucune</span>";
}
// Wrap everything in a flex container to keep them on one line
const content = apps.map(app => `
<div class="me-1" title="${app.name}">
<img src="${app.logoMiniUrl}"
alt="${app.name}"
style="height: 35px; width: 35px; object-fit: contain; border-radius: 4px;">
</div>
`).join('');
return `<div class="d-flex flex-wrap align-items-center">${content}</div>`;
}
}
];
// 2. Add the conditional column if admin value is true
if (this.adminValue) {
columns.push({
title: "<b>Base de données</b>",
field: "bddName",
hozAlign: "left",
},
{
title: "<b>Actions</b>",
field: "id",
width: 120,
hozAlign: "center",
headerSort: false,
formatter: (cell) => {
const id = cell.getValue();
// Return a button that Stimulus can listen to
return `<div class="d-flex gap-2 align-content-center">
<button class="btn btn-link p-0 border-0" data-action="click->project#openEditModal"
data-id="${id}">
${pencilIcon()}</button>
<button class="btn btn-link p-0 border-0" data-action="click->project#deleteProject"
data-id="${id}"> ${trashIcon()} </button>
</div>`;
}
});
}
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 => `
<div class="col-md-4 mb-3">
<div class="form-check border p-2 rounded d-flex align-items-center gap-2">
<input class="form-check-input ms-1" type="checkbox" name="applications[]" value="${app.id}" id="app_${app.id}">
<label class="form-check-label d-flex align-items-center gap-2" for="app_${app.id}">
<img src="${app.logoMiniUrl}" alt="${app.name}" style="height: 20px; width: 20px; object-fit: contain;">
${app.name}
</label>
</div>
</div>
`).join('');
} catch (error) {
this.appListTarget.innerHTML = '<div class="text-danger">Erreur de chargement.</div>';
}
}
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.");
}
}
}

View File

@ -18,7 +18,7 @@ export default class extends Controller {
orgId: Number orgId: Number
} }
static targets = ["select"]; static targets = ["select", "statusButton"];
connect() { connect() {
this.roleSelect(); this.roleSelect();
@ -180,7 +180,7 @@ export default class extends Controller {
const isActive = Boolean(statut); const isActive = Boolean(statut);
const actionClass = isActive ? 'deactivate-user' : 'activate-user'; 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'; const actionColorClass = isActive ? 'color-secondary' : 'color-primary';
// SVGs // SVGs
@ -678,7 +678,7 @@ export default class extends Controller {
const isActive = (statut === "ACTIVE"); const isActive = (statut === "ACTIVE");
const actionClass = isActive ? 'deactivate-user' : 'activate-user'; 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'; const actionColorClass = isActive ? 'color-secondary' : 'color-primary';
// SVGs // SVGs
@ -874,4 +874,48 @@ export default class extends Controller {
columns 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";
}
}
} }

View File

@ -22,7 +22,7 @@ export const TABULATOR_FR_LANG = {
}; };
export function eyeIconLink(url) { export function eyeIconLink(url) {
return `<a href="${url}" class="p-3 align-middle color-primary" title="Voir"> return `<a href="${url}" class="p-3 align-middle color-primary" title="Accéder au profil" target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" <svg xmlns="http://www.w3.org/2000/svg"
width="35px" width="35px"
height="35px" height="35px"
@ -32,6 +32,25 @@ export function eyeIconLink(url) {
</a>`; </a>`;
} }
export function pencilIcon() {
return `
<span class="align-middle color-primary">
<svg xmlns="http://www.w3.org/2000/svg" width="35px" height="35px" viewBox="0 0 24 24">
<path fill="currentColor" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168zM11.207 2.5L13.5 4.793L14.793 3.5L12.5 1.207zm1.586 3L10.5 3.207L4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293zm-9.761 5.175l-.106.106l-1.528 3.821l3.821-1.528l.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325"/></svg>
</span>
`
}
export function trashIcon(url) {
return `
<span class="align-middle color-delete">
<svg xmlns="http://www.w3.org/2000/svg" width="35px" height="35px" viewBox="0 0 24 24">
<path fill="currentColor" d="M6.5 1h3a.5.5 0 0 1 .5.5v1H6v-1a.5.5 0 0 1 .5-.5M11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3A1.5 1.5 0 0 0 5 1.5v1H1.5a.5.5 0 0 0 0 1h.538l.853 10.66A2 2 0 0 0 4.885 16h6.23a2 2 0 0 0 1.994-1.84l.853-10.66h.538a.5.5 0 0 0 0-1zm1.958 1l-.846 10.58a1 1 0 0 1-.997.92h-6.23a1 1 0 0 1-.997-.92L3.042 3.5zm-7.487 1a.5.5 0 0 1 .528.47l.5 8.5a.5.5 0 0 1-.998.06L5 5.03a.5.5 0 0 1 .47-.53Zm5.058 0a.5.5 0 0 1 .47.53l-.5 8.5a.5.5 0 1 1-.998-.06l.5-8.5a.5.5 0 0 1 .528-.47M8 4.5a.5.5 0 0 1 .5.5v8.5a.5.5 0 0 1-1 0V5a.5.5 0 0 1 .5-.5"/></svg>
</span>
`
}
export function deactivateUserIcon() { export function deactivateUserIcon() {
return `<svg xmlns="http://www.w3.org/2000/svg" width="35" height="35" viewBox="0 0 640 512"> return `<svg xmlns="http://www.w3.org/2000/svg" width="35" height="35" viewBox="0 0 640 512">
<path fill="currentColor" d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2s-6.3 25.5 4.1 33.7l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L353.3 251.6C407.9 237 448 187.2 448 128C448 57.3 390.7 0 320 0c-69.8 0-126.5 55.8-128 125.2zm225.5 299.2C170.5 309.4 96 387.2 96 482.3c0 16.4 13.3 29.7 29.7 29.7h388.6c3.9 0 7.6-.7 11-2.1z"/> <path fill="currentColor" d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2s-6.3 25.5 4.1 33.7l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L353.3 251.6C407.9 237 448 187.2 448 128C448 57.3 390.7 0 320 0c-69.8 0-126.5 55.8-128 125.2zm225.5 299.2C170.5 309.4 96 387.2 96 482.3c0 16.4 13.3 29.7 29.7 29.7h388.6c3.9 0 7.6-.7 11-2.1z"/>

View File

@ -4,10 +4,14 @@
--primary-blue-dark : #094754; --primary-blue-dark : #094754;
--black-font: #1D1E1C; --black-font: #1D1E1C;
--delete : #E42E31; --delete : #E42E31;
--delete-dark : #aa1618;
--disable : #A3A3A3; --disable : #A3A3A3;
--check : #80F20E; --check : #5cae09;
--check-dark: #3a6e05;
--secondary : #cc664c; --secondary : #cc664c;
--secondary-dark : #a5543d; --secondary-dark : #a5543d;
--warning : #d2b200;
--warning-dark: #c4a600;
} }
html { html {
@ -103,12 +107,41 @@ body {
border: var(--primary-blue-light); 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{ .btn-danger{
background: var(--delete); background: var(--delete);
color : #FFFFFF; color : #FFFFFF;
border: var(--delete); border: var(--delete-dark);
border-radius: 1rem; border-radius: 1rem;
} }
.btn-danger:hover{
background: var(--delete-dark);
color : #FFFFFF;
border: var(--delete);
}
.color-primary{ .color-primary{
color: var(--primary-blue-light) !important; color: var(--primary-blue-light) !important;
@ -117,6 +150,13 @@ body {
color: var(--primary-blue-dark); color: var(--primary-blue-dark);
} }
.color-delete{
color: var(--delete) !important;
}
.color-delete-dark{
color: var(--delete-dark);
}
.btn-secondary{ .btn-secondary{
background: var(--secondary); background: var(--secondary);
color : #FFFFFF; color : #FFFFFF;

35
docs/Organization.md Normal file
View File

@ -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

34
docs/Role_Hierarchy.md Normal file
View File

@ -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

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260210131727 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->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');
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260211142643 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->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');
}
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260211145606 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->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');
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260216092531 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->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');
}
}

View File

@ -5,6 +5,7 @@ namespace App\Controller;
use App\Entity\Actions; use App\Entity\Actions;
use App\Entity\Organizations; use App\Entity\Organizations;
use App\Service\ActionService; use App\Service\ActionService;
use App\Service\UserService;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
@ -15,14 +16,15 @@ class ActionController extends AbstractController
{ {
public function __construct( public function __construct(
private EntityManagerInterface $entityManager, 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'])] #[Route('/organization/{id}/activities-ajax', name: 'app_organization_activities_ajax', methods: ['GET'])]
public function fetchActivitiesAjax(Organizations $organization): JsonResponse public function fetchActivitiesAjax(Organizations $organization): JsonResponse
{ {
$this->denyAccessUnlessGranted('ROLE_ADMIN'); $this->denyAccessUnlessGranted('ROLE_USER');
if($this->userService->isAdminOfOrganization($organization) || $this->isGranted('ROLE_ADMIN')) {
$actions = $this->entityManager->getRepository(Actions::class)->findBy( $actions = $this->entityManager->getRepository(Actions::class)->findBy(
['Organization' => $organization], ['Organization' => $organization],
['date' => 'DESC'], ['date' => 'DESC'],
@ -32,4 +34,6 @@ class ActionController extends AbstractController
return new JsonResponse($formattedActivities); return new JsonResponse($formattedActivities);
} }
return new JsonResponse(['error' => 'You are not authorized to access this page.'], 403);
}
} }

View File

@ -51,7 +51,7 @@ class ApplicationController extends AbstractController
#[Route(path: '/edit/{id}', name: 'edit', methods: ['GET', 'POST'])] #[Route(path: '/edit/{id}', name: 'edit', methods: ['GET', 'POST'])]
public function edit(int $id, Request $request): Response{ public function edit(int $id, Request $request): Response{
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN'); $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); $actingUser = $this->getUser();
$application = $this->entityManager->getRepository(Apps::class)->find($id); $application = $this->entityManager->getRepository(Apps::class)->find($id);
if (!$application) { if (!$application) {
$this->loggerService->logEntityNotFound('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'])] #[Route(path:'/user/{id}', name: 'user', methods: ['GET'])]
public function getApplicationUsers(int $id): JSONResponse public function getApplicationUsers(int $id): JSONResponse
{ {
$user = $this->userRepository->find($id); $user = $this->userRepository->find($id);
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); $actingUser = $this->getUser();
if (!$user) { if (!$user) {
$this->loggerService->logEntityNotFound('User', ['message'=> 'User not found for application list'], $actingUser->getId()); $this->loggerService->logEntityNotFound('User', ['message'=> 'User not found for application list'], $actingUser->getId());
return new JsonResponse(['error' => 'User not found'], Response::HTTP_NOT_FOUND); 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); 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);
}
} }

View File

@ -2,6 +2,7 @@
namespace App\Controller; namespace App\Controller;
use App\Service\UserService;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Bundle\SecurityBundle\Security; use Symfony\Bundle\SecurityBundle\Security;
@ -11,10 +12,15 @@ use Symfony\Component\Routing\Attribute\Route;
final class IndexController extends AbstractController final class IndexController extends AbstractController
{ {
public function __construct(private readonly UserService $userService)
{
}
#[Route('/', name: 'app_index')] #[Route('/', name: 'app_index')]
public function index(): Response 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'); return $this->redirectToRoute('organization_index');
} }

View File

@ -20,7 +20,7 @@ class MercureController extends AbstractController
public function getMercureToken(Request $request): JsonResponse public function getMercureToken(Request $request): JsonResponse
{ {
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); $user =$this->getUser();
$domain = $request->getSchemeAndHttpHost(); $domain = $request->getSchemeAndHttpHost();

View File

@ -29,7 +29,7 @@ class NotificationController extends AbstractController
public function index(): JsonResponse public function index(): JsonResponse
{ {
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN'); $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); $user =$this->getUser();
$notifications = $this->notificationRepository->findRecentByUser($user, 50); $notifications = $this->notificationRepository->findRecentByUser($user, 50);
$unreadCount = $this->notificationRepository->countUnreadByUser($user); $unreadCount = $this->notificationRepository->countUnreadByUser($user);
@ -44,7 +44,7 @@ class NotificationController extends AbstractController
public function unread(): JsonResponse public function unread(): JsonResponse
{ {
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); $user =$this->getUser();
$notifications = $this->notificationRepository->findUnreadByUser($user); $notifications = $this->notificationRepository->findUnreadByUser($user);
$unreadCount = count($notifications); $unreadCount = count($notifications);
@ -59,7 +59,7 @@ class NotificationController extends AbstractController
public function count(): JsonResponse public function count(): JsonResponse
{ {
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); $user =$this->getUser();
$unreadCount = $this->notificationRepository->countUnreadByUser($user); $unreadCount = $this->notificationRepository->countUnreadByUser($user);
@ -70,7 +70,7 @@ class NotificationController extends AbstractController
public function markAsRead(int $id): JsonResponse public function markAsRead(int $id): JsonResponse
{ {
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); $user =$this->getUser();
$notification = $this->notificationRepository->find($id); $notification = $this->notificationRepository->find($id);
@ -88,7 +88,7 @@ class NotificationController extends AbstractController
public function markAllAsRead(): JsonResponse public function markAllAsRead(): JsonResponse
{ {
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); $user =$this->getUser();
$count = $this->notificationRepository->markAllAsReadForUser($user); $count = $this->notificationRepository->markAllAsReadForUser($user);
@ -99,7 +99,7 @@ class NotificationController extends AbstractController
public function delete(int $id): JsonResponse public function delete(int $id): JsonResponse
{ {
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); $user =$this->getUser();
$notification = $this->notificationRepository->find($id); $notification = $this->notificationRepository->find($id);

View File

@ -43,35 +43,42 @@ class OrganizationController extends AbstractController
private readonly ActionService $actionService, private readonly ActionService $actionService,
private readonly UserOrganizationService $userOrganizationService, private readonly UserOrganizationService $userOrganizationService,
private readonly OrganizationsRepository $organizationsRepository, 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'])] #[Route(path: '/', name: 'index', methods: ['GET'])]
public function index(): Response public function index(): Response
{ {
$this->denyAccessUnlessGranted('ROLE_ADMIN'); $this->denyAccessUnlessGranted('ROLE_USER');
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); $actingUser = $this->getUser();
if($this->userService->hasAccessTo($actingUser, true)){
$orgCount = $this->organizationsRepository->count(['isDeleted' => false]); // 1. Super Admin Case: Just show the list
if(!$this->isGranted("ROLE_SUPER_ADMIN")){ if ($this->isGranted("ROLE_ADMIN")) {
$userUO = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['users' => $actingUser, 'isActive' => true]); return $this->render('organization/index.html.twig', ['hasOrganizations' => true]);
$uoAdmin = 0;
foreach($userUO as $u){
if($this->userService->isAdminOfOrganization($u->getOrganization())){
$uoAdmin++;
} }
// 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($uoAdmin === 1){
return $this->redirectToRoute('organization_show', ['id' => $userUO[0]->getOrganization()->getId()]); // If multiple orgs, show the list
if (count($orgs) > 1) {
return $this->render('organization/index.html.twig', ['hasOrganizations' => true]);
} }
}
return $this->render('organization/index.html.twig', [ // 3. Fallback: No access/No orgs found
'hasOrganizations' => $orgCount > 0 $this->loggerService->logEntityNotFound('Organization', [
]); 'user_id' => $actingUser->getUserIdentifier(),
} 'message' => 'No admin organizations found'
$this->loggerService->logAccessDenied($actingUser->getId()); ], $actingUser->getUserIdentifier());
throw new AccessDeniedHttpException('Access denied');
$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 public function new(Request $request): Response
{ {
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN'); $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); $actingUser = $this->getUser();
if ($request->isMethod('POST')) { if ($request->isMethod('POST')) {
$organization = new Organizations(); $organization = new Organizations();
$form = $this->createForm(OrganizationForm::class, $organization); $form = $this->createForm(OrganizationForm::class, $organization);
@ -90,16 +97,17 @@ class OrganizationController extends AbstractController
$this->organizationsService->handleLogo($organization, $logoFile); $this->organizationsService->handleLogo($organization, $logoFile);
} }
try { try {
$organization->setProjectPrefix($this->organizationsService->generateUniqueProjectPrefix());
$this->entityManager->persist($organization); $this->entityManager->persist($organization);
$this->entityManager->flush(); $this->entityManager->flush();
$this->loggerService->logOrganizationInformation($organization->getId(), $actingUser->getId(), "Organization Created"); $this->loggerService->logOrganizationInformation($organization->getId(), $actingUser->getUserIdentifier(), "Organization Created");
$this->loggerService->logSuperAdmin($actingUser->getId(), $actingUser->getId(), "Organization Created", $organization->getId()); $this->loggerService->logSuperAdmin($actingUser->getUserIdentifier(), $actingUser->getUserIdentifier(), "Organization Created", $organization->getId());
$this->actionService->createAction("Create Organization", $actingUser, $organization, $organization->getName()); $this->actionService->createAction("Create Organization", $actingUser, $organization, $organization->getName());
$this->addFlash('success', 'Organisation crée avec succès.'); $this->addFlash('success', 'Organisation crée avec succès.');
return $this->redirectToRoute('organization_index'); return $this->redirectToRoute('organization_index');
} catch (Exception $e) { } catch (Exception $e) {
$this->addFlash('error', 'Erreur lors de la création de l\'organization'); $this->addFlash('danger', 'Erreur lors de la création de l\'organization');
$this->loggerService->logError('Error creating organization', ['acting_user_id' => $actingUser->getId(), 'error' => $e->getMessage()]); $this->loggerService->logError('Error creating organization', ['acting_user_id' => $actingUser->getUserIdentifier(), 'error' => $e->getMessage()]);
} }
} }
return $this->render('organization/new.html.twig', [ return $this->render('organization/new.html.twig', [
@ -117,40 +125,17 @@ class OrganizationController extends AbstractController
public function edit(Request $request, $id): Response public function edit(Request $request, $id): Response
{ {
$this->denyAccessUnlessGranted('ROLE_ADMIN'); $this->denyAccessUnlessGranted('ROLE_ADMIN');
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); $actingUser = $this->getUser();
$organization = $this->organizationsRepository->find($id); $organization = $this->organizationsRepository->find($id);
if (!$organization) { if (!$organization) {
$this->loggerService->logEntityNotFound('Organization', [ $this->loggerService->logEntityNotFound('Organization', [
'org_id' => $id, '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'); 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 = $this->createForm(OrganizationForm::class, $organization);
$form->handleRequest($request); $form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
@ -161,16 +146,16 @@ class OrganizationController extends AbstractController
try { try {
$this->entityManager->persist($organization); $this->entityManager->persist($organization);
$this->entityManager->flush(); $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")) { 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->actionService->createAction("Edit Organization", $actingUser, $organization, $organization->getName());
$this->addFlash('success', 'Organisation modifiée avec succès.'); $this->addFlash('success', 'Organisation modifiée avec succès.');
return $this->redirectToRoute('organization_index'); return $this->redirectToRoute('organization_index');
}catch (Exception $e) { }catch (Exception $e) {
$this->addFlash('error', 'Erreur lors de la modification de l\'organization'); $this->addFlash('danger', 'Erreur lors de la modification de l\'organization');
$this->loggerService->logError('Error editing organization', ['acting_user_id' => $actingUser->getId(), 'error' => $e->getMessage()]); $this->loggerService->logError('Error editing organization', ['acting_user_id' => $actingUser->getUserIdentifier(), 'error' => $e->getMessage()]);
} }
} }
return $this->render('organization/edit.html.twig', [ return $this->render('organization/edit.html.twig', [
@ -182,24 +167,25 @@ class OrganizationController extends AbstractController
#[Route(path: '/view/{id}', name: 'show', methods: ['GET'])] #[Route(path: '/view/{id}', name: 'show', methods: ['GET'])]
public function view($id): Response public function view($id): Response
{ {
$this->denyAccessUnlessGranted('ROLE_ADMIN'); $this->denyAccessUnlessGranted('ROLE_USER');
$organization = $this->organizationsRepository->find($id); $organization = $this->organizationsRepository->find($id);
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); $actingUser = $this->getUser();
if (!$organization) { if (!$organization) {
$this->loggerService->logEntityNotFound('Organization', [ $this->loggerService->logEntityNotFound('Organization', [
'org_id' => $id, 'org_id' => $id,
'message' => 'Organization not found for view' 'message' => 'Organization not found for view'
], $actingUser->getId()); ], $actingUser->getUserIdentifier());
$this->addFlash('error', 'Erreur, l\'organization est introuvable.'); $this->addFlash('danger', 'Erreur, l\'organization est introuvable.');
return $this->redirectToRoute('organization_index'); return $this->redirectToRoute('organization_index');
} }
//check if the user is admin of the organization //check if the user is admin of the organization
if (!$this->userService->isAdminOfOrganization($organization) && !$this->isGranted("ROLE_SUPER_ADMIN")) { if (!$this->userService->isAdminOfOrganization($organization) && !$this->isGranted("ROLE_ADMIN")) {
$this->loggerService->logAccessDenied($actingUser->getId()); $this->loggerService->logAccessDenied($actingUser->getUserIdentifier());
$this->addFlash('error', 'Erreur, accès refusé.'); $this->addFlash('danger', 'Erreur, accès refusé.');
throw new AccessDeniedHttpException('Access denied'); throw new AccessDeniedHttpException('Access denied');
} }
//TODO: add project to the response
$allApps = $this->entityManager->getRepository(Apps::class)->findAll(); // appsAll $allApps = $this->entityManager->getRepository(Apps::class)->findAll(); // appsAll
$orgApps = $organization->getApps()->toArray(); // apps $orgApps = $organization->getApps()->toArray(); // apps
@ -219,15 +205,15 @@ class OrganizationController extends AbstractController
#[Route(path: '/delete/{id}', name: 'delete', methods: ['POST'])] #[Route(path: '/delete/{id}', name: 'delete', methods: ['POST'])]
public function delete($id): Response public function delete($id): Response
{ {
$this->denyAccessUnlessGranted("ROLE_ADMIN"); $this->denyAccessUnlessGranted("ROLE_SUPER_ADMIN");
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); $actingUser = $this->getUser();
$organization = $this->organizationsRepository->find($id); $organization = $this->organizationsRepository->find($id);
if (!$organization) { if (!$organization) {
$this->loggerService->logEntityNotFound('Organization', [ $this->loggerService->logEntityNotFound('Organization', [
'org_id' => $id, 'org_id' => $id,
'message' => 'Organization not found for delete' 'message' => 'Organization not found for delete'
], $actingUser->getId()); ], $actingUser->getUserIdentifier());
$this->addFlash('error', 'Erreur, l\'organization est introuvable.'); $this->addFlash('danger', 'Erreur, l\'organization est introuvable.');
throw $this->createNotFoundException(self::NOT_FOUND); throw $this->createNotFoundException(self::NOT_FOUND);
} }
try { try {
@ -240,14 +226,14 @@ class OrganizationController extends AbstractController
$this->entityManager->persist($organization); $this->entityManager->persist($organization);
$this->actionService->createAction("Delete Organization", $actingUser, $organization, $organization->getName()); $this->actionService->createAction("Delete Organization", $actingUser, $organization, $organization->getName());
$this->entityManager->flush(); $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")) { 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.'); $this->addFlash('success', 'Organisation supprimée avec succès.');
}catch (\Exception $e){ }catch (\Exception $e){
$this->loggerService->logError($actingUser->getId(), ['message' => 'Error deleting organization: '.$e->getMessage()]); $this->loggerService->logError($actingUser->getUserIdentifier(), ['message' => 'Error deleting organization: '.$e->getMessage()]);
$this->addFlash('error', 'Erreur lors de la suppression de l\'organization.'); $this->addFlash('danger', 'Erreur lors de la suppression de l\'organization.');
} }
return $this->redirectToRoute('organization_index'); return $this->redirectToRoute('organization_index');
@ -257,22 +243,21 @@ class OrganizationController extends AbstractController
public function deactivate($id): Response public function deactivate($id): Response
{ {
$this->denyAccessUnlessGranted("ROLE_SUPER_ADMIN"); $this->denyAccessUnlessGranted("ROLE_SUPER_ADMIN");
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); $actingUser = $this->getUser();
$organization = $this->organizationsRepository->find($id); $organization = $this->organizationsRepository->find($id);
if (!$organization) { if (!$organization) {
$this->loggerService->logEntityNotFound('Organization', [ $this->loggerService->logEntityNotFound('Organization', [
'org_id' => $id, 'org_id' => $id,
'message' => 'Organization not found for deactivate' 'message' => 'Organization not found for deactivate'
], $actingUser->getId()); ], $actingUser->getUserIdentifier());
$this->addFlash('error', 'Erreur, l\'organization est introuvable.'); $this->addFlash('danger', 'Erreur, l\'organization est introuvable.');
throw $this->createNotFoundException(self::NOT_FOUND); throw $this->createNotFoundException(self::NOT_FOUND);
} }
$organization->setIsActive(false); $organization->setIsActive(false);
// $this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, null, $organization);
$this->entityManager->persist($organization); $this->entityManager->persist($organization);
$this->actionService->createAction("Deactivate Organization", $actingUser, $organization, $organization->getName()); $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.'); $this->addFlash('success', 'Organisation désactivé avec succès.');
return $this->redirectToRoute('organization_index'); return $this->redirectToRoute('organization_index');
} }
@ -281,20 +266,20 @@ class OrganizationController extends AbstractController
public function activate($id): Response public function activate($id): Response
{ {
$this->denyAccessUnlessGranted("ROLE_SUPER_ADMIN"); $this->denyAccessUnlessGranted("ROLE_SUPER_ADMIN");
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); $actingUser = $this->getUser();
$organization = $this->organizationsRepository->find($id); $organization = $this->organizationsRepository->find($id);
if (!$organization) { if (!$organization) {
$this->loggerService->logEntityNotFound('Organization', [ $this->loggerService->logEntityNotFound('Organization', [
'org_id' => $id, 'org_id' => $id,
'message' => 'Organization not found for activate' 'message' => 'Organization not found for activate'
], $actingUser->getId()); ], $actingUser->getUserIdentifier());
$this->addFlash('error', 'Erreur, l\'organization est introuvable.'); $this->addFlash('danger', 'Erreur, l\'organization est introuvable.');
throw $this->createNotFoundException(self::NOT_FOUND); throw $this->createNotFoundException(self::NOT_FOUND);
} }
$organization->setIsActive(true); $organization->setIsActive(true);
$this->entityManager->persist($organization); $this->entityManager->persist($organization);
$this->loggerService->logOrganizationInformation($organization->getId(), $actingUser->getId(),'Organization Activated'); $this->loggerService->logOrganizationInformation($organization->getId(), $actingUser->getUserIdentifier(),'Organization Activated');
$this->loggerService->logSuperAdmin($actingUser->getId(), $actingUser->getId(),'Organization Activated', $organization->getId()); $this->loggerService->logSuperAdmin($actingUser->getUserIdentifier(), $actingUser->getUserIdentifier(),'Organization Activated', $organization->getId());
$this->actionService->createAction("Activate Organization", $actingUser, $organization, $organization->getName()); $this->actionService->createAction("Activate Organization", $actingUser, $organization, $organization->getName());
$this->addFlash('success', 'Organisation activée avec succès.'); $this->addFlash('success', 'Organisation activée avec succès.');
return $this->redirectToRoute('organization_index'); return $this->redirectToRoute('organization_index');
@ -304,56 +289,23 @@ class OrganizationController extends AbstractController
#[Route(path: '/data', name: 'data', methods: ['GET'])] #[Route(path: '/data', name: 'data', methods: ['GET'])]
public function data(Request $request): JsonResponse public function data(Request $request): JsonResponse
{ {
$this->denyAccessUnlessGranted('ROLE_ADMIN'); $this->denyAccessUnlessGranted('ROLE_USER');
$page = max(1, (int)$request->query->get('page', 1));
$size = max(1, (int)$request->query->get('size', 10));
$page = max(1, $request->query->getInt('page', 1));
$size = max(1, $request->query->getInt('size', 10));
$filters = $request->query->all('filter'); $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') $total = count($paginator);
->where('o.isDeleted = :del')->setParameter('del', false);
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) { $data = array_map(function (Organizations $org) {
return [ return [
'id' => $org->getId(), 'id' => $org->getId(),
@ -363,17 +315,12 @@ class OrganizationController extends AbstractController
'active' => $org->isActive(), 'active' => $org->isActive(),
'showUrl' => $this->generateUrl('organization_show', ['id' => $org->getId()]), 'showUrl' => $this->generateUrl('organization_show', ['id' => $org->getId()]),
]; ];
}, $rows); }, iterator_to_array($paginator));
$lastPage = (int)ceil($total / $size);
return $this->json([ return $this->json([
'data' => $data, 'data' => $data,
'last_page' => $lastPage, 'last_page' => (int)ceil($total / $size),
'total' => $total, // optional, useful for debugging 'total' => $total,
]); ]);
} }
} }

View File

@ -0,0 +1,162 @@
<?php
namespace App\Controller;
use App\Entity\Apps;
use App\Entity\Project;
use App\Repository\AppsRepository;
use App\Repository\OrganizationsRepository;
use App\Repository\ProjectRepository;
use App\Service\ProjectService;
use App\Service\UserService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Asset\Packages;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
#[Route(path: '/project', name: 'project_')]
final class ProjectController extends AbstractController
{
public function __construct(private readonly EntityManagerInterface $entityManager,
private readonly OrganizationsRepository $organizationsRepository,
private readonly ProjectRepository $projectRepository,
private readonly ProjectService $projectService,
private readonly UserService $userService, private readonly AppsRepository $appsRepository)
{
}
#[Route('/', name: '_index', methods: ['GET'])]
public function index(): Response
{
return $this->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);
}
}

View File

@ -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'])] #[Route('/view/{id}', name: 'show', methods: ['GET'])]
public function view(int $id, Request $request): Response public function view(int $id, Request $request): Response
@ -70,128 +79,42 @@ class UserController extends AbstractController
$this->denyAccessUnlessGranted('ROLE_USER'); $this->denyAccessUnlessGranted('ROLE_USER');
// Utilisateur courant (acting user) via UserService // Utilisateur courant (acting user) via UserService
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); $actingUser = $this->getUser();
// Vérification des droits d'accès supplémentaires
// Chargement de l'utilisateur cible à afficher // Chargement de l'utilisateur cible à afficher
$user = $this->userRepository->find($id); $user = $this->userRepository->find($id);
if (!$user) { if (!$user) {
$this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getId()); $this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getUserIdentifier());
$this->addFlash('error', "L'utilisateur demandé n'existe pas."); $this->addFlash('danger', "L'utilisateur demandé n'existe pas.");
throw $this->createNotFoundException(self::NOT_FOUND); throw $this->createNotFoundException(self::NOT_FOUND);
} }
if (!$this->userService->hasAccessTo($user)) { //if hasAccessTo is false, turn to true and deny access
$this->loggerService->logAccessDenied($actingUser->getId()); if (!$this->userService->isAdminOfUser($user) && !$this->isGranted('ROLE_ADMIN')) {
$this->addFlash('error', "L'utilisateur demandé n'existe pas."); $this->loggerService->logAccessDenied($actingUser->getUserIdentifier());
$this->addFlash('danger', "Vous n'avez pas accès à cette information.");
throw new AccessDeniedHttpException (self::ACCESS_DENIED); throw new AccessDeniedHttpException (self::ACCESS_DENIED);
} }
try { try {
// Paramètre optionnel de contexte organisationnel // Paramètre optionnel de contexte organisationnel
$orgId = $request->query->get('organizationId'); $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) { if ($orgId) {
// Contexte organisation précis : récupérer l'organisation et les liens UO // TODO: afficher les projets de l'organisation
$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();
} else { } else {
// Pas de contexte org : récupérer toutes les UO actives de l'utilisateur // Afficher tous les projets 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,
]);
} }
}
// 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) { } catch (\Exception $e) {
$this->loggerService->logError('error while loading user information', [ $this->loggerService->logError('error while loading user information', [
'target_user_id' => $id, 'target_user_id' => $id,
'acting_user_id' => $actingUser->getId(), 'acting_user_id' => $actingUser->getUserIdentifier(),
'error' => $e->getMessage(), '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'); $referer = $request->headers->get('referer');
return $this->redirect($referer ?? $this->generateUrl('app_index')); return $this->redirect($referer ?? $this->generateUrl('app_index'));
} }
return $this->render('user/show.html.twig', [ return $this->render('user/show.html.twig', [
'user' => $user, 'user' => $user,
'organizationId' => $orgId ?? null, '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 public function edit(int $id, Request $request): Response
{ {
$this->denyAccessUnlessGranted('ROLE_USER'); $this->denyAccessUnlessGranted('ROLE_USER');
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); $actingUser = $this->getUser();
$user = $this->userRepository->find($id); $user = $this->userRepository->find($id);
if (!$user) { if (!$user) {
$this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getId()); $this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getUserIdentifier());
$this->addFlash('error', "L'utilisateur demandé n'existe pas."); $this->addFlash('danger', "L'utilisateur demandé n'existe pas.");
throw $this->createNotFoundException(self::NOT_FOUND); throw $this->createNotFoundException(self::NOT_FOUND);
} }
try { try {
if ($this->userService->hasAccessTo($user)) { if ($this->userService->isAdminOfUser($user)) {
$form = $this->createForm(UserForm::class, $user); $form = $this->createForm(UserForm::class, $user);
$form->handleRequest($request); $form->handleRequest($request);
@ -221,31 +144,31 @@ class UserController extends AbstractController
$this->entityManager->flush(); $this->entityManager->flush();
//log and action //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'); $orgId = $request->get('organizationId');
if ($orgId) { if ($orgId) {
$org = $this->organizationRepository->find($orgId); $org = $this->organizationRepository->find($orgId);
if ($org) { if ($org) {
$this->actionService->createAction("Edit user information", $actingUser, $org, $user->getUserIdentifier()); $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')) { if ($this->isGranted('ROLE_SUPER_ADMIN')) {
$this->loggerService->logSuperAdmin( $this->loggerService->logSuperAdmin(
$user->getId(), $user->getId(),
$actingUser->getId(), $actingUser->getUserIdentifier(),
"Super Admin accessed user edit page", "Super Admin accessed user edit page",
); );
} }
$this->addFlash('success', 'Information modifié avec success.'); $this->addFlash('success', 'Information modifié avec success.');
return $this->redirectToRoute('user_show', ['id' => $user->getId(), 'organizationId' => $orgId]); return $this->redirectToRoute('user_show', ['id' => $user->getId(), 'organizationId' => $orgId]);
} }
$this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getId()); $this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getUserIdentifier());
$this->addFlash('error', "L'organisation n'existe pas."); $this->addFlash('danger', "L'organisation n'existe pas.");
throw $this->createNotFoundException(self::NOT_FOUND); throw $this->createNotFoundException(self::NOT_FOUND);
} }
if ($this->isGranted('ROLE_SUPER_ADMIN')) { if ($this->isGranted('ROLE_SUPER_ADMIN')) {
$this->loggerService->logSuperAdmin( $this->loggerService->logSuperAdmin(
$user->getId(), $user->getId(),
$actingUser->getId(), $actingUser->getUserIdentifier(),
"Super Admin accessed user edit page", "Super Admin accessed user edit page",
); );
} }
@ -260,11 +183,11 @@ class UserController extends AbstractController
'organizationId' => $request->get('organizationId') 'organizationId' => $request->get('organizationId')
]); ]);
} }
$this->loggerService->logAccessDenied($actingUser->getId()); $this->loggerService->logAccessDenied($actingUser->getUserIdentifier());
$this->addFlash('error', "Accès non autorisé."); $this->addFlash('danger', "Accès non autorisé.");
throw $this->createAccessDeniedException(self::ACCESS_DENIED); throw $this->createAccessDeniedException(self::ACCESS_DENIED);
} catch (\Exception $e) { } 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()); $this->errorLogger->critical($e->getMessage());
} }
// Default deny access. shouldn't reach here normally. // Default deny access. shouldn't reach here normally.
@ -275,52 +198,47 @@ class UserController extends AbstractController
#[Route('/new', name: 'new', methods: ['GET', 'POST'])] #[Route('/new', name: 'new', methods: ['GET', 'POST'])]
public function new(Request $request): Response public function new(Request $request): Response
{ {
$this->denyAccessUnlessGranted('ROLE_ADMIN'); $this->denyAccessUnlessGranted('ROLE_USER');
try { try {
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); $actingUser =$this->getUser();
if (!$this->userService->hasAccessTo($actingUser)) {
$this->loggerService->logAccessDenied($actingUser->getId());
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
}
$user = new User(); $user = new User();
$form = $this->createForm(UserForm::class, $user); $form = $this->createForm(UserForm::class, $user);
$form->handleRequest($request); $form->handleRequest($request);
$orgId = $request->get('organizationId'); $orgId = $request->query->get('organizationId') ?? $request->request->get('organizationId');
if ($orgId) { if ($orgId) {
$org = $this->organizationRepository->find($orgId); $org = $this->organizationRepository->find($orgId);
if (!$org) { if (!$org) {
$this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getId()); $this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getUserIdentifier());
$this->addFlash('error', "L'organisation n'existe pas."); $this->addFlash('danger', "L'organisation n'existe pas.");
throw $this->createNotFoundException(self::NOT_FOUND); throw $this->createNotFoundException(self::NOT_FOUND);
} }
if ($this->isGranted('ROLE_ADMIN') && !$this->userService->isAdminOfOrganization($org) && !$this->isGranted('ROLE_SUPER_ADMIN')) { if (!$this->isGranted('ROLE_ADMIN') && !$this->userService->isAdminOfOrganization($org)) {
$this->loggerService->logAccessDenied($actingUser->getId()); $this->loggerService->logAccessDenied($actingUser->getUserIdentifier());
$this->addFlash('error', "Accès non autorisé."); $this->addFlash('danger', "Accès non autorisé.");
throw $this->createAccessDeniedException(self::ACCESS_DENIED); throw $this->createAccessDeniedException(self::ACCESS_DENIED);
} }
} elseif ($this->isGranted('ROLE_ADMIN')) { } else{
$this->loggerService->logAccessDenied($actingUser->getId()); $this->loggerService->logAccessDenied($actingUser->getUserIdentifier());
$this->addFlash('error', "Accès non autorisé."); $this->addFlash('danger', "Accès non autorisé.");
throw $this->createAccessDeniedException(self::ACCESS_DENIED); throw $this->createAccessDeniedException(self::ACCESS_DENIED);
} }
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
$existingUser = $this->userRepository->findOneBy(['email' => $user->getEmail()]); $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) { if ($existingUser && $org) {
$this->userService->addExistingUserToOrganization( $this->userService->addExistingUserToOrganization(
$existingUser, $existingUser,
$org, $org,
$actingUser,
); );
if ($this->isGranted('ROLE_SUPER_ADMIN')) { if ($this->isGranted('ROLE_ADMIN')) {
$this->loggerService->logSuperAdmin( $this->loggerService->logSuperAdmin(
$existingUser->getId(), $existingUser->getId(),
$actingUser->getId(), $actingUser->getUserIdentifier(),
"Super Admin linked user to organization", "Super Admin linked user to organization",
$org->getId(), $org->getId(),
); );
@ -329,62 +247,18 @@ class UserController extends AbstractController
return $this->redirectToRoute('organization_show', ['id' => $orgId]); return $this->redirectToRoute('organization_show', ['id' => $orgId]);
} }
//Code semi-mort : On ne peut plus créer un utilisateur sans organisation // Case : user doesn't already exist
// 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,
// ]);
// }
$picture = $form->get('pictureUrl')->getData(); $picture = $form->get('pictureUrl')->getData();
$this->userService->createNewUser($user, $actingUser, $picture); $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( $this->userService->linkUserToOrganization(
$user, $user,
$org, $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. '); $this->addFlash('success', 'Nouvel utilisateur créé et ajouté à l\'organisation avec succès. ');
return $this->redirectToRoute('organization_show', ['id' => $orgId]); return $this->redirectToRoute('organization_show', ['id' => $orgId]);
} }
$this->addFlash('success', 'Nouvel utilisateur créé avec succès. ');
return $this->redirectToRoute('user_index');
}
return $this->render('user/new.html.twig', [ return $this->render('user/new.html.twig', [
'user' => $user, 'user' => $user,
@ -396,38 +270,32 @@ class UserController extends AbstractController
$this->errorLogger->critical($e->getMessage()); $this->errorLogger->critical($e->getMessage());
if ($orgId) { 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]); 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'); 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 public function activeStatus(int $id, Request $request): JsonResponse
{ {
$this->denyAccessUnlessGranted('ROLE_ADMIN'); $this->denyAccessUnlessGranted('ROLE_ADMIN');
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); $actingUser =$this->getUser();
$status = $request->get('status'); $status = $request->request->get('status');
try { 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); $user = $this->userRepository->find($id);
if (!$user) { if (!$user) {
$this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getId()); $this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getUserIdentifier());
throw $this->createNotFoundException(self::NOT_FOUND); throw $this->createNotFoundException(self::NOT_FOUND);
} }
// Deactivate
if ($status === 'deactivate') { if ($status === 'deactivate') {
$user->setIsActive(false); $user->setIsActive(false);
@ -440,12 +308,12 @@ class UserController extends AbstractController
$user->setModifiedAt(new \DateTimeImmutable('now')); $user->setModifiedAt(new \DateTimeImmutable('now'));
$this->entityManager->persist($user); $this->entityManager->persist($user);
$this->entityManager->flush(); $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')) { if ($this->isGranted('ROLE_SUPER_ADMIN')) {
$this->loggerService->logSuperAdmin( $this->loggerService->logSuperAdmin(
$user->getId(), $user->getId(),
$actingUser->getId(), $actingUser->getUserIdentifier(),
'Super admin deactivated user' 'Super admin deactivated user'
); );
} }
@ -461,13 +329,13 @@ class UserController extends AbstractController
$user->setModifiedAt(new \DateTimeImmutable('now')); $user->setModifiedAt(new \DateTimeImmutable('now'));
$this->entityManager->persist($user); $this->entityManager->persist($user);
$this->entityManager->flush(); $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')) { if ($this->isGranted('ROLE_SUPER_ADMIN')) {
$this->loggerService->logSuperAdmin( $this->loggerService->logSuperAdmin(
$user->getId(), $user->getId(),
$actingUser->getId(), $actingUser->getUserIdentifier(),
'Super admin activated user' '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 public function activateStatusOrganization(int $id, Request $request): JsonResponse
{ {
$this->denyAccessUnlessGranted('ROLE_ADMIN'); $this->denyAccessUnlessGranted('ROLE_USER');
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); $actingUser = $this->getUser();
try { try {
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);
}
$user = $this->userRepository->find($id); $user = $this->userRepository->find($id);
if (!$user) { if (!$user) {
$this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getId()); $this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getUserIdentifier());
throw $this->createNotFoundException(self::NOT_FOUND); 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->getUserIdentifier());
throw $this->createNotFoundException(self::NOT_FOUND);
}
$uo = $this->uoRepository->findOneBy(['users' => $user, $uo = $this->uoRepository->findOneBy(['users' => $user,
'organization' => $org]); 'organization' => $org]);
if (!$uo) { if (!$uo) {
$this->loggerService->logEntityNotFound('UsersOrganization', ['user_id' => $user->getId(), $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); throw $this->createNotFoundException(self::NOT_FOUND);
} }
$status = $request->get('status'); $status = $request->request->get('status');
if ($status === 'deactivate') { if ($status === 'deactivate') {
$uo->setIsActive(false); $uo->setIsActive(false);
$this->userOrganizationAppService->deactivateAllUserOrganizationsAppLinks($uo); $this->userOrganizationAppService->deactivateAllUserOrganizationsAppLinks($uo);
@ -532,7 +401,7 @@ class UserController extends AbstractController
$data = ['user' => $user, $data = ['user' => $user,
'organization' => $org]; 'organization' => $org];
$this->organizationsService->notifyOrganizationAdmins($data, "USER_DEACTIVATED"); $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()); $this->actionService->createAction("Deactivate user in organization", $actingUser, $org, $org->getName() . " for user " . $user->getUserIdentifier());
return new JsonResponse(['status' => 'deactivated'], Response::HTTP_OK); return new JsonResponse(['status' => 'deactivated'], Response::HTTP_OK);
} }
@ -540,7 +409,7 @@ class UserController extends AbstractController
$uo->setIsActive(true); $uo->setIsActive(true);
$this->entityManager->persist($uo); $this->entityManager->persist($uo);
$this->entityManager->flush(); $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()); $this->actionService->createAction("Activate user in organization", $actingUser, $org, $org->getName() . " for user " . $user->getUserIdentifier());
$data = ['user' => $user, $data = ['user' => $user,
'organization' => $org]; 'organization' => $org];
@ -566,14 +435,14 @@ class UserController extends AbstractController
{ {
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN'); $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); $actingUser = $this->getUser();
try { try {
$user = $this->userRepository->find($id); $user = $this->userRepository->find($id);
if (!$user) { if (!$user) {
// Security/audit log for missing user // Security/audit log for missing user
$this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getId()); $this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getUserIdentifier());
$this->addFlash('error', "L'utilisateur demandé n'existe pas."); $this->addFlash('danger', "L'utilisateur demandé n'existe pas.");
throw $this->createNotFoundException(self::NOT_FOUND); throw $this->createNotFoundException(self::NOT_FOUND);
} }
@ -585,7 +454,7 @@ class UserController extends AbstractController
$user->setModifiedAt(new \DateTimeImmutable('now')); $user->setModifiedAt(new \DateTimeImmutable('now'));
// Deactivate all org links // Deactivate all org links
$this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, $user); $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 // Revoke tokens if connected
if ($this->userService->isUserConnected($user->getUserIdentifier())) { if ($this->userService->isUserConnected($user->getUserIdentifier())) {
@ -595,13 +464,13 @@ class UserController extends AbstractController
$this->entityManager->flush(); $this->entityManager->flush();
// User management log // 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) // Super admin log (standardized style)
if ($this->isGranted('ROLE_SUPER_ADMIN')) { if ($this->isGranted('ROLE_SUPER_ADMIN')) {
$this->loggerService->logSuperAdmin( $this->loggerService->logSuperAdmin(
$user->getId(), $user->getId(),
$actingUser->getId(), $actingUser->getUserIdentifier(),
'Super admin deleted user' 'Super admin deleted user'
); );
} }
@ -636,68 +505,11 @@ class UserController extends AbstractController
if ($e instanceof NotFoundHttpException) { if ($e instanceof NotFoundHttpException) {
throw $e; // keep 404 semantics 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'); 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 * AJAX endpoint for user listing with pagination
* Get all the users that aren´t deleted and are active * Get all the users that aren´t deleted and are active
@ -707,40 +519,14 @@ class UserController extends AbstractController
{ {
$this->denyAccessUnlessGranted("ROLE_ADMIN"); $this->denyAccessUnlessGranted("ROLE_ADMIN");
$page = max(1, (int)$request->query->get('page', 1)); $page = max(1, $request->query->getInt('page', 1));
$size = max(1, (int)$request->query->get('size', 10)); $size = max(1, $request->query->getInt('size', 10));
// Get filter parameters
$filters = $request->query->all('filter', []); $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) { $data = array_map(function (User $user) {
return [ return [
'id' => $user->getId(), 'id' => $user->getId(),
@ -752,33 +538,15 @@ class UserController extends AbstractController
'showUrl' => $this->generateUrl('user_show', ['id' => $user->getId()]), 'showUrl' => $this->generateUrl('user_show', ['id' => $user->getId()]),
'statut' => $user->isActive(), 'statut' => $user->isActive(),
]; ];
}, $rows); }, iterator_to_array($paginator));
$lastPage = (int)ceil($total / $size);
return $this->json([ return $this->json([
'data' => $data, 'data' => $data,
'last_page' => $lastPage, 'last_page' => (int)ceil($total / $size),
'total' => $total, '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 * AJAX endpoint for new users listing
* Get the 5 most recently created users for an organization * 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'])] #[Route(path: '/data/new', name: 'dataNew', methods: ['GET'])]
public function dataNew(Request $request): JsonResponse public function dataNew(Request $request): JsonResponse
{ {
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); $actingUser = $this->getUser();
if ($this->userService->hasAccessTo($actingUser, true) && $this->isGranted("ROLE_ADMIN")) {
$orgId = $request->query->get('orgId'); $orgId = $request->query->get('orgId');
$uos = $this->uoRepository->findBy(['organization' => $orgId, 'statut' => ["ACCEPTED", "INVITED"]], $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); orderBy: ['createdAt' => 'DESC'], limit: 5);
@ -822,10 +596,15 @@ class UserController extends AbstractController
#[Route(path: '/data/admin', name: 'dataAdmin', methods: ['GET'])] #[Route(path: '/data/admin', name: 'dataAdmin', methods: ['GET'])]
public function dataAdmin(Request $request): JsonResponse public function dataAdmin(Request $request): JsonResponse
{ {
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); $actingUser = $this->getUser();
if ($this->userService->hasAccessTo($actingUser, true) && $this->isGranted("ROLE_ADMIN")) {
$orgId = $request->query->get('orgId'); $orgId = $request->query->get('orgId');
$uos = $this->uoRepository->findBy(['organization' => $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']); $roleAdmin = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN']);
$users = []; $users = [];
foreach ($uos as $uo) { foreach ($uos as $uo) {
@ -863,76 +642,54 @@ class UserController extends AbstractController
#[Route(path: '/data/organization', name: 'dataUserOrganization', methods: ['GET'])] #[Route(path: '/data/organization', name: 'dataUserOrganization', methods: ['GET'])]
public function dataUserOrganization(Request $request): JsonResponse public function dataUserOrganization(Request $request): JsonResponse
{ {
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); $actingUser = $this->getUser();
if ($this->userService->hasAccessTo($actingUser, true) && $this->isGranted("ROLE_ADMIN")) {
$orgId = $request->query->get('orgId'); $orgId = $request->query->get('orgId');
$page = max(1, (int)$request->query->get('page', 1));
$size = max(1, (int)$request->query->get('size', 10));
$org = $this->organizationRepository->find($orgId);
if (!$org) {
$this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getUserIdentifier());
throw $this->createNotFoundException(self::NOT_FOUND);
}
// 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') ?? []; $filters = $request->query->all('filter') ?? [];
$repo = $this->uoRepository; // Get paginated results from Repository
$paginator = $this->uoRepository->findByOrganizationWithFilters($org, $page, $size, $filters);
$total = count($paginator);
// Base query // Format the data using your existing service method
$qb = $repo->createQueryBuilder('uo') $data = $this->userService->formatStatutForOrganizations(iterator_to_array($paginator));
->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([ return $this->json([
'data' => $data, 'data' => $data,
'last_page' => $lastPage, 'last_page' => (int)ceil($total / $size),
'total' => $total, 'total' => $total,
]); ]);
} }
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
}
#[Route(path: '/organization/resend-invitation/{userId}', name: 'resend_invitation', methods: ['POST'])] #[Route(path: '/organization/resend-invitation/{userId}', name: 'resend_invitation', methods: ['POST'])]
public function resendInvitation(int $userId, Request $request): JsonResponse public function resendInvitation(int $userId, Request $request): JsonResponse
{ {
$this->denyAccessUnlessGranted("ROLE_ADMIN"); $this->denyAccessUnlessGranted("ROLE_USER");
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); $actingUser = $this->getUser();
if ($this->userService->hasAccessTo($actingUser, true)) { $orgId = $request->request->get('organizationId');
$orgId = $request->get('organizationId');
$org = $this->organizationRepository->find($orgId); $org = $this->organizationRepository->find($orgId);
if (!$org) { if (!$org) {
$this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getId()); $this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getUserIdentifier());
throw $this->createNotFoundException(self::NOT_FOUND); throw $this->createNotFoundException(self::NOT_FOUND);
} }
if ($this->userService->isAdminOfOrganization($org)) {
$user = $this->userRepository->find($userId); $user = $this->userRepository->find($userId);
if (!$user) { 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); throw $this->createNotFoundException(self::NOT_FOUND);
} }
$token = $this->userService->generatePasswordToken($user, $org->getId()); $token = $this->userService->generatePasswordToken($user, $org->getId());
@ -945,7 +702,7 @@ class UserController extends AbstractController
if (!$uo) { if (!$uo) {
$this->loggerService->logEntityNotFound('UsersOrganization', [ $this->loggerService->logEntityNotFound('UsersOrganization', [
'user_id' => $user->getId(), 'user_id' => $user->getId(),
'organization_id' => $orgId], $actingUser->getId()); 'organization_id' => $orgId], $actingUser->getUserIdentifier());
throw $this->createNotFoundException(self::NOT_FOUND); throw $this->createNotFoundException(self::NOT_FOUND);
} }
$uo->setModifiedAt(new \DateTimeImmutable()); $uo->setModifiedAt(new \DateTimeImmutable());
@ -959,7 +716,7 @@ class UserController extends AbstractController
$this->loggerService->logCritical('Error while resending invitation', [ $this->loggerService->logCritical('Error while resending invitation', [
'target_user_id' => $user->getId(), 'target_user_id' => $user->getId(),
'organization_id' => $orgId, 'organization_id' => $orgId,
'acting_user_id' => $actingUser->getId(), 'acting_user_id' => $actingUser->getUserIdentifier(),
'error' => $e->getMessage(), 'error' => $e->getMessage(),
]); ]);
return $this->json(['message' => 'Erreur lors de l\'envoie du mail.'], Response::HTTP_INTERNAL_SERVER_ERROR); 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'])] #[Route(path: '/accept-invitation', name: 'accept', methods: ['GET'])]
public function acceptInvitation(Request $request): Response public function acceptInvitation(Request $request): Response
{ {
$token = $request->get('token'); $token = $request->query->get('token');
$userId = $request->get('id'); $userId = $request->query->get('id');
if (!$token || !$userId) { if (!$token || !$userId) {
$this->loggerService->logEntityNotFound('Token or UserId missing in accept invitation', [ $this->loggerService->logEntityNotFound('Token or UserId missing in accept invitation', [

View File

@ -61,12 +61,22 @@ class Organizations
#[ORM\OneToMany(targetEntity: UserOrganizatonApp::class, mappedBy: 'organization')] #[ORM\OneToMany(targetEntity: UserOrganizatonApp::class, mappedBy: 'organization')]
private Collection $userOrganizatonApps; private Collection $userOrganizatonApps;
#[ORM\Column(length: 4, nullable: true)]
private ?string $projectPrefix = null;
/**
* @var Collection<int, Project>
*/
#[ORM\OneToMany(targetEntity: Project::class, mappedBy: 'organization')]
private Collection $projects;
public function __construct() public function __construct()
{ {
$this->apps = new ArrayCollection(); $this->apps = new ArrayCollection();
$this->actions = new ArrayCollection(); $this->actions = new ArrayCollection();
$this->createdAt = new \DateTimeImmutable(); $this->createdAt = new \DateTimeImmutable();
$this->userOrganizatonApps = new ArrayCollection(); $this->userOrganizatonApps = new ArrayCollection();
$this->projects = new ArrayCollection();
} }
public function getId(): ?int public function getId(): ?int
@ -256,4 +266,46 @@ class Organizations
return $this; return $this;
} }
public function getProjectPrefix(): ?string
{
return $this->projectPrefix;
}
public function setProjectPrefix(?string $projectPrefix): static
{
$this->projectPrefix = $projectPrefix;
return $this;
}
/**
* @return Collection<int, Project>
*/
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;
}
} }

149
src/Entity/Project.php Normal file
View File

@ -0,0 +1,149 @@
<?php
namespace App\Entity;
use App\Repository\ProjectRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: ProjectRepository::class)]
class Project
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $name = null;
#[ORM\ManyToOne(inversedBy: 'projects')]
#[ORM\JoinColumn(nullable: false)]
private ?Organizations $organization = null;
#[ORM\Column(nullable: true)]
private ?array $applications = null;
#[ORM\Column]
private ?\DateTimeImmutable $createdAt = null;
#[ORM\Column]
private ?\DateTimeImmutable $modifiedAt = null;
#[ORM\Column]
private ?bool $isActive = null;
#[ORM\Column]
private ?bool $isDeleted = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $bddName = null;
public function __construct()
{
$this->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;
}
}

View File

@ -41,6 +41,9 @@ class UsersOrganizations
#[ORM\Column(nullable: true)] #[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $modifiedAt = null; private ?\DateTimeImmutable $modifiedAt = null;
#[ORM\ManyToOne]
private ?Roles $role = null;
public function __construct() public function __construct()
{ {
$this->isActive = true; // Default value for isActive $this->isActive = true; // Default value for isActive
@ -147,4 +150,16 @@ class UsersOrganizations
return $this; return $this;
} }
public function getRole(): ?Roles
{
return $this->role;
}
public function setRole(?Roles $role): static
{
$this->role = $role;
return $this;
}
} }

View File

@ -30,26 +30,21 @@ class UserSubscriber implements EventSubscriberInterface
$user = $event->getNewUser(); $user = $event->getNewUser();
$actingUser = $event->getActingUser(); $actingUser = $event->getActingUser();
// 1. Generate Token (If logic was moved here, otherwise assume UserService set it) // 1. Send Email
// 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.
if ($user->getPasswordToken()) { if ($user->getPasswordToken()) {
$this->emailService->sendPasswordSetupEmail($user, $user->getPasswordToken()); $this->emailService->sendPasswordSetupEmail($user, $user->getPasswordToken());
} }
// 3. Log the creation // 2. Logic-based Logging (Moved from Service)
$this->loggerService->logUserCreated($user->getId(), $actingUser->getId()); if (in_array('ROLE_ADMIN', $actingUser->getRoles(), true)) {
$this->loggerService->logSuperAdmin(
// 4. Create the Audit Action $user->getId(),
$this->actionService->createAction( $actingUser->getId(),
"Create new user", "Super Admin created new user: " . $user->getUserIdentifier()
$actingUser,
null,
$user->getUserIdentifier()
); );
} }
// 3. General Audit Trail
$this->actionService->createAction("USER_CREATED", $actingUser, null, $user->getUserIdentifier());
}
} }

View File

@ -3,8 +3,11 @@
namespace App\Repository; namespace App\Repository;
use App\Entity\Organizations; use App\Entity\Organizations;
use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\Tools\Pagination\Paginator;
use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\ManagerRegistry;
use App\Entity\UsersOrganizations;
/** /**
* @extends ServiceEntityRepository<Organizations> * @extends ServiceEntityRepository<Organizations>
@ -16,28 +19,37 @@ class OrganizationsRepository extends ServiceEntityRepository
parent::__construct($registry, Organizations::class); parent::__construct($registry, Organizations::class);
} }
// /** public function findAdmissibleOrganizations(User $user, bool $isSuperAdmin, int $page, int $size, array $filters = []): Paginator
// * @return Organizations[] Returns an array of Organizations objects {
// */ $qb = $this->createQueryBuilder('o')
// public function findByExampleField($value): array ->where('o.isDeleted = :del')
// { ->setParameter('del', false);
// return $this->createQueryBuilder('o')
// ->andWhere('o.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('o.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?Organizations // 1. Security Logic: If not Super Admin, join UsersOrganizations to filter
// { if (!$isSuperAdmin) {
// return $this->createQueryBuilder('o') $qb->innerJoin(UsersOrganizations::class, 'uo', 'WITH', 'uo.organization = o')
// ->andWhere('o.exampleField = :val') ->andWhere('uo.users = :user')
// ->setParameter('val', $value) ->andWhere('uo.role = :roleAdmin')
// ->getQuery() ->andWhere('uo.isActive = true')
// ->getOneOrNullResult() ->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);
}
} }

View File

@ -0,0 +1,61 @@
<?php
namespace App\Repository;
use App\Entity\Project;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Project>
*/
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();
}
}

View File

@ -4,11 +4,11 @@ namespace App\Repository;
use App\Entity\User; use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\Tools\Pagination\Paginator;
use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException; use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
use App\Entity\UsersOrganizations;
/** /**
* @extends ServiceEntityRepository<User> * @extends ServiceEntityRepository<User>
@ -35,21 +35,33 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader
} }
/** /**
* Returns active users that are NOT in any UsersOrganizations mapping. * @param int $page
* Returns User entities. * @param int $size
* * @param array $filters
* @return User[] * @return Paginator
*/ */
public function findUsersWithoutOrganization(): array public function findActiveUsersForTabulator(int $page, int $size, array $filters = []): Paginator
{ {
$qb = $this->createQueryBuilder('u') $qb = $this->createQueryBuilder('u')
->select('u') ->where('u.isDeleted = :del')
->leftJoin(UsersOrganizations::class, 'uo', 'WITH', 'uo.users = u') ->setParameter('del', false);
->andWhere('u.isDeleted = :uDeleted')
->andWhere('uo.id IS NULL')
->orderBy('u.surname', 'ASC')
->setParameter('uDeleted', 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);
} }
} }

View File

@ -6,8 +6,10 @@ use App\Entity\Organizations;
use App\Entity\User; use App\Entity\User;
use App\Entity\UsersOrganizations; use App\Entity\UsersOrganizations;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\Tools\Pagination\Paginator;
use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\ManagerRegistry;
/** /**
* @extends ServiceEntityRepository<UsersOrganizations> * @extends ServiceEntityRepository<UsersOrganizations>
*/ */
@ -19,121 +21,64 @@ class UsersOrganizationsRepository extends ServiceEntityRepository
} }
/** /**
* Returns active user-organization mappings with joined User and Organization. * Checks if an acting user has administrative rights over a target user
* Only active and non-deleted users and organizations are included. * based on shared organizational memberships.
*
* @return UsersOrganizations[]
*/ */
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') $qb = $this->createQueryBuilder('uo')
->addSelect('u', 'o') ->innerJoin('uo.users', 'u')
->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);
}
return $qb->getQuery()->getResult();
}
/**
* 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 [];
}
$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') ->where('uo.organization = :org')
->andWhere('uo.isActive = :uoActive') ->setParameter('org', $org);
->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(); // 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'] . '%');
} }
/** // Apply complex sorting
* Find all the admins of an Organization. $qb->orderBy('uo.isActive', 'DESC')
* limited to 10 results. ->addOrderBy("CASE WHEN uo.statut = 'INVITED' THEN 0 ELSE 1 END", 'ASC');
*
* @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(); // Pagination
$qb->setFirstResult(($page - 1) * $size)
->setMaxResults($size);
return new Paginator($qb);
} }
} }

View File

@ -23,7 +23,7 @@ readonly class LoggerService
// User Management Logs // 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", [ $this->userManagementLogger->notice("New user created: $userId", [
'target_user_id' => $userId, 'target_user_id' => $userId,
@ -34,7 +34,7 @@ readonly class LoggerService
} }
// Organization Management Logs // 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', [ $this->organizationManagementLogger->notice('User-Organization link created', [
'target_user_id' => $userId, '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', [ $this->organizationManagementLogger->notice('Existing user added to organization', [
'target_user_id' => $userId, 'target_user_id' => $userId,
@ -59,7 +59,7 @@ readonly class LoggerService
} }
// Email Notification Logs // 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, [ $this->emailNotificationLogger->notice($message, [
'target_user_id' => $userId, '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", [ $this->emailNotificationLogger->notice("Existing user notification email sent to $userId", [
'target_user_id' => $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, [ $this->adminActionsLogger->notice($message, [
'target_user_id' => $userId, 'target_user_id' => $userId,
@ -116,7 +116,7 @@ readonly class LoggerService
} }
// Security Logs // Security Logs
public function logAccessDenied(?int $actingUserId): void public function logAccessDenied(int|string $actingUserId): void
{ {
$this->securityLogger->warning('Access denied', [ $this->securityLogger->warning('Access denied', [
'acting_user_id' => $actingUserId, '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, [ $this->userManagementLogger->notice($message, [
'target_user_id'=> $targetId, '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, [ $this->adminActionsLogger->notice($message, [
'target_id' => $targetId, '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, [ $this->errorLogger->error('Entity not found', array_merge($criteria, [
'entity_type' => $entityType, '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, [ $this->organizationManagementLogger->info($message, [
'organization_id' => $organizationId, '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, [ $this->accessControlLogger->info($message, [
'target_user_id' => $userId, '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, [ $this->accessControlLogger->info($string, array_merge($array, [
'acting_user_id' => $actingUser, 'acting_user_id' => $actingUser,

View File

@ -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);
}
} }

View File

@ -0,0 +1,43 @@
<?php
namespace App\Service;
use App\Repository\AppsRepository;
use Symfony\Component\String\Slugger\AsciiSlugger;
class ProjectService{
public function __construct(private readonly AppsRepository $appsRepository)
{
}
/** Function that will return the project name.
* Project name are build using the project prefix field present in the organization entity and the normalized project name.
* The normalized project name is the project name with all spaces replaced by underscores and all characters in lowercase.
* For example, if the project prefix is "yumi" and the project name is "My Project", the project name will be "yumi_my_project".
*
* @param string $projectName The name of the project.
* @param string $projectPrefix The prefix of the project.
* @return string The project name.
*/
public function getProjectDbName(string $projectName, string $projectPrefix): string
{
$slugger = new AsciiSlugger();
$slug = $slugger->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;
}
}

View File

@ -6,6 +6,7 @@ use App\Entity\Actions;
use App\Entity\Organizations; use App\Entity\Organizations;
use App\Entity\User; use App\Entity\User;
use App\Entity\UsersOrganizations; use App\Entity\UsersOrganizations;
use App\Repository\RolesRepository;
use App\Service\ActionService; use App\Service\ActionService;
use App\Service\LoggerService; use App\Service\LoggerService;
use \App\Service\UserOrganizationAppService; use \App\Service\UserOrganizationAppService;
@ -20,7 +21,7 @@ readonly class UserOrganizationService
{ {
public function __construct( 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;
}
} }

View File

@ -8,6 +8,7 @@ use App\Entity\Roles;
use App\Entity\User; use App\Entity\User;
use App\Entity\UserOrganizatonApp; use App\Entity\UserOrganizatonApp;
use App\Entity\UsersOrganizations; use App\Entity\UsersOrganizations;
use App\Repository\RolesRepository;
use DateTimeImmutable; use DateTimeImmutable;
use DateTimeZone; use DateTimeZone;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
@ -33,7 +34,7 @@ class UserService
private readonly ActionService $actionService, private readonly ActionService $actionService,
private readonly EmailService $emailService, private readonly EmailService $emailService,
private readonly OrganizationsService $organizationsService, 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)); 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. * Check if the user is currently connected.
@ -75,26 +93,30 @@ class UserService
} }
/** /**
* Check if the user have the rights to access the page * Determines if the currently logged-in user has permission to manage or view a target User.
* Self check can be skipped when checking access for the current 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 User $user The target User object we are checking access against.
* @param bool $skipSelfCheck * * @return bool True if access is permitted, false otherwise.
* @return bool * @throws Exception If database or security context issues occur.
* @throws Exception
*/ */
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; 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; return true;
} }
$userOrganizations = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['users' => $user]); $userOrganizations = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['users' => $user]);
if ($userOrganizations) { if ($userOrganizations) {
foreach ($userOrganizations as $uo) { 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; 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 * Check if the acting 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 * A user is considered an admin of an organization if they have an active UsersOrganizations link with the role of ADMIN for that organization.
* entity role 'ROLE_ADMIN' in the UsersOrganizationsApp entity
* (if he is admin for any application of the organization).
* *
* @param Organizations $organizations * @param Organizations $organizations
* @return bool * @return bool
@ -115,20 +162,15 @@ class UserService
*/ */
public function isAdminOfOrganization(Organizations $organizations): bool public function isAdminOfOrganization(Organizations $organizations): bool
{ {
$actingUser = $this->getUserByIdentifier($this->security->getUser()->getUserIdentifier()); $actingUser =$this->security->getUser();
$uo = $this->entityManager->getRepository(UsersOrganizations::class)->findOneBy(['users' => $actingUser, 'organization' => $organizations]);
$roleAdmin = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN']); $roleAdmin = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN']);
if ($uo) { $uoAdmin = $this->entityManager->getRepository(UsersOrganizations::class)->findOneBy(['users' => $actingUser,
$uoa = $this->entityManager->getRepository(UserOrganizatonApp::class)->findOneBy(['userOrganization' => $uo, 'organization' => $organizations,
'role'=> $roleAdmin, 'role'=> $roleAdmin,
'isActive' => true]); 'isActive' => true]);
if ($uoa && $this->security->isGranted('ROLE_ADMIN')) { return $uoAdmin !== null;
return true;
}
}
return false;
}
}
/** /**
* Get the user by their identifier. * Get the user by their identifier.
@ -499,14 +541,7 @@ class UserService
$user->setIsActive(true); $user->setIsActive(true);
$this->entityManager->persist($user); $this->entityManager->persist($user);
} }
$uo = new UsersOrganizations(); $uo = $this->linkUserToOrganization($user, $organization);
$uo->setUsers($user);
$uo->setOrganization($organization);
$uo->setStatut("INVITED");
$uo->setIsActive(false);
$uo->setModifiedAt(new \DateTimeImmutable('now'));
$this->entityManager->persist($uo);
$this->entityManager->flush();
return $uo->getId(); return $uo->getId();
} }
@ -554,12 +589,11 @@ class UserService
public function addExistingUserToOrganization( public function addExistingUserToOrganization(
User $existingUser, User $existingUser,
Organizations $org, Organizations $org,
User $actingUser,
): int ): int
{ {
try { try {
$uoId = $this->handleExistingUser($existingUser, $org); $uoId = $this->handleExistingUser($existingUser, $org);
$actingUser = $this->getUserByIdentifier($this->security->getUser()->getUserIdentifier());
$this->loggerService->logExistingUserAddedToOrg( $this->loggerService->logExistingUserAddedToOrg(
$existingUser->getId(), $existingUser->getId(),
$org->getId(), $org->getId(),
@ -593,20 +627,16 @@ class UserService
try { try {
$this->formatUserData($user, $picture, true); $this->formatUserData($user, $picture, true);
// Generate token here if it's part of the user persistence flow
$token = $this->generatePasswordToken($user);
$user->setisActive(false); $user->setisActive(false);
$this->generatePasswordToken($user); // Set token on the entity
$this->entityManager->persist($user); $this->entityManager->persist($user);
$this->entityManager->flush(); $this->entityManager->flush();
$this->eventDispatcher->dispatch(new UserCreatedEvent($user, $actingUser)); $this->eventDispatcher->dispatch(new UserCreatedEvent($user, $actingUser));
} catch (\Exception $e) { } catch (\Exception $e) {
// Error logging remains here because the event won't fire if exception occurs $this->loggerService->logError('Error creating user: ' . $e->getMessage());
$this->loggerService->logError('Error creating new user: ' . $e->getMessage(), [
'target_user_email' => $user->getEmail(),
'acting_user_id' => $actingUser->getId(),
]);
throw $e; throw $e;
} }
} }
@ -617,15 +647,17 @@ class UserService
public function linkUserToOrganization( public function linkUserToOrganization(
User $user, User $user,
Organizations $org, Organizations $org,
User $actingUser,
): UsersOrganizations ): UsersOrganizations
{ {
$actingUser = $this->getUserByIdentifier($this->security->getUser()->getUserIdentifier());
try { try {
$roleUser = $this->rolesRepository->findOneBy(['name' => 'USER']);
$uo = new UsersOrganizations(); $uo = new UsersOrganizations();
$uo->setUsers($user); $uo->setUsers($user);
$uo->setOrganization($org); $uo->setOrganization($org);
$uo->setStatut("INVITED"); $uo->setStatut("INVITED");
$uo->setIsActive(false); $uo->setIsActive(false);
$uo->setRole($roleUser);
$uo->setModifiedAt(new \DateTimeImmutable('now')); $uo->setModifiedAt(new \DateTimeImmutable('now'));
$this->entityManager->persist($uo); $this->entityManager->persist($uo);
$this->entityManager->flush(); $this->entityManager->flush();
@ -644,6 +676,15 @@ class UserService
$org, $org,
"Added {$user->getUserIdentifier()} to {$org->getName()}" "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); $this->sendNewUserNotifications($user, $org, $actingUser);

View File

@ -0,0 +1,41 @@
<?php
namespace App\Twig;
use App\Service\UserService;
use Symfony\Bundle\SecurityBundle\Security;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
class MenuExtension extends AbstractExtension
{
public function __construct(
private readonly UserService $userService,
private readonly Security $security
) {}
public function getFunctions(): array
{
return [
// We create a new Twig function called 'can_view_org_menu'
new TwigFunction('can_view_org_menu', [$this, 'canViewOrgMenu']),
];
}
public function canViewOrgMenu(): bool
{
$user = $this->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);
}
}

View File

@ -3,7 +3,7 @@
{% set current_route = app.request.attributes.get('_route') %} {% set current_route = app.request.attributes.get('_route') %}
<ul class="nav"> <ul class="nav">
{% if is_granted("ROLE_SUPER_ADMIN") %} {% if is_granted("ROLE_ADMIN") %}
{# 2. Check if route is 'app_index' #} {# 2. Check if route is 'app_index' #}
<li class="nav-item {{ current_route == 'app_index' ? 'active' : '' }}"> <li class="nav-item {{ current_route == 'app_index' ? 'active' : '' }}">
<a class="nav-link" href="{{ path('app_index') }}"> <a class="nav-link" href="{{ path('app_index') }}">
@ -21,7 +21,7 @@
</a> </a>
</li> </li>
{% if is_granted('ROLE_SUPER_ADMIN') %} {% if is_granted('ROLE_ADMIN') %}
<li class="nav-item {{ current_route starts with 'user_' ? 'active' : '' }}"> <li class="nav-item {{ current_route starts with 'user_' ? 'active' : '' }}">
<a class="nav-link" href="{{ path('user_index') }}"> <a class="nav-link" href="{{ path('user_index') }}">
<i class="icon-grid menu-icon">{{ ux_icon('fa6-regular:circle-user', {height: '15px', width: '15px'}) }}</i> <i class="icon-grid menu-icon">{{ ux_icon('fa6-regular:circle-user', {height: '15px', width: '15px'}) }}</i>
@ -30,13 +30,13 @@
</li> </li>
{% endif %} {% endif %}
{% if can_view_org_menu() %}
<li class="nav-item {{ current_route starts with 'organization_' ? 'active' : '' }}"> <li class="nav-item {{ current_route starts with 'organization_' ? 'active' : '' }}">
{% if is_granted('ROLE_ADMIN') %}
<a class="nav-link" href="{{ path('organization_index') }}"> <a class="nav-link" href="{{ path('organization_index') }}">
<i class="icon-grid menu-icon"> {{ ux_icon('bi:buildings', {height: '15px', width: '15px'}) }}</i> <i class="icon-grid menu-icon"> {{ ux_icon('bi:buildings', {height: '15px', width: '15px'}) }}</i>
<span class="menu-title">Organizations</span> <span class="menu-title">Organizations</span>
</a> </a>
{% endif %}
</li> </li>
{% endif %}
</ul> </ul>
</nav> </nav>

View File

@ -1,6 +1,7 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% block body %} {% block body %}
{% set isSA = is_granted('ROLE_SUPER_ADMIN')%}
<div class="w-100 h-100 p-5 m-auto"> <div class="w-100 h-100 p-5 m-auto">
{% for type, messages in app.flashes %} {% for type, messages in app.flashes %}
{% for message in messages %} {% for message in messages %}
@ -18,7 +19,7 @@
<h1 class="mb-4 ms-3">{{ organization.name|title }} - Dashboard</h1> <h1 class="mb-4 ms-3">{{ organization.name|title }} - Dashboard</h1>
</div> </div>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
{% if is_granted("ROLE_SUPER_ADMIN") %} {% if isSA %}
<a href="{{ path('organization_edit', {'id': organization.id}) }}" class="btn btn-primary">Gérer <a href="{{ path('organization_edit', {'id': organization.id}) }}" class="btn btn-primary">Gérer
l'organisation</a> l'organisation</a>
<form method="POST" action="{{ path('organization_delete', {'id': organization.id}) }}" <form method="POST" action="{{ path('organization_delete', {'id': organization.id}) }}"
@ -101,20 +102,61 @@
</div> </div>
{# APPLICATION ROW #} {# APPLICATION ROW #}
{# TODO: Weird gap not going away #} {# TODO:remove app acces and replace wioth project overview#}
<div class="row mb-3 "> <div class="row mb-3 card no-header-bg"
{% for application in applications %} data-controller="project"
<div class="col-6 mb-3"> data-project-list-project-value="true"
{% include 'application/appSmall.html.twig' with { data-project-org-id-value="{{ organization.id }}"
application: application data-project-admin-value="{{ isSA ? 'true' : 'false' }}">
} %} <div class="card-header d-flex justify-content-between align-items-center">
<h2>Mes Projets</h2>
{% if is_granted("ROLE_SUPER_ADMIN") %}
{# Trigger for the Modal #}
<button type="button" class="btn btn-primary" data-action="click->project#openCreateModal">
Crée un projet
</button>
{% endif %}
</div>
<div class="card-body">
<div id="tabulator-projectListOrganization">
</div>
</div>
<div class="modal fade" id="createProjectModal" tabindex="-1" aria-hidden="true" data-project-target="modal">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" data-project-target="formTitle">Nouveau Projet</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form data-action="submit->project#submitForm">
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Nom du projet</label>
<input type="text" name="name"
data-project-target="nameInput"
class="form-control" required>
</div>
<label class="form-label">Applications</label>
<div class="row" data-project-target="appList">
{# Checkboxes will be injected here #}
<div class="text-center p-3">Chargement des applications...</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
<button type="submit" class="btn btn-primary">Enregistrer</button>
</div>
</form>
</div>
</div>
</div> </div>
{% endfor %}
</div> </div>
</div> </div>
{# Activities col #} {# Activities col #}
<div class="col-3 m-auto"> <div class="col-3 m-auto">
<div class="card border-0" <div class="card "
data-controller="organization" data-controller="organization"
data-organization-activities-value = "true" data-organization-activities-value = "true"
data-organization-id-value="{{ organization.id }}"> data-organization-id-value="{{ organization.id }}">
@ -122,7 +164,7 @@
<div class="card-header d-flex justify-content-between align-items-center border-0"> <div class="card-header d-flex justify-content-between align-items-center border-0">
<h3>Activité récente</h3> <h3>Activité récente</h3>
<button class="btn btn-sm btn-outline-secondary" data-action="organization#loadActivities"> <button class="btn btn-sm btn-primary" data-action="organization#loadActivities">
<i class="fas fa-sync"></i> Rafraîchir <i class="fas fa-sync"></i> Rafraîchir
</button> </button>
</div> </div>
@ -148,6 +190,7 @@
{% endblock %} {% endblock %}

View File

@ -0,0 +1,5 @@
{% extends 'base.html.twig' %}
{% block body %}
{% endblock %}

View File

@ -3,7 +3,7 @@
{% block title %}User Profile{% endblock %} {% block title %}User Profile{% endblock %}
{% block body %} {% block body %}
{% if is_granted('ROLE_SUPER_ADMIN') %} {% if is_granted('ROLE_ADMIN') %}
<div class="w-100 h-100 p-5 m-auto"> <div class="w-100 h-100 p-5 m-auto">
{% for type, messages in app.flashes %} {% for type, messages in app.flashes %}
{% for message in messages %} {% for message in messages %}
@ -40,7 +40,7 @@
{% else %} {% else %}
<div class="w-100 h-100 p-5 m-auto"> <div class="w-100 h-100 p-5 m-auto">
<div class="alert alert-warning"> <div class="alert alert-danger">
<h4>Accès limité</h4> <h4>Accès limité</h4>
<p>Vous n'avez pas les permissions nécessaires pour voir la liste des utilisateurs.</p> <p>Vous n'avez pas les permissions nécessaires pour voir la liste des utilisateurs.</p>
</div> </div>

View File

@ -3,7 +3,7 @@
{% block body %} {% block body %}
<div class="w-100 h-100 p-5 m-auto"> <div class="w-100 h-100 p-5 m-auto">
<div class="card p-3 m-3 border-0 no-header-bg"> <div class="card p-3 m-3 border-0 no-header-bg" data-controller="user" data-user-id-value="{{ user.id }}">
{% for type, messages in app.flashes %} {% for type, messages in app.flashes %}
{% for message in messages %} {% for message in messages %}
<div class="alert alert-{{ type }}"> <div class="alert alert-{{ type }}">
@ -11,20 +11,22 @@
</div> </div>
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
{% if is_granted("ROLE_ADMIN") %}
<div class="card-header border-0 d-flex justify-content-between align-items-center "> <div class="card-header border-0 d-flex justify-content-between align-items-center ">
<div class="card-title"> <div class="card-title"><h1>Gestion Utilisateur</h1></div>
<h1>Gestion Utilisateur</h1>
</div>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
{% if is_granted("ROLE_SUPER_ADMIN") %} {% if is_granted("ROLE_ADMIN") %}
<a href="{{ path('user_delete', {'id': user.id}) }}" <button
class="btn btn-secondary">Supprimer</a> class="btn {{ user.active ? 'btn-secondary' : 'btn-success' }}"
data-user-target="statusButton"
data-action="click->user#toggleStatus"
data-active="{{ user.active ? 'true' : 'false' }}">
{{ user.active ? 'Désactiver' : 'Réactiver' }}
</button>
<a href="{{ path('user_delete', {'id': user.id}) }}" class="btn btn-warning">Supprimer</a>
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% endif %}
<div class="card-body"> <div class="card-body">
@ -34,102 +36,106 @@
<div class="card border-0 no-header-bg "> <div class="card border-0 no-header-bg ">
<div class="card-header"> <div class="card-header">
<div class="card-title"> <div class="card-title">
<h1>Vos applications</h1> <h1>Information d'organisation</h1>
</div> </div>
</div> </div>
<div class="card-body ms-4">
{# TODO: dynamic number of project#}
<p><b>Projet : </b>69 projets vous sont attribués</p>
</div>
<div class="card-body"> {# <div class="card-body">#}
<div class="row g-2"> {# <div class="row g-2">#}
{% for app in apps %} {# {% for app in apps %}#}
<div class="col-12 col-md-6"> {# <div class="col-12 col-md-6">#}
<div class="card h-100"> {# <div class="card h-100">#}
<div class="card-header d-flex gap-2"> {# <div class="card-header d-flex gap-2">#}
{% if app.logoMiniUrl %} {# {% if app.logoMiniUrl %}#}
<img src="{{ asset(application.entity.logoMiniUrl) }}" alt="Logo {{ app.name }}" {# <img src="{{ asset(appli.entity.logoMiniUrl) }}" alt="Logo {{ app.name }}"#}
class="rounded-circle" style="width:50px; height:50px;"> {# class="rounded-circle" style="width:50px; height:50px;">#}
{% endif %} {# {% endif %}#}
<div class="card-title"> {# <div class="card-title">#}
<h1>{{ app.name|title }}</h1> {# <h1>{{ app.name|title }}</h1>#}
</div> {# </div>#}
</div> {# </div>#}
<div class="card-body"> {# <div class="card-body">#}
<div class="row"> {# <div class="row">#}
<p> {# <p>#}
<b>Description :</b> {# <b>Description :</b>#}
{{ app.descriptionSmall|default('Aucune description disponible.')|raw }} {# {{ app.descriptionSmall|default('Aucune description disponible.')|raw }}#}
</p> {# </p>#}
</div> {# </div>#}
{# find appGroup once, used in both editable and read-only branches #} {# #}{# find appGroup once, used in both editable and read-only branches #}
{% set appGroup = data.uoas[app.id]|default(null) %} {# {% set appGroup = data.uoas[app.id]|default(null) %}#}
{% if canEdit %} {# {% if canEdit %}#}
<form method="POST" {# <form method="POST"#}
action="{{ path('user_application_role', { id: data.singleUo.id }) }}"> {# action="{{ path('user_application_role', { id: data.singleUo.id }) }}">#}
<div class="form-group mb-3"> {# <div class="form-group mb-3">#}
<label for="roles-{{ app.id }}"><b>Rôles :</b></label> {# <label for="roles-{{ app.id }}"><b>Rôles :</b></label>#}
<div class="form-check"> {# <div class="form-check">#}
{% if appGroup %} {# {% if appGroup %}#}
{% for role in data.rolesArray %} {# {% for role in data.rolesArray %}#}
<input class="form-check-input" type="checkbox" {# <input class="form-check-input" type="checkbox"#}
name="roles[]" {# name="roles[]"#}
value="{{ role.id }}" {# value="{{ role.id }}"#}
id="role-{{ role.id }}-app-{{ app.id }}" {# id="role-{{ role.id }}-app-{{ app.id }}"#}
{% if role.id in appGroup.selectedRoleIds %}checked{% endif %}> {# {% if role.id in appGroup.selectedRoleIds %}checked{% endif %}>#}
<label class="form-check" {# <label class="form-check"#}
for="role-{{ role.id }}-app-{{ app.id }}"> {# for="role-{{ role.id }}-app-{{ app.id }}">#}
{% if role.name == 'USER' %} {# {% if role.name == 'USER' %}#}
Accès {# Accès#}
{% else %} {# {% else %}#}
{{ role.name|capitalize }} {# {{ role.name|capitalize }}#}
{% endif %} {# {% endif %}#}
</label> {# </label>#}
{% endfor %} {# {% endfor %}#}
{% else %} {# {% else %}#}
<p class="text-muted">Aucun rôle défini pour cette application.</p> {# <p class="text-muted">Aucun rôle défini pour cette application.</p>#}
{% endif %} {# {% endif %}#}
</div> {# </div>#}
<button type="submit" name="appId" value="{{ app.id }}" {# <button type="submit" name="appId" value="{{ app.id }}"#}
class="btn btn-primary mt-2"> {# class="btn btn-primary mt-2">#}
Sauvegarder {# Sauvegarder#}
</button> {# </button>#}
</div> {# </div>#}
</form> {# </form>#}
{% else %} {# {% else %}#}
<div class="form-group mb-3"> {# <div class="form-group mb-3">#}
<label for="roles-{{ app.id }}"><b>Rôles :</b></label> {# <label for="roles-{{ app.id }}"><b>Rôles :</b></label>#}
<div class="form-check"> {# <div class="form-check">#}
{% if appGroup %} {# {% if appGroup %}#}
{% for role in data.rolesArray %} {# {% for role in data.rolesArray %}#}
<input class="form-check-input" type="checkbox" {# <input class="form-check-input" type="checkbox"#}
disabled {# disabled#}
name="roles[]" {# name="roles[]"#}
value="{{ role.id }}" {# value="{{ role.id }}"#}
id="role-{{ role.id }}-app-{{ app.id }}" {# id="role-{{ role.id }}-app-{{ app.id }}"#}
{% if role.id in appGroup.selectedRoleIds %}checked{% endif %}> {# {% if role.id in appGroup.selectedRoleIds %}checked{% endif %}>#}
<label class="form-check" {# <label class="form-check"#}
for="role-{{ role.id }}-app-{{ app.id }}"> {# for="role-{{ role.id }}-app-{{ app.id }}">#}
{% if role.name == 'USER' %} {# {% if role.name == 'USER' %}#}
Accès {# Accès#}
{% else %} {# {% else %}#}
{{ role.name|capitalize }} {# {{ role.name|capitalize }}#}
{% endif %} {# {% endif %}#}
</label> {# </label>#}
{% endfor %} {# {% endfor %}#}
{% else %} {# {% else %}#}
<p class="text-muted">Aucun rôle défini pour cette application.</p> {# <p class="text-muted">Aucun rôle défini pour cette application.</p>#}
{% endif %} {# {% endif %}#}
</div> {# </div>#}
</div> {# </div>#}
{% endif %} {# {% endif %}#}
</div> {# </div>#}
</div> {# </div>#}
</div> {# </div>#}
{% endfor %} {# {% endfor %}#}
</div> {# </div>#}
</div> {# </div>#}
</div> </div>

View File

@ -12,18 +12,13 @@
</div> </div>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
{% if canEdit %}
<a href="{{ path('user_edit', {'id': user.id, 'organizationId': organizationId}) }}"
class="btn btn-primary">Modifier</a>
{% elseif user.id == app.user.id or is_granted("ROLE_SUPER_ADMIN") %}
<a href="{{ path('user_edit', {'id': user.id}) }}" <a href="{{ path('user_edit', {'id': user.id}) }}"
class="btn btn-primary">Modifier</a> class="btn btn-primary">Modifier</a>
{% endif %}
</div> </div>
</div> </div>
<div class="card-body "> <div class="card-body ms-4">
<p><b>Email: </b>{{ user.email }}</p> <p><b>Email: </b>{{ user.email }}</p>
<p><b>Dernière connection: </b>{{ user.lastConnection|date('d/m/Y') }} <p><b>Dernière connection: </b>{{ user.lastConnection|date('d/m/Y') }}
à {{ user.lastConnection|date('H:m:s') }} </p> à {{ user.lastConnection|date('H:m:s') }} </p>