Merge branch 'doc' into 'develop'
Doc See merge request easy-solutions/apps/easyportal!31
This commit is contained in:
commit
056325bcf3
|
|
@ -18,7 +18,7 @@ export default class extends Controller {
|
|||
this.loadActivities();
|
||||
setInterval(() => {
|
||||
this.loadActivities();
|
||||
}, 60000); // Refresh every 60 seconds
|
||||
}, 300000); // Refresh every 5 minutes
|
||||
}
|
||||
if (this.tableValue && this.sadminValue) {
|
||||
this.table();
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ export default class extends Controller {
|
|||
orgId: Number
|
||||
}
|
||||
|
||||
static targets = ["select"];
|
||||
static targets = ["select", "statusButton"];
|
||||
|
||||
connect() {
|
||||
this.roleSelect();
|
||||
|
|
@ -180,7 +180,7 @@ export default class extends Controller {
|
|||
const isActive = Boolean(statut);
|
||||
|
||||
const actionClass = isActive ? 'deactivate-user' : 'activate-user';
|
||||
const actionTitle = isActive ? 'Désactiver' : 'Réactiver';
|
||||
const actionTitle = isActive ? 'Désactiver l\'utilisateur': 'Réactiver l\'utilisateur';
|
||||
const actionColorClass = isActive ? 'color-secondary' : 'color-primary';
|
||||
|
||||
// SVGs
|
||||
|
|
@ -678,7 +678,7 @@ export default class extends Controller {
|
|||
const isActive = (statut === "ACTIVE");
|
||||
|
||||
const actionClass = isActive ? 'deactivate-user' : 'activate-user';
|
||||
const actionTitle = isActive ? 'Désactiver' : 'Réactiver';
|
||||
const actionTitle = isActive ? 'Désactiver l\'utilisateur': 'Réactiver l\'utilisateur' ;
|
||||
const actionColorClass = isActive ? 'color-secondary' : 'color-primary';
|
||||
|
||||
// SVGs
|
||||
|
|
@ -874,4 +874,48 @@ export default class extends Controller {
|
|||
columns
|
||||
});
|
||||
};
|
||||
|
||||
async toggleStatus(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const button = this.statusButtonTarget;
|
||||
const isActive = button.dataset.active === "true";
|
||||
const newStatus = isActive ? 'deactivate' : 'activate';
|
||||
const confirmMsg = isActive ? "Désactiver cet utilisateur ?" : "Réactiver cet utilisateur ?";
|
||||
|
||||
if (!confirm(confirmMsg)) return;
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('status', newStatus);
|
||||
|
||||
const response = await fetch(`/user/activeStatus/${this.idValue}`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {'X-Requested-With': 'XMLHttpRequest'}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
this.updateButtonUI(!isActive);
|
||||
} else {
|
||||
alert('Erreur lors de la mise à jour');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Erreur de connexion');
|
||||
}
|
||||
}
|
||||
|
||||
updateButtonUI(nowActive) {
|
||||
const btn = this.statusButtonTarget;
|
||||
|
||||
if (nowActive) {
|
||||
btn.textContent = "Désactiver";
|
||||
btn.classList.replace("btn-success", "btn-secondary");
|
||||
btn.dataset.active = "true";
|
||||
} else {
|
||||
btn.textContent = "Réactiver";
|
||||
btn.classList.replace("btn-secondary", "btn-success");
|
||||
btn.dataset.active = "false";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ export const TABULATOR_FR_LANG = {
|
|||
};
|
||||
|
||||
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"
|
||||
width="35px"
|
||||
height="35px"
|
||||
|
|
@ -32,6 +32,25 @@ export function eyeIconLink(url) {
|
|||
</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() {
|
||||
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"/>
|
||||
|
|
|
|||
|
|
@ -4,10 +4,14 @@
|
|||
--primary-blue-dark : #094754;
|
||||
--black-font: #1D1E1C;
|
||||
--delete : #E42E31;
|
||||
--delete-dark : #aa1618;
|
||||
--disable : #A3A3A3;
|
||||
--check : #80F20E;
|
||||
--check : #5cae09;
|
||||
--check-dark: #3a6e05;
|
||||
--secondary : #cc664c;
|
||||
--secondary-dark : #a5543d;
|
||||
--warning : #d2b200;
|
||||
--warning-dark: #c4a600;
|
||||
}
|
||||
|
||||
html {
|
||||
|
|
@ -103,12 +107,41 @@ body {
|
|||
border: var(--primary-blue-light);
|
||||
}
|
||||
|
||||
.btn-success{
|
||||
background: var(--check);
|
||||
color : #FFFFFF;
|
||||
border: var(--check-dark);
|
||||
border-radius: 1rem;
|
||||
}
|
||||
.btn-success:hover{
|
||||
background: var(--check-dark);
|
||||
color : #FFFFFF;
|
||||
border: var(--check);
|
||||
}
|
||||
|
||||
.btn-warning{
|
||||
background: var(--warning);
|
||||
color : #FFFFFF;
|
||||
border: var(--warning-dark);
|
||||
border-radius: 1rem;
|
||||
}
|
||||
.btn-warning:hover{
|
||||
background: var(--warning-dark);
|
||||
color : #FFFFFF;
|
||||
border: var(--warning);
|
||||
}
|
||||
|
||||
.btn-danger{
|
||||
background: var(--delete);
|
||||
color : #FFFFFF;
|
||||
border: var(--delete);
|
||||
border: var(--delete-dark);
|
||||
border-radius: 1rem;
|
||||
}
|
||||
.btn-danger:hover{
|
||||
background: var(--delete-dark);
|
||||
color : #FFFFFF;
|
||||
border: var(--delete);
|
||||
}
|
||||
|
||||
.color-primary{
|
||||
color: var(--primary-blue-light) !important;
|
||||
|
|
@ -117,6 +150,13 @@ body {
|
|||
color: var(--primary-blue-dark);
|
||||
}
|
||||
|
||||
.color-delete{
|
||||
color: var(--delete) !important;
|
||||
}
|
||||
.color-delete-dark{
|
||||
color: var(--delete-dark);
|
||||
}
|
||||
|
||||
.btn-secondary{
|
||||
background: var(--secondary);
|
||||
color : #FFFFFF;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ namespace App\Controller;
|
|||
use App\Entity\Actions;
|
||||
use App\Entity\Organizations;
|
||||
use App\Service\ActionService;
|
||||
use App\Service\UserService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
|
|
@ -15,21 +16,24 @@ class ActionController extends AbstractController
|
|||
{
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager,
|
||||
private ActionService $actionService
|
||||
private ActionService $actionService, private readonly UserService $userService
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/organization/{id}/activities-ajax', name: 'app_organization_activities_ajax', methods: ['GET'])]
|
||||
public function fetchActivitiesAjax(Organizations $organization): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
$actions = $this->entityManager->getRepository(Actions::class)->findBy(
|
||||
['Organization' => $organization],
|
||||
['date' => 'DESC'],
|
||||
10
|
||||
);
|
||||
$formattedActivities = $this->actionService->formatActivities($actions);
|
||||
$this->denyAccessUnlessGranted('ROLE_USER');
|
||||
if($this->userService->isAdminOfOrganization($organization) || $this->isGranted('ROLE_ADMIN')) {
|
||||
$actions = $this->entityManager->getRepository(Actions::class)->findBy(
|
||||
['Organization' => $organization],
|
||||
['date' => 'DESC'],
|
||||
10
|
||||
);
|
||||
$formattedActivities = $this->actionService->formatActivities($actions);
|
||||
|
||||
return new JsonResponse($formattedActivities);
|
||||
return new JsonResponse($formattedActivities);
|
||||
}
|
||||
return new JsonResponse(['error' => 'You are not authorized to access this page.'], 403);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ class ApplicationController extends AbstractController
|
|||
#[Route(path: '/edit/{id}', name: 'edit', methods: ['GET', 'POST'])]
|
||||
public function edit(int $id, Request $request): Response{
|
||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$actingUser = $this->getUser();
|
||||
$application = $this->entityManager->getRepository(Apps::class)->find($id);
|
||||
if (!$application) {
|
||||
$this->loggerService->logEntityNotFound('Application', [
|
||||
|
|
@ -102,95 +102,11 @@ class ApplicationController extends AbstractController
|
|||
|
||||
}
|
||||
|
||||
#[Route(path: '/authorize/{id}', name: 'authorize', methods: ['POST'])]
|
||||
public function authorize(int $id, Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
try{
|
||||
$application = $this->entityManager->getRepository(Apps::class)->find($id);
|
||||
if (!$application) {
|
||||
$this->loggerService->logEntityNotFound('Application', [
|
||||
'applicationId' => $id,
|
||||
'message' => "Application not found for authorization."
|
||||
], $actingUser->getId());
|
||||
throw $this->createNotFoundException("L'application n'existe pas.");
|
||||
}
|
||||
$orgId = $request->get('organizationId');
|
||||
|
||||
$organization = $this->entityManager->getRepository(Organizations::Class)->find($orgId);
|
||||
if (!$organization) {
|
||||
$this->loggerService->logEntityNotFound('Organization', [
|
||||
'Organization_id' => $orgId,
|
||||
'message' => "Organization not found for authorization."
|
||||
], $actingUser->getId());
|
||||
throw $this->createNotFoundException("L'Organization n'existe pas.");
|
||||
}
|
||||
$application->addOrganization($organization);
|
||||
$this->loggerService->logApplicationInformation('Application Authorized', [
|
||||
'applicationId' => $application->getId(),
|
||||
'applicationName' => $application->getName(),
|
||||
'organizationId' => $organization->getId(),
|
||||
'message' => "Application authorized for organization."
|
||||
], $actingUser->getId());
|
||||
$this->entityManager->persist($application);
|
||||
$this->entityManager->flush();
|
||||
$this->actionService->createAction("Authorization d'accès", $actingUser, $organization, $application->getName());
|
||||
return new Response('', Response::HTTP_OK);
|
||||
}catch (HttpExceptionInterface $e){
|
||||
throw $e;
|
||||
} catch (\Exception $e){
|
||||
$this->loggerService->logError('Application Authorization Failed', [
|
||||
'applicationId' => $id,
|
||||
'error' => $e->getMessage(),
|
||||
'message' => "Failed to authorize application.",
|
||||
'acting_user_id' => $actingUser->getId()
|
||||
]);
|
||||
return new Response('Erreur lors de l\'autorisation de l\'application.', Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
#[Route(path: '/revoke/{id}', name: 'revoke', methods: ['POST'])]
|
||||
public function revoke(int $id, Request $request)
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$application = $this->entityManager->getRepository(Apps::class)->find($id);
|
||||
if (!$application) {
|
||||
$this->loggerService->logEntityNotFound('Application', [
|
||||
'applicationId' => $id,
|
||||
'message' => "Application not found for authorization removal."
|
||||
], $actingUser->getId());
|
||||
throw $this->createNotFoundException("L'application n'existe pas.");
|
||||
}
|
||||
$orgId = $request->get('organizationId');
|
||||
$organization = $this->entityManager->getRepository(Organizations::Class)->find($orgId);
|
||||
if (!$organization) {
|
||||
$this->loggerService->logEntityNotFound('Organization', [
|
||||
'Organization_id' => $orgId,
|
||||
'message' => "Organization not found for authorization removal."
|
||||
], $actingUser->getId());
|
||||
throw $this->createNotFoundException("L'Organization n'existe pas.");
|
||||
}
|
||||
$application->removeOrganization($organization);
|
||||
$this->loggerService->logApplicationInformation('Application Authorized removed', [
|
||||
'applicationId' => $application->getId(),
|
||||
'applicationName' => $application->getName(),
|
||||
'organizationId' => $organization->getId(),
|
||||
'message' => "Application authorized removed for organization."
|
||||
], $actingUser->getId());
|
||||
$this->actionService->createAction("Authorization retirer", $actingUser, $organization, $application->getName());
|
||||
|
||||
return new Response('', Response::HTTP_OK);
|
||||
}
|
||||
|
||||
#[Route(path:'/user/{id}', name: 'user', methods: ['GET'])]
|
||||
public function getApplicationUsers(int $id): JSONResponse
|
||||
{
|
||||
$user = $this->userRepository->find($id);
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$actingUser = $this->getUser();
|
||||
if (!$user) {
|
||||
$this->loggerService->logEntityNotFound('User', ['message'=> 'User not found for application list'], $actingUser->getId());
|
||||
return new JsonResponse(['error' => 'User not found'], Response::HTTP_NOT_FOUND);
|
||||
|
|
@ -211,4 +127,20 @@ class ApplicationController extends AbstractController
|
|||
|
||||
return new JsonResponse($data, Response::HTTP_OK);
|
||||
}
|
||||
|
||||
#[Route(path: '/data/all', name: 'data_all', methods: ['GET'])]
|
||||
public function getAllApplications(): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
|
||||
$applications = $this->entityManager->getRepository(Apps::class)->findAll();
|
||||
$data = array_map(function($app) {
|
||||
return [
|
||||
'id' => $app->getId(),
|
||||
'name' => $app->getName(),
|
||||
'subDomain' => $app->getSubDomain(),
|
||||
'logoMiniUrl' => $this->assetsManager->getUrl($app->getLogoMiniUrl()),
|
||||
];
|
||||
}, $applications);
|
||||
return new JsonResponse($data, Response::HTTP_OK);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Service\UserService;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
|
|
@ -11,10 +12,15 @@ use Symfony\Component\Routing\Attribute\Route;
|
|||
|
||||
final class IndexController extends AbstractController
|
||||
{
|
||||
public function __construct(private readonly UserService $userService)
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/', name: 'app_index')]
|
||||
public function index(): Response
|
||||
{
|
||||
if ($this->isGranted('ROLE_ADMIN')) {
|
||||
|
||||
if ($this->isGranted('ROLE_ADMIN') || ($this->isGranted('ROLE_USER') && $this->userService->isAdminInAnyOrganization($this->getUser()))) {
|
||||
return $this->redirectToRoute('organization_index');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ class MercureController extends AbstractController
|
|||
public function getMercureToken(Request $request): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
|
||||
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$user =$this->getUser();
|
||||
|
||||
$domain = $request->getSchemeAndHttpHost();
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class NotificationController extends AbstractController
|
|||
public function index(): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
||||
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$user =$this->getUser();
|
||||
|
||||
$notifications = $this->notificationRepository->findRecentByUser($user, 50);
|
||||
$unreadCount = $this->notificationRepository->countUnreadByUser($user);
|
||||
|
|
@ -44,7 +44,7 @@ class NotificationController extends AbstractController
|
|||
public function unread(): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
|
||||
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$user =$this->getUser();
|
||||
|
||||
$notifications = $this->notificationRepository->findUnreadByUser($user);
|
||||
$unreadCount = count($notifications);
|
||||
|
|
@ -59,7 +59,7 @@ class NotificationController extends AbstractController
|
|||
public function count(): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
|
||||
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$user =$this->getUser();
|
||||
|
||||
$unreadCount = $this->notificationRepository->countUnreadByUser($user);
|
||||
|
||||
|
|
@ -70,7 +70,7 @@ class NotificationController extends AbstractController
|
|||
public function markAsRead(int $id): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
|
||||
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$user =$this->getUser();
|
||||
|
||||
$notification = $this->notificationRepository->find($id);
|
||||
|
||||
|
|
@ -88,7 +88,7 @@ class NotificationController extends AbstractController
|
|||
public function markAllAsRead(): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
|
||||
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$user =$this->getUser();
|
||||
|
||||
$count = $this->notificationRepository->markAllAsReadForUser($user);
|
||||
|
||||
|
|
@ -99,7 +99,7 @@ class NotificationController extends AbstractController
|
|||
public function delete(int $id): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
|
||||
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$user =$this->getUser();
|
||||
|
||||
$notification = $this->notificationRepository->find($id);
|
||||
|
||||
|
|
|
|||
|
|
@ -43,35 +43,42 @@ class OrganizationController extends AbstractController
|
|||
private readonly ActionService $actionService,
|
||||
private readonly UserOrganizationService $userOrganizationService,
|
||||
private readonly OrganizationsRepository $organizationsRepository,
|
||||
private readonly AwsService $awsService, private readonly LoggerService $loggerService, private readonly LoggerInterface $logger)
|
||||
private readonly LoggerService $loggerService)
|
||||
{
|
||||
}
|
||||
|
||||
#[Route(path: '/', name: 'index', methods: ['GET'])]
|
||||
public function index(): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
if($this->userService->hasAccessTo($actingUser, true)){
|
||||
$orgCount = $this->organizationsRepository->count(['isDeleted' => false]);
|
||||
if(!$this->isGranted("ROLE_SUPER_ADMIN")){
|
||||
$userUO = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['users' => $actingUser, 'isActive' => true]);
|
||||
$uoAdmin = 0;
|
||||
foreach($userUO as $u){
|
||||
if($this->userService->isAdminOfOrganization($u->getOrganization())){
|
||||
$uoAdmin++;
|
||||
}
|
||||
}
|
||||
if($uoAdmin === 1){
|
||||
return $this->redirectToRoute('organization_show', ['id' => $userUO[0]->getOrganization()->getId()]);
|
||||
}
|
||||
}
|
||||
return $this->render('organization/index.html.twig', [
|
||||
'hasOrganizations' => $orgCount > 0
|
||||
]);
|
||||
$this->denyAccessUnlessGranted('ROLE_USER');
|
||||
$actingUser = $this->getUser();
|
||||
|
||||
// 1. Super Admin Case: Just show the list
|
||||
if ($this->isGranted("ROLE_ADMIN")) {
|
||||
return $this->render('organization/index.html.twig', ['hasOrganizations' => true]);
|
||||
}
|
||||
$this->loggerService->logAccessDenied($actingUser->getId());
|
||||
throw new AccessDeniedHttpException('Access denied');
|
||||
|
||||
// 2. Organization Admin Case: Get their specific orgs
|
||||
$orgs = $this->userOrganizationService->getAdminOrganizationsForUser($actingUser);
|
||||
|
||||
// If exactly one org, jump straight to it
|
||||
if (count($orgs) === 1) {
|
||||
return $this->redirectToRoute('organization_show', ['id' => $orgs[0]->getId()]);
|
||||
}
|
||||
|
||||
// If multiple orgs, show the list
|
||||
if (count($orgs) > 1) {
|
||||
return $this->render('organization/index.html.twig', ['hasOrganizations' => true]);
|
||||
}
|
||||
|
||||
// 3. Fallback: No access/No orgs found
|
||||
$this->loggerService->logEntityNotFound('Organization', [
|
||||
'user_id' => $actingUser->getUserIdentifier(),
|
||||
'message' => 'No admin organizations found'
|
||||
], $actingUser->getUserIdentifier());
|
||||
|
||||
$this->addFlash('danger', 'Erreur, aucune organisation trouvée.');
|
||||
return $this->redirectToRoute('app_index');
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -79,7 +86,7 @@ class OrganizationController extends AbstractController
|
|||
public function new(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$actingUser = $this->getUser();
|
||||
if ($request->isMethod('POST')) {
|
||||
$organization = new Organizations();
|
||||
$form = $this->createForm(OrganizationForm::class, $organization);
|
||||
|
|
@ -90,16 +97,17 @@ class OrganizationController extends AbstractController
|
|||
$this->organizationsService->handleLogo($organization, $logoFile);
|
||||
}
|
||||
try {
|
||||
$organization->setProjectPrefix($this->organizationsService->generateUniqueProjectPrefix());
|
||||
$this->entityManager->persist($organization);
|
||||
$this->entityManager->flush();
|
||||
$this->loggerService->logOrganizationInformation($organization->getId(), $actingUser->getId(), "Organization Created");
|
||||
$this->loggerService->logSuperAdmin($actingUser->getId(), $actingUser->getId(), "Organization Created", $organization->getId());
|
||||
$this->loggerService->logOrganizationInformation($organization->getId(), $actingUser->getUserIdentifier(), "Organization Created");
|
||||
$this->loggerService->logSuperAdmin($actingUser->getUserIdentifier(), $actingUser->getUserIdentifier(), "Organization Created", $organization->getId());
|
||||
$this->actionService->createAction("Create Organization", $actingUser, $organization, $organization->getName());
|
||||
$this->addFlash('success', 'Organisation crée avec succès.');
|
||||
return $this->redirectToRoute('organization_index');
|
||||
} catch (Exception $e) {
|
||||
$this->addFlash('error', 'Erreur lors de la création de l\'organization');
|
||||
$this->loggerService->logError('Error creating organization', ['acting_user_id' => $actingUser->getId(), 'error' => $e->getMessage()]);
|
||||
$this->addFlash('danger', 'Erreur lors de la création de l\'organization');
|
||||
$this->loggerService->logError('Error creating organization', ['acting_user_id' => $actingUser->getUserIdentifier(), 'error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
return $this->render('organization/new.html.twig', [
|
||||
|
|
@ -117,40 +125,17 @@ class OrganizationController extends AbstractController
|
|||
public function edit(Request $request, $id): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$actingUser = $this->getUser();
|
||||
$organization = $this->organizationsRepository->find($id);
|
||||
if (!$organization) {
|
||||
$this->loggerService->logEntityNotFound('Organization', [
|
||||
'org_id' => $id,
|
||||
'message' => 'Organization not found for edit'], $actingUser->getId()
|
||||
'message' => 'Organization not found for edit'], $actingUser->getUserIdentifier()
|
||||
);
|
||||
$this->addFlash('error', 'Erreur, l\'organization est introuvable.');
|
||||
$this->addFlash('danger', 'Erreur, l\'organization est introuvable.');
|
||||
return $this->redirectToRoute('organization_index');
|
||||
}
|
||||
if (!$this->isGranted("ROLE_SUPER_ADMIN")) {
|
||||
//check if the user is admin of the organization
|
||||
$uo = $this->entityManager->getRepository(UsersOrganizations::class)->findOneBy(['users' => $actingUser, 'organization' => $organization]);
|
||||
if (!$uo) {
|
||||
$this->loggerService->logEntityNotFound('UO link', [
|
||||
'user_id' => $actingUser->getId(),
|
||||
'org_id' => $organization->getId(),
|
||||
'message' => 'UO link not found for edit organization'
|
||||
], $actingUser->getId());
|
||||
$this->addFlash('error', 'Erreur, accès refusé.');
|
||||
return $this->redirectToRoute('organization_index');
|
||||
}
|
||||
$roleAdmin = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN']);
|
||||
$uoaAdmin = $this->entityManager->getRepository(UserOrganizatonApp::class)->findOneBy(['userOrganization' => $uo, 'role' => $roleAdmin]);
|
||||
if (!$uoaAdmin) {
|
||||
$this->loggerService->logEntityNotFound('UOA link', [
|
||||
'uo_id' => $uo->getId(),
|
||||
'role_id' => $roleAdmin->getId(),
|
||||
'message' => 'UOA link not found for edit organization, user is not admin of organization'
|
||||
], $actingUser->getId());
|
||||
$this->addFlash('error', 'Erreur, accès refusé.');
|
||||
return $this->redirectToRoute('organization_index');
|
||||
}
|
||||
}
|
||||
|
||||
$form = $this->createForm(OrganizationForm::class, $organization);
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
|
|
@ -161,16 +146,16 @@ class OrganizationController extends AbstractController
|
|||
try {
|
||||
$this->entityManager->persist($organization);
|
||||
$this->entityManager->flush();
|
||||
$this->loggerService->logOrganizationInformation($organization->getId(), $actingUser->getId(), "Organization Edited");
|
||||
$this->loggerService->logOrganizationInformation($organization->getId(), $actingUser->getUserIdentifier(), "Organization Edited");
|
||||
if ($this->isGranted("ROLE_SUPER_ADMIN")) {
|
||||
$this->loggerService->logSuperAdmin($actingUser->getId(), $actingUser->getId(), "Organization Edited", $organization->getId());
|
||||
$this->loggerService->logSuperAdmin($actingUser->getUserIdentifier(), $actingUser->getUserIdentifier(), "Organization Edited", $organization->getId());
|
||||
}
|
||||
$this->actionService->createAction("Edit Organization", $actingUser, $organization, $organization->getName());
|
||||
$this->addFlash('success', 'Organisation modifiée avec succès.');
|
||||
return $this->redirectToRoute('organization_index');
|
||||
}catch (Exception $e) {
|
||||
$this->addFlash('error', 'Erreur lors de la modification de l\'organization');
|
||||
$this->loggerService->logError('Error editing organization', ['acting_user_id' => $actingUser->getId(), 'error' => $e->getMessage()]);
|
||||
$this->addFlash('danger', 'Erreur lors de la modification de l\'organization');
|
||||
$this->loggerService->logError('Error editing organization', ['acting_user_id' => $actingUser->getUserIdentifier(), 'error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
return $this->render('organization/edit.html.twig', [
|
||||
|
|
@ -182,24 +167,25 @@ class OrganizationController extends AbstractController
|
|||
#[Route(path: '/view/{id}', name: 'show', methods: ['GET'])]
|
||||
public function view($id): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
$this->denyAccessUnlessGranted('ROLE_USER');
|
||||
$organization = $this->organizationsRepository->find($id);
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$actingUser = $this->getUser();
|
||||
if (!$organization) {
|
||||
$this->loggerService->logEntityNotFound('Organization', [
|
||||
'org_id' => $id,
|
||||
'message' => 'Organization not found for view'
|
||||
], $actingUser->getId());
|
||||
$this->addFlash('error', 'Erreur, l\'organization est introuvable.');
|
||||
], $actingUser->getUserIdentifier());
|
||||
$this->addFlash('danger', 'Erreur, l\'organization est introuvable.');
|
||||
return $this->redirectToRoute('organization_index');
|
||||
}
|
||||
//check if the user is admin of the organization
|
||||
if (!$this->userService->isAdminOfOrganization($organization) && !$this->isGranted("ROLE_SUPER_ADMIN")) {
|
||||
$this->loggerService->logAccessDenied($actingUser->getId());
|
||||
$this->addFlash('error', 'Erreur, accès refusé.');
|
||||
if (!$this->userService->isAdminOfOrganization($organization) && !$this->isGranted("ROLE_ADMIN")) {
|
||||
$this->loggerService->logAccessDenied($actingUser->getUserIdentifier());
|
||||
$this->addFlash('danger', 'Erreur, accès refusé.');
|
||||
throw new AccessDeniedHttpException('Access denied');
|
||||
}
|
||||
|
||||
//TODO: add project to the response
|
||||
$allApps = $this->entityManager->getRepository(Apps::class)->findAll(); // appsAll
|
||||
$orgApps = $organization->getApps()->toArray(); // apps
|
||||
|
||||
|
|
@ -219,15 +205,15 @@ class OrganizationController extends AbstractController
|
|||
#[Route(path: '/delete/{id}', name: 'delete', methods: ['POST'])]
|
||||
public function delete($id): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted("ROLE_ADMIN");
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$this->denyAccessUnlessGranted("ROLE_SUPER_ADMIN");
|
||||
$actingUser = $this->getUser();
|
||||
$organization = $this->organizationsRepository->find($id);
|
||||
if (!$organization) {
|
||||
$this->loggerService->logEntityNotFound('Organization', [
|
||||
'org_id' => $id,
|
||||
'message' => 'Organization not found for delete'
|
||||
], $actingUser->getId());
|
||||
$this->addFlash('error', 'Erreur, l\'organization est introuvable.');
|
||||
], $actingUser->getUserIdentifier());
|
||||
$this->addFlash('danger', 'Erreur, l\'organization est introuvable.');
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
try {
|
||||
|
|
@ -240,14 +226,14 @@ class OrganizationController extends AbstractController
|
|||
$this->entityManager->persist($organization);
|
||||
$this->actionService->createAction("Delete Organization", $actingUser, $organization, $organization->getName());
|
||||
$this->entityManager->flush();
|
||||
$this->loggerService->logOrganizationInformation($organization->getId(), $actingUser->getId(),'Organization Deleted');
|
||||
$this->loggerService->logOrganizationInformation($organization->getId(), $actingUser->getUserIdentifier(),'Organization Deleted');
|
||||
if ($this->isGranted("ROLE_SUPER_ADMIN")) {
|
||||
$this->loggerService->logSuperAdmin($actingUser->getId(), $actingUser->getId(),'Organization Deleted', $organization->getId());
|
||||
$this->loggerService->logSuperAdmin($actingUser->getUserIdentifier(), $actingUser->getUserIdentifier(),'Organization Deleted', $organization->getId());
|
||||
}
|
||||
$this->addFlash('success', 'Organisation supprimée avec succès.');
|
||||
}catch (\Exception $e){
|
||||
$this->loggerService->logError($actingUser->getId(), ['message' => 'Error deleting organization: '.$e->getMessage()]);
|
||||
$this->addFlash('error', 'Erreur lors de la suppression de l\'organization.');
|
||||
$this->loggerService->logError($actingUser->getUserIdentifier(), ['message' => 'Error deleting organization: '.$e->getMessage()]);
|
||||
$this->addFlash('danger', 'Erreur lors de la suppression de l\'organization.');
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('organization_index');
|
||||
|
|
@ -257,22 +243,21 @@ class OrganizationController extends AbstractController
|
|||
public function deactivate($id): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted("ROLE_SUPER_ADMIN");
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$actingUser = $this->getUser();
|
||||
$organization = $this->organizationsRepository->find($id);
|
||||
if (!$organization) {
|
||||
$this->loggerService->logEntityNotFound('Organization', [
|
||||
'org_id' => $id,
|
||||
'message' => 'Organization not found for deactivate'
|
||||
], $actingUser->getId());
|
||||
$this->addFlash('error', 'Erreur, l\'organization est introuvable.');
|
||||
], $actingUser->getUserIdentifier());
|
||||
$this->addFlash('danger', 'Erreur, l\'organization est introuvable.');
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
|
||||
$organization->setIsActive(false);
|
||||
// $this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, null, $organization);
|
||||
$this->entityManager->persist($organization);
|
||||
$this->actionService->createAction("Deactivate Organization", $actingUser, $organization, $organization->getName());
|
||||
$this->loggerService->logSuperAdmin($actingUser->getId(), $actingUser->getId(),'Organization deactivated', $organization->getId());
|
||||
$this->loggerService->logSuperAdmin($actingUser->getUserIdentifier(), $actingUser->getUserIdentifier(),'Organization deactivated', $organization->getId());
|
||||
$this->addFlash('success', 'Organisation désactivé avec succès.');
|
||||
return $this->redirectToRoute('organization_index');
|
||||
}
|
||||
|
|
@ -281,20 +266,20 @@ class OrganizationController extends AbstractController
|
|||
public function activate($id): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted("ROLE_SUPER_ADMIN");
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$actingUser = $this->getUser();
|
||||
$organization = $this->organizationsRepository->find($id);
|
||||
if (!$organization) {
|
||||
$this->loggerService->logEntityNotFound('Organization', [
|
||||
'org_id' => $id,
|
||||
'message' => 'Organization not found for activate'
|
||||
], $actingUser->getId());
|
||||
$this->addFlash('error', 'Erreur, l\'organization est introuvable.');
|
||||
], $actingUser->getUserIdentifier());
|
||||
$this->addFlash('danger', 'Erreur, l\'organization est introuvable.');
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
$organization->setIsActive(true);
|
||||
$this->entityManager->persist($organization);
|
||||
$this->loggerService->logOrganizationInformation($organization->getId(), $actingUser->getId(),'Organization Activated');
|
||||
$this->loggerService->logSuperAdmin($actingUser->getId(), $actingUser->getId(),'Organization Activated', $organization->getId());
|
||||
$this->loggerService->logOrganizationInformation($organization->getId(), $actingUser->getUserIdentifier(),'Organization Activated');
|
||||
$this->loggerService->logSuperAdmin($actingUser->getUserIdentifier(), $actingUser->getUserIdentifier(),'Organization Activated', $organization->getId());
|
||||
$this->actionService->createAction("Activate Organization", $actingUser, $organization, $organization->getName());
|
||||
$this->addFlash('success', 'Organisation activée avec succès.');
|
||||
return $this->redirectToRoute('organization_index');
|
||||
|
|
@ -304,56 +289,23 @@ class OrganizationController extends AbstractController
|
|||
#[Route(path: '/data', name: 'data', methods: ['GET'])]
|
||||
public function data(Request $request): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
|
||||
|
||||
$page = max(1, (int)$request->query->get('page', 1));
|
||||
$size = max(1, (int)$request->query->get('size', 10));
|
||||
$this->denyAccessUnlessGranted('ROLE_USER');
|
||||
|
||||
$page = max(1, $request->query->getInt('page', 1));
|
||||
$size = max(1, $request->query->getInt('size', 10));
|
||||
$filters = $request->query->all('filter');
|
||||
|
||||
// Fetch paginated results
|
||||
$paginator = $this->organizationsRepository->findAdmissibleOrganizations(
|
||||
$this->getUser(),
|
||||
$this->isGranted('ROLE_ADMIN'), // Super Admin check
|
||||
$page,
|
||||
$size,
|
||||
$filters
|
||||
);
|
||||
|
||||
$qb = $this->organizationsRepository->createQueryBuilder('o')
|
||||
->where('o.isDeleted = :del')->setParameter('del', false);
|
||||
$total = count($paginator);
|
||||
|
||||
if (!empty($filters['name'])) {
|
||||
$qb->andWhere('o.name LIKE :name')
|
||||
->setParameter('name', '%' . $filters['name'] . '%');
|
||||
}
|
||||
if (!empty($filters['email'])) {
|
||||
$qb->andWhere('o.email LIKE :email')
|
||||
->setParameter('email', '%' . $filters['email'] . '%');
|
||||
}
|
||||
if (!$this->isGranted('ROLE_SUPER_ADMIN')) {
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$uo = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['users' => $actingUser]);
|
||||
|
||||
$allowedOrgIds = [];
|
||||
foreach ($uo as $item) {
|
||||
if ($this->userService->isAdminOfOrganization($item->getOrganization())) {
|
||||
$allowedOrgIds[] = $item->getOrganization()->getId();
|
||||
}
|
||||
}
|
||||
|
||||
// If user has no organizations, ensure query returns nothing (or handle typically)
|
||||
if (empty($allowedOrgIds)) {
|
||||
$qb->andWhere('1 = 0'); // Force empty result
|
||||
} else {
|
||||
$qb->andWhere('o.id IN (:orgIds)')
|
||||
->setParameter('orgIds', $allowedOrgIds);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Count total
|
||||
$countQb = clone $qb;
|
||||
$total = (int)$countQb->select('COUNT(o.id)')->getQuery()->getSingleScalarResult();
|
||||
|
||||
// Pagination
|
||||
$offset = ($page - 1) * $size;
|
||||
$rows = $qb->setFirstResult($offset)->setMaxResults($size)->getQuery()->getResult();
|
||||
|
||||
// Map to array
|
||||
$data = array_map(function (Organizations $org) {
|
||||
return [
|
||||
'id' => $org->getId(),
|
||||
|
|
@ -363,17 +315,12 @@ class OrganizationController extends AbstractController
|
|||
'active' => $org->isActive(),
|
||||
'showUrl' => $this->generateUrl('organization_show', ['id' => $org->getId()]),
|
||||
];
|
||||
}, $rows);
|
||||
|
||||
$lastPage = (int)ceil($total / $size);
|
||||
}, iterator_to_array($paginator));
|
||||
|
||||
return $this->json([
|
||||
'data' => $data,
|
||||
'last_page' => $lastPage,
|
||||
'total' => $total, // optional, useful for debugging
|
||||
'last_page' => (int)ceil($total / $size),
|
||||
'total' => $total,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -61,7 +61,16 @@ class UserController extends AbstractController
|
|||
)
|
||||
{
|
||||
}
|
||||
|
||||
//TODO: Move mailing/notification logic to event listeners/subscribers for better separation of concerns and to avoid bloating the controller with non-controller logic. Keep in mind the potential for circular dependencies and design accordingly (e.g. using interfaces or decoupled events).
|
||||
#[Route(path: '/', name: 'index', methods: ['GET'])]
|
||||
public function index(): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
$totalUsers = $this->userRepository->count(['isDeleted' => false, 'isActive' => true]);
|
||||
return $this->render('user/index.html.twig', [
|
||||
'users' => $totalUsers
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/view/{id}', name: 'show', methods: ['GET'])]
|
||||
public function view(int $id, Request $request): Response
|
||||
|
|
@ -70,128 +79,42 @@ class UserController extends AbstractController
|
|||
$this->denyAccessUnlessGranted('ROLE_USER');
|
||||
|
||||
// Utilisateur courant (acting user) via UserService
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
|
||||
// Vérification des droits d'accès supplémentaires
|
||||
|
||||
$actingUser = $this->getUser();
|
||||
|
||||
// Chargement de l'utilisateur cible à afficher
|
||||
$user = $this->userRepository->find($id);
|
||||
if (!$user) {
|
||||
$this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getId());
|
||||
$this->addFlash('error', "L'utilisateur demandé n'existe pas.");
|
||||
$this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getUserIdentifier());
|
||||
$this->addFlash('danger', "L'utilisateur demandé n'existe pas.");
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
if (!$this->userService->hasAccessTo($user)) {
|
||||
$this->loggerService->logAccessDenied($actingUser->getId());
|
||||
$this->addFlash('error', "L'utilisateur demandé n'existe pas.");
|
||||
//if hasAccessTo is false, turn to true and deny access
|
||||
if (!$this->userService->isAdminOfUser($user) && !$this->isGranted('ROLE_ADMIN')) {
|
||||
$this->loggerService->logAccessDenied($actingUser->getUserIdentifier());
|
||||
$this->addFlash('danger', "Vous n'avez pas accès à cette information.");
|
||||
throw new AccessDeniedHttpException (self::ACCESS_DENIED);
|
||||
}
|
||||
try {
|
||||
// Paramètre optionnel de contexte organisationnel
|
||||
$orgId = $request->query->get('organizationId');
|
||||
|
||||
// Liste de toutes les applications (pour créer des groupes même si vides)
|
||||
$apps = $this->appsRepository->findAll();
|
||||
|
||||
// Initialisations pour la résolution des UsersOrganizations (UO)
|
||||
$singleUo = null;
|
||||
$uoActive = null;
|
||||
|
||||
// get uo or uoS based on orgId
|
||||
if ($orgId) {
|
||||
// Contexte organisation précis : récupérer l'organisation et les liens UO
|
||||
$organization = $this->organizationRepository->findBy(['id' => $orgId]);
|
||||
$uoList = $this->uoRepository->findBy([
|
||||
'users' => $user,
|
||||
'organization' => $organization,
|
||||
'isActive' => true,
|
||||
]);
|
||||
|
||||
if (!$uoList) {
|
||||
$this->loggerService->logEntityNotFound('UsersOrganization', [
|
||||
'user_id' => $user->getId(),
|
||||
'organization_id' => $orgId],
|
||||
$actingUser->getId());
|
||||
$this->addFlash('error', "L'utilisateur n'est pas actif dans cette organisation.");
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
|
||||
// Si contexte org donné, on retient la première UO (singleUo)
|
||||
$singleUo = $uoList[0];
|
||||
$data["singleUo"] = $singleUo;
|
||||
$uoActive = $singleUo->isActive();
|
||||
// TODO: afficher les projets de l'organisation
|
||||
} else {
|
||||
// Pas de contexte org : récupérer toutes les UO actives de l'utilisateur
|
||||
$uoList = $this->uoRepository->findBy([
|
||||
'users' => $user,
|
||||
'isActive' => true,
|
||||
]);
|
||||
if (!$uoList) {
|
||||
$data['rolesArray'] = $this->userService->getRolesArrayForUser($actingUser, true);
|
||||
return $this->render('user/show.html.twig', [
|
||||
'user' => $user,
|
||||
'organizationId' => $orgId ?? null,
|
||||
'uoActive' => $uoActive ?? null,
|
||||
'apps' => $apps ?? [],
|
||||
'data' => $data ?? [],
|
||||
'canEdit' => false,
|
||||
]);
|
||||
}
|
||||
// Afficher tous les projets de l'utilisateur
|
||||
}
|
||||
// Charger les liens UserOrganizationApp (UOA) actifs pour les UO trouvées
|
||||
// Load user-organization-app roles (can be empty)
|
||||
$uoa = $this->entityManager
|
||||
->getRepository(UserOrganizatonApp::class)
|
||||
->findBy([
|
||||
'userOrganization' => $uoList,
|
||||
'isActive' => true,
|
||||
]);
|
||||
// Group UOA by app and ensure every app has a group
|
||||
$data['uoas'] = $this->userOrganizationAppService
|
||||
->groupUserOrganizationAppsByApplication(
|
||||
$uoa,
|
||||
$apps,
|
||||
$singleUo ? $singleUo->getId() : null
|
||||
);
|
||||
|
||||
//Build roles based on user permissions.
|
||||
//Admin can't see or edit a super admin user
|
||||
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
|
||||
$data['rolesArray'] = $this->rolesRepository->findAll();
|
||||
} elseif (!$orgId) {
|
||||
$data['rolesArray'] = $this->userService->getRolesArrayForUser($actingUser, true);
|
||||
} else {
|
||||
$data['rolesArray'] = $this->userService->getRolesArrayForUser($actingUser);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Calcul du flag de modification : utilisateur admin ET exactement 1 UO
|
||||
if (empty($uoa) || !$orgId){
|
||||
$canEdit = false;
|
||||
}else{
|
||||
$canEdit = $this->userService->canEditRolesCheck($actingUser, $user, $this->isGranted('ROLE_ADMIN'), $singleUo, $organization);
|
||||
}
|
||||
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->loggerService->logError('error while loading user information', [
|
||||
'target_user_id' => $id,
|
||||
'acting_user_id' => $actingUser->getId(),
|
||||
'acting_user_id' => $actingUser->getUserIdentifier(),
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
$this->addFlash('error', 'Une erreur est survenue lors du chargement des informations utilisateur.');
|
||||
$this->addFlash('danger', 'Une erreur est survenue lors du chargement des informations utilisateur.');
|
||||
$referer = $request->headers->get('referer');
|
||||
return $this->redirect($referer ?? $this->generateUrl('app_index'));
|
||||
}
|
||||
return $this->render('user/show.html.twig', [
|
||||
'user' => $user,
|
||||
'organizationId' => $orgId ?? null,
|
||||
'uoActive' => $uoActive ?? null,
|
||||
'apps' => $apps ?? [],
|
||||
'data' => $data ?? [],
|
||||
'canEdit' => $canEdit ?? false,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -199,15 +122,15 @@ class UserController extends AbstractController
|
|||
public function edit(int $id, Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_USER');
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$actingUser = $this->getUser();
|
||||
$user = $this->userRepository->find($id);
|
||||
if (!$user) {
|
||||
$this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getId());
|
||||
$this->addFlash('error', "L'utilisateur demandé n'existe pas.");
|
||||
$this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getUserIdentifier());
|
||||
$this->addFlash('danger', "L'utilisateur demandé n'existe pas.");
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
try {
|
||||
if ($this->userService->hasAccessTo($user)) {
|
||||
if ($this->userService->isAdminOfUser($user)) {
|
||||
|
||||
$form = $this->createForm(UserForm::class, $user);
|
||||
$form->handleRequest($request);
|
||||
|
|
@ -221,31 +144,31 @@ class UserController extends AbstractController
|
|||
$this->entityManager->flush();
|
||||
|
||||
//log and action
|
||||
$this->loggerService->logUserAction($user->getId(), $actingUser->getId(), 'User information edited');
|
||||
$this->loggerService->logUserAction($user->getId(), $actingUser->getUserIdentifier(), 'User information edited');
|
||||
$orgId = $request->get('organizationId');
|
||||
if ($orgId) {
|
||||
$org = $this->organizationRepository->find($orgId);
|
||||
if ($org) {
|
||||
$this->actionService->createAction("Edit user information", $actingUser, $org, $user->getUserIdentifier());
|
||||
$this->loggerService->logUserAction($user->getId(), $actingUser->getId(), 'User information edited');
|
||||
$this->loggerService->logUserAction($user->getId(), $actingUser->getUserIdentifier(), 'User information edited');
|
||||
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
|
||||
$this->loggerService->logSuperAdmin(
|
||||
$user->getId(),
|
||||
$actingUser->getId(),
|
||||
$actingUser->getUserIdentifier(),
|
||||
"Super Admin accessed user edit page",
|
||||
);
|
||||
}
|
||||
$this->addFlash('success', 'Information modifié avec success.');
|
||||
return $this->redirectToRoute('user_show', ['id' => $user->getId(), 'organizationId' => $orgId]);
|
||||
}
|
||||
$this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getId());
|
||||
$this->addFlash('error', "L'organisation n'existe pas.");
|
||||
$this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getUserIdentifier());
|
||||
$this->addFlash('danger', "L'organisation n'existe pas.");
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
|
||||
$this->loggerService->logSuperAdmin(
|
||||
$user->getId(),
|
||||
$actingUser->getId(),
|
||||
$actingUser->getUserIdentifier(),
|
||||
"Super Admin accessed user edit page",
|
||||
);
|
||||
}
|
||||
|
|
@ -260,11 +183,11 @@ class UserController extends AbstractController
|
|||
'organizationId' => $request->get('organizationId')
|
||||
]);
|
||||
}
|
||||
$this->loggerService->logAccessDenied($actingUser->getId());
|
||||
$this->addFlash('error', "Accès non autorisé.");
|
||||
$this->loggerService->logAccessDenied($actingUser->getUserIdentifier());
|
||||
$this->addFlash('danger', "Accès non autorisé.");
|
||||
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||
} catch (\Exception $e) {
|
||||
$this->addFlash('error', 'Une erreur est survenue lors de la modification des informations utilisateur.');
|
||||
$this->addFlash('danger', 'Une erreur est survenue lors de la modification des informations utilisateur.');
|
||||
$this->errorLogger->critical($e->getMessage());
|
||||
}
|
||||
// Default deny access. shouldn't reach here normally.
|
||||
|
|
@ -275,52 +198,47 @@ class UserController extends AbstractController
|
|||
#[Route('/new', name: 'new', methods: ['GET', 'POST'])]
|
||||
public function new(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
$this->denyAccessUnlessGranted('ROLE_USER');
|
||||
try {
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
if (!$this->userService->hasAccessTo($actingUser)) {
|
||||
$this->loggerService->logAccessDenied($actingUser->getId());
|
||||
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||
}
|
||||
$actingUser =$this->getUser();
|
||||
|
||||
$user = new User();
|
||||
$form = $this->createForm(UserForm::class, $user);
|
||||
$form->handleRequest($request);
|
||||
|
||||
$orgId = $request->get('organizationId');
|
||||
$orgId = $request->query->get('organizationId') ?? $request->request->get('organizationId');
|
||||
if ($orgId) {
|
||||
$org = $this->organizationRepository->find($orgId);
|
||||
if (!$org) {
|
||||
$this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getId());
|
||||
$this->addFlash('error', "L'organisation n'existe pas.");
|
||||
$this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getUserIdentifier());
|
||||
$this->addFlash('danger', "L'organisation n'existe pas.");
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
if ($this->isGranted('ROLE_ADMIN') && !$this->userService->isAdminOfOrganization($org) && !$this->isGranted('ROLE_SUPER_ADMIN')) {
|
||||
$this->loggerService->logAccessDenied($actingUser->getId());
|
||||
$this->addFlash('error', "Accès non autorisé.");
|
||||
if (!$this->isGranted('ROLE_ADMIN') && !$this->userService->isAdminOfOrganization($org)) {
|
||||
$this->loggerService->logAccessDenied($actingUser->getUserIdentifier());
|
||||
$this->addFlash('danger', "Accès non autorisé.");
|
||||
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||
}
|
||||
} elseif ($this->isGranted('ROLE_ADMIN')) {
|
||||
$this->loggerService->logAccessDenied($actingUser->getId());
|
||||
$this->addFlash('error', "Accès non autorisé.");
|
||||
} else{
|
||||
$this->loggerService->logAccessDenied($actingUser->getUserIdentifier());
|
||||
$this->addFlash('danger', "Accès non autorisé.");
|
||||
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||
}
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$existingUser = $this->userRepository->findOneBy(['email' => $user->getEmail()]);
|
||||
|
||||
// Case : User exists + has organization context
|
||||
// Case : User exists -> link him to given organization if not already linked, else error message
|
||||
if ($existingUser && $org) {
|
||||
$this->userService->addExistingUserToOrganization(
|
||||
$existingUser,
|
||||
$org,
|
||||
$actingUser,
|
||||
);
|
||||
|
||||
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
|
||||
if ($this->isGranted('ROLE_ADMIN')) {
|
||||
$this->loggerService->logSuperAdmin(
|
||||
$existingUser->getId(),
|
||||
$actingUser->getId(),
|
||||
$actingUser->getUserIdentifier(),
|
||||
"Super Admin linked user to organization",
|
||||
$org->getId(),
|
||||
);
|
||||
|
|
@ -329,61 +247,17 @@ class UserController extends AbstractController
|
|||
return $this->redirectToRoute('organization_show', ['id' => $orgId]);
|
||||
}
|
||||
|
||||
//Code semi-mort : On ne peut plus créer un utilisateur sans organisation
|
||||
// Case : User exists but NO organization context -> throw error on email field.
|
||||
|
||||
// if ($existingUser) {
|
||||
// $this->loggerService->logError('Attempt to create user with existing email without organization', [
|
||||
// 'target_user_email' => $user->getid(),
|
||||
// 'acting_user_id' => $actingUser->getId(),
|
||||
// ]);
|
||||
//
|
||||
// $form->get('email')->addError(
|
||||
// new \Symfony\Component\Form\FormError(
|
||||
// 'This email is already in use. Add the user to an organization instead.'
|
||||
// )
|
||||
// );
|
||||
//
|
||||
// return $this->render('user/new.html.twig', [
|
||||
// 'user' => $user,
|
||||
// 'form' => $form->createView(),
|
||||
// 'organizationId' => $orgId,
|
||||
// ]);
|
||||
// }
|
||||
// Case : user doesn't already exist
|
||||
|
||||
$picture = $form->get('pictureUrl')->getData();
|
||||
$this->userService->createNewUser($user, $actingUser, $picture);
|
||||
|
||||
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
|
||||
$this->loggerService->logSuperAdmin(
|
||||
$user->getId(),
|
||||
$actingUser->getId(),
|
||||
"Super Admin created new user",
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
// Case : Organization provided and user doesn't already exist
|
||||
if ($orgId) {
|
||||
$this->userService->linkUserToOrganization(
|
||||
$user,
|
||||
$org,
|
||||
$actingUser,
|
||||
);
|
||||
|
||||
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
|
||||
$this->loggerService->logSuperAdmin(
|
||||
$user->getId(),
|
||||
$actingUser->getId(),
|
||||
"Super Admin linked user to organization during creation",
|
||||
$org->getId()
|
||||
);
|
||||
}
|
||||
$this->addFlash('success', 'Nouvel utilisateur créé et ajouté à l\'organisation avec succès. ');
|
||||
return $this->redirectToRoute('organization_show', ['id' => $orgId]);
|
||||
}
|
||||
$this->addFlash('success', 'Nouvel utilisateur créé avec succès. ');
|
||||
return $this->redirectToRoute('user_index');
|
||||
$this->userService->linkUserToOrganization(
|
||||
$user,
|
||||
$org,
|
||||
);
|
||||
$this->addFlash('success', 'Nouvel utilisateur créé et ajouté à l\'organisation avec succès. ');
|
||||
return $this->redirectToRoute('organization_show', ['id' => $orgId]);
|
||||
}
|
||||
|
||||
return $this->render('user/new.html.twig', [
|
||||
|
|
@ -396,38 +270,32 @@ class UserController extends AbstractController
|
|||
$this->errorLogger->critical($e->getMessage());
|
||||
|
||||
if ($orgId) {
|
||||
$this->addFlash('error', 'Une erreur est survenue lors de la création de l\'utilisateur pour l\'organisation .');
|
||||
$this->addFlash('danger', 'Une erreur est survenue lors de la création de l\'utilisateur pour l\'organisation .');
|
||||
return $this->redirectToRoute('organization_show', ['id' => $orgId]);
|
||||
}
|
||||
$this->addFlash('error', 'Une erreur est survenue lors de la création de l\'utilisateur.');
|
||||
$this->addFlash('danger', 'Une erreur est survenue lors de la création de l\'utilisateur.');
|
||||
return $this->redirectToRoute('user_index');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[Route('/activeStatus/{id}', name: 'active_status', methods: ['GET', 'POST'])]
|
||||
/**
|
||||
* Endpoint to activate/deactivate a user (soft delete)
|
||||
* If deactivating, also deactivate all org links and revoke tokens
|
||||
*/
|
||||
#[Route('/activeStatus/{id}', name: 'active_status', methods: ['POST'])]
|
||||
public function activeStatus(int $id, Request $request): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$status = $request->get('status');
|
||||
$actingUser =$this->getUser();
|
||||
$status = $request->request->get('status');
|
||||
try {
|
||||
// Access control
|
||||
if (!$this->userService->hasAccessTo($actingUser, true)) {
|
||||
$this->loggerService->logAccessDenied($actingUser->getId());
|
||||
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||
}
|
||||
|
||||
// Load target user
|
||||
$user = $this->userRepository->find($id);
|
||||
if (!$user) {
|
||||
$this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getId());
|
||||
|
||||
$this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getUserIdentifier());
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
|
||||
// Deactivate
|
||||
if ($status === 'deactivate') {
|
||||
$user->setIsActive(false);
|
||||
|
||||
|
|
@ -440,12 +308,12 @@ class UserController extends AbstractController
|
|||
$user->setModifiedAt(new \DateTimeImmutable('now'));
|
||||
$this->entityManager->persist($user);
|
||||
$this->entityManager->flush();
|
||||
$this->loggerService->logUserAction($user->getId(), $actingUser->getId(), 'User deactivated');
|
||||
$this->loggerService->logUserAction($user->getId(), $actingUser->getUserIdentifier(), 'User deactivated');
|
||||
|
||||
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
|
||||
$this->loggerService->logSuperAdmin(
|
||||
$user->getId(),
|
||||
$actingUser->getId(),
|
||||
$actingUser->getUserIdentifier(),
|
||||
'Super admin deactivated user'
|
||||
);
|
||||
}
|
||||
|
|
@ -461,13 +329,13 @@ class UserController extends AbstractController
|
|||
$user->setModifiedAt(new \DateTimeImmutable('now'));
|
||||
$this->entityManager->persist($user);
|
||||
$this->entityManager->flush();
|
||||
$this->loggerService->logUserAction($user->getId(), $actingUser->getId(), 'User activated');
|
||||
$this->loggerService->logUserAction($user->getId(), $actingUser->getUserIdentifier(), 'User activated');
|
||||
|
||||
|
||||
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
|
||||
$this->loggerService->logSuperAdmin(
|
||||
$user->getId(),
|
||||
$actingUser->getId(),
|
||||
$actingUser->getUserIdentifier(),
|
||||
'Super admin activated user'
|
||||
);
|
||||
}
|
||||
|
|
@ -498,32 +366,33 @@ class UserController extends AbstractController
|
|||
}
|
||||
}
|
||||
|
||||
#[Route('/organization/activateStatus/{id}', name: 'activate_organization', methods: ['GET', 'POST'])]
|
||||
#[Route('/organization/activateStatus/{id}', name: 'activate_organization', methods: ['POST'])]
|
||||
public function activateStatusOrganization(int $id, Request $request): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$this->denyAccessUnlessGranted('ROLE_USER');
|
||||
$actingUser = $this->getUser();
|
||||
try {
|
||||
if ($this->userService->hasAccessTo($actingUser, true)) {
|
||||
$orgId = $request->get('organizationId');
|
||||
$user = $this->userRepository->find($id);
|
||||
if (!$user) {
|
||||
$this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getUserIdentifier());
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
if ($this->userService->isAdminOfUser($user)) {
|
||||
$orgId = $request->request->get('organizationId');
|
||||
$org = $this->organizationRepository->find($orgId);
|
||||
if (!$org) {
|
||||
$this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getId());
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
$user = $this->userRepository->find($id);
|
||||
if (!$user) {
|
||||
$this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getId());
|
||||
$this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getUserIdentifier());
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
|
||||
$uo = $this->uoRepository->findOneBy(['users' => $user,
|
||||
'organization' => $org]);
|
||||
if (!$uo) {
|
||||
$this->loggerService->logEntityNotFound('UsersOrganization', ['user_id' => $user->getId(),
|
||||
'organization_id' => $org->getId()], $actingUser->getId());
|
||||
'organization_id' => $org->getId()], $actingUser->getUserIdentifier());
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
$status = $request->get('status');
|
||||
$status = $request->request->get('status');
|
||||
if ($status === 'deactivate') {
|
||||
$uo->setIsActive(false);
|
||||
$this->userOrganizationAppService->deactivateAllUserOrganizationsAppLinks($uo);
|
||||
|
|
@ -532,7 +401,7 @@ class UserController extends AbstractController
|
|||
$data = ['user' => $user,
|
||||
'organization' => $org];
|
||||
$this->organizationsService->notifyOrganizationAdmins($data, "USER_DEACTIVATED");
|
||||
$this->loggerService->logOrganizationInformation($org->getId(), $actingUser->getId(), "UO link deactivated with uo id : {$uo->getId()}");
|
||||
$this->loggerService->logOrganizationInformation($org->getId(), $actingUser->getUserIdentifier(), "UO link deactivated with uo id : {$uo->getId()}");
|
||||
$this->actionService->createAction("Deactivate user in organization", $actingUser, $org, $org->getName() . " for user " . $user->getUserIdentifier());
|
||||
return new JsonResponse(['status' => 'deactivated'], Response::HTTP_OK);
|
||||
}
|
||||
|
|
@ -540,7 +409,7 @@ class UserController extends AbstractController
|
|||
$uo->setIsActive(true);
|
||||
$this->entityManager->persist($uo);
|
||||
$this->entityManager->flush();
|
||||
$this->loggerService->logOrganizationInformation($orgId, $actingUser->getId(), "UO link activated with uo id : {$uo->getId()}");
|
||||
$this->loggerService->logOrganizationInformation($orgId, $actingUser->getUserIdentifier(), "UO link activated with uo id : {$uo->getId()}");
|
||||
$this->actionService->createAction("Activate user in organization", $actingUser, $org, $org->getName() . " for user " . $user->getUserIdentifier());
|
||||
$data = ['user' => $user,
|
||||
'organization' => $org];
|
||||
|
|
@ -566,14 +435,14 @@ class UserController extends AbstractController
|
|||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
||||
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$actingUser = $this->getUser();
|
||||
|
||||
try {
|
||||
$user = $this->userRepository->find($id);
|
||||
if (!$user) {
|
||||
// Security/audit log for missing user
|
||||
$this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getId());
|
||||
$this->addFlash('error', "L'utilisateur demandé n'existe pas.");
|
||||
$this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getUserIdentifier());
|
||||
$this->addFlash('danger', "L'utilisateur demandé n'existe pas.");
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
|
||||
|
|
@ -585,7 +454,7 @@ class UserController extends AbstractController
|
|||
$user->setModifiedAt(new \DateTimeImmutable('now'));
|
||||
// Deactivate all org links
|
||||
$this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, $user);
|
||||
$this->loggerService->logOrganizationInformation($user->getId(), $actingUser->getId(), 'All user organization links deactivated');
|
||||
$this->loggerService->logOrganizationInformation($user->getId(), $actingUser->getUserIdentifier(), 'All user organization links deactivated');
|
||||
|
||||
// Revoke tokens if connected
|
||||
if ($this->userService->isUserConnected($user->getUserIdentifier())) {
|
||||
|
|
@ -595,13 +464,13 @@ class UserController extends AbstractController
|
|||
$this->entityManager->flush();
|
||||
|
||||
// User management log
|
||||
$this->loggerService->logUserAction($user->getId(), $actingUser->getId(), 'User deleted');
|
||||
$this->loggerService->logUserAction($user->getId(), $actingUser->getUserIdentifier(), 'User deleted');
|
||||
|
||||
// Super admin log (standardized style)
|
||||
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
|
||||
$this->loggerService->logSuperAdmin(
|
||||
$user->getId(),
|
||||
$actingUser->getId(),
|
||||
$actingUser->getUserIdentifier(),
|
||||
'Super admin deleted user'
|
||||
);
|
||||
}
|
||||
|
|
@ -636,68 +505,11 @@ class UserController extends AbstractController
|
|||
if ($e instanceof NotFoundHttpException) {
|
||||
throw $e; // keep 404 semantics
|
||||
}
|
||||
$this->addFlash('error', 'Erreur lors de la suppression de l\'utilisateur\.');
|
||||
$this->addFlash('danger', 'Erreur lors de la suppression de l\'utilisateur\.');
|
||||
return $this->redirectToRoute('user_index');
|
||||
}
|
||||
}
|
||||
|
||||
#[Route(path: '/application/roles/{id}', name: 'application_role', methods: ['GET', 'POST'])]
|
||||
public function applicationRole(int $id, Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted("ROLE_ADMIN");
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
|
||||
if ($this->userService->hasAccessTo($actingUser, true)) {
|
||||
$uo = $this->entityManager->getRepository(UsersOrganizations::class)->find($id);
|
||||
if (!$uo) {
|
||||
$this->loggerService->logEntityNotFound('UsersOrganization', ['id' => $id], $actingUser->getId());
|
||||
$this->addFlash('error', "La liaison utilisateur-organisation n'existe pas.");
|
||||
throw new NotFoundHttpException("UserOrganization not found");
|
||||
}
|
||||
$application = $this->entityManager->getRepository(Apps::class)->find($request->get('appId'));
|
||||
if (!$application) {
|
||||
$this->loggerService->logEntityNotFound('Application', ['id' => $request->get('appId')], $actingUser->getId());
|
||||
$this->addFlash('error', "L'application demandée n'existe pas.");
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
|
||||
$selectedRolesIds = $request->get('roles', []);
|
||||
$roleUser = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'USER']);
|
||||
if (!$roleUser) {
|
||||
$this->loggerService->logEntityNotFound('Role', ['name' => 'USER'], $actingUser->getId());
|
||||
$this->addFlash('error', "Le role de l'utilisateur n'existe pas.");
|
||||
throw $this->createNotFoundException('User role not found');
|
||||
}
|
||||
|
||||
if (!empty($selectedRolesIds)) {
|
||||
// Si le role User n'est pas sélectionné, on désactive tous les liens (affiché comme 'accès' dans l'UI)
|
||||
if (!in_array((string)$roleUser->getId(), $selectedRolesIds, true)) {
|
||||
$this->userOrganizationAppService->deactivateAllUserOrganizationsAppLinks($uo, $application);
|
||||
} else {
|
||||
$this->userOrganizationAppService->syncRolesForUserOrganizationApp(
|
||||
$uo,
|
||||
$application,
|
||||
$selectedRolesIds,
|
||||
$actingUser
|
||||
);
|
||||
}
|
||||
|
||||
} else {
|
||||
$this->userOrganizationAppService->deactivateAllUserOrganizationsAppLinks($uo, $application);
|
||||
}
|
||||
|
||||
$user = $uo->getUsers();
|
||||
$this->addFlash('success', 'Rôles mis à jour avec succès.');
|
||||
return $this->redirectToRoute('user_show', [
|
||||
'user' => $user,
|
||||
'id' => $user->getId(),
|
||||
'organizationId' => $uo->getOrganization()->getId()
|
||||
]);
|
||||
}
|
||||
|
||||
throw $this->createAccessDeniedException();
|
||||
}
|
||||
|
||||
/*
|
||||
* AJAX endpoint for user listing with pagination
|
||||
* Get all the users that aren´t deleted and are active
|
||||
|
|
@ -707,40 +519,14 @@ class UserController extends AbstractController
|
|||
{
|
||||
$this->denyAccessUnlessGranted("ROLE_ADMIN");
|
||||
|
||||
$page = max(1, (int)$request->query->get('page', 1));
|
||||
$size = max(1, (int)$request->query->get('size', 10));
|
||||
|
||||
// Get filter parameters
|
||||
$page = max(1, $request->query->getInt('page', 1));
|
||||
$size = max(1, $request->query->getInt('size', 10));
|
||||
$filters = $request->query->all('filter', []);
|
||||
|
||||
$repo = $this->userRepository;
|
||||
// Call the repository
|
||||
$paginator = $this->userRepository->findActiveUsersForTabulator($page, $size, $filters);
|
||||
$total = count($paginator);
|
||||
|
||||
// Base query
|
||||
$qb = $repo->createQueryBuilder('u')
|
||||
->where('u.isDeleted = :del')->setParameter('del', false);
|
||||
|
||||
// Apply filters
|
||||
if (!empty($filters['name'])) {
|
||||
$qb->andWhere('u.surname LIKE :name')
|
||||
->setParameter('name', '%' . $filters['name'] . '%');
|
||||
}
|
||||
if (!empty($filters['prenom'])) {
|
||||
$qb->andWhere('u.name LIKE :prenom')
|
||||
->setParameter('prenom', '%' . $filters['prenom'] . '%');
|
||||
}
|
||||
if (!empty($filters['email'])) {
|
||||
$qb->andWhere('u.email LIKE :email')
|
||||
->setParameter('email', '%' . $filters['email'] . '%');
|
||||
}
|
||||
|
||||
$countQb = clone $qb;
|
||||
$total = (int)$countQb->select('COUNT(u.id)')->getQuery()->getSingleScalarResult();
|
||||
|
||||
// Pagination
|
||||
$offset = ($page - 1) * $size;
|
||||
$rows = $qb->setFirstResult($offset)->setMaxResults($size)->getQuery()->getResult();
|
||||
|
||||
// Map to array
|
||||
$data = array_map(function (User $user) {
|
||||
return [
|
||||
'id' => $user->getId(),
|
||||
|
|
@ -752,33 +538,15 @@ class UserController extends AbstractController
|
|||
'showUrl' => $this->generateUrl('user_show', ['id' => $user->getId()]),
|
||||
'statut' => $user->isActive(),
|
||||
];
|
||||
}, $rows);
|
||||
$lastPage = (int)ceil($total / $size);
|
||||
}, iterator_to_array($paginator));
|
||||
|
||||
return $this->json([
|
||||
'data' => $data,
|
||||
'last_page' => $lastPage,
|
||||
'last_page' => (int)ceil($total / $size),
|
||||
'total' => $total,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route(path: '/', name: 'index', methods: ['GET'])]
|
||||
public function index(): Response
|
||||
{
|
||||
$this->isGranted('ROLE_SUPER_ADMIN');
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
if ($this->userService->hasAccessTo($actingUser, true) && $this->isGranted("ROLE_ADMIN")) {
|
||||
$totalUsers = $this->userRepository->count(['isDeleted' => false, 'isActive' => true]);
|
||||
return $this->render('user/index.html.twig', [
|
||||
'users' => $totalUsers
|
||||
]);
|
||||
}
|
||||
|
||||
//shouldn't be reached normally
|
||||
$this->loggerService->logAccessDenied($actingUser->getId());
|
||||
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||
}
|
||||
|
||||
/*
|
||||
* AJAX endpoint for new users listing
|
||||
* Get the 5 most recently created users for an organization
|
||||
|
|
@ -786,10 +554,16 @@ class UserController extends AbstractController
|
|||
#[Route(path: '/data/new', name: 'dataNew', methods: ['GET'])]
|
||||
public function dataNew(Request $request): JsonResponse
|
||||
{
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
if ($this->userService->hasAccessTo($actingUser, true) && $this->isGranted("ROLE_ADMIN")) {
|
||||
$orgId = $request->query->get('orgId');
|
||||
$uos = $this->uoRepository->findBy(['organization' => $orgId, 'statut' => ["ACCEPTED", "INVITED"]],
|
||||
$actingUser = $this->getUser();
|
||||
$orgId = $request->query->get('orgId');
|
||||
$org = $this->organizationRepository->find($orgId);
|
||||
if (!$org) {
|
||||
$this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getUserIdentifier());
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
if ($this->userService->isAdminOfOrganization($org) || $this->isGranted("ROLE_ADMIN")) {
|
||||
|
||||
$uos = $this->uoRepository->findBy(['organization' => $org, 'statut' => ["ACCEPTED", "INVITED"]],
|
||||
orderBy: ['createdAt' => 'DESC'], limit: 5);
|
||||
|
||||
|
||||
|
|
@ -822,10 +596,15 @@ class UserController extends AbstractController
|
|||
#[Route(path: '/data/admin', name: 'dataAdmin', methods: ['GET'])]
|
||||
public function dataAdmin(Request $request): JsonResponse
|
||||
{
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
if ($this->userService->hasAccessTo($actingUser, true) && $this->isGranted("ROLE_ADMIN")) {
|
||||
$orgId = $request->query->get('orgId');
|
||||
$uos = $this->uoRepository->findBy(['organization' => $orgId]);
|
||||
$actingUser = $this->getUser();
|
||||
$orgId = $request->query->get('orgId');
|
||||
$org = $this->organizationRepository->find($orgId);
|
||||
if (!$org) {
|
||||
$this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getUserIdentifier());
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
if ($this->userService->isAdminOfOrganization($org) || $this->isGranted("ROLE_ADMIN")) {
|
||||
$uos = $this->uoRepository->findBy(['organization' => $org]);
|
||||
$roleAdmin = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN']);
|
||||
$users = [];
|
||||
foreach ($uos as $uo) {
|
||||
|
|
@ -863,76 +642,54 @@ class UserController extends AbstractController
|
|||
#[Route(path: '/data/organization', name: 'dataUserOrganization', methods: ['GET'])]
|
||||
public function dataUserOrganization(Request $request): JsonResponse
|
||||
{
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$actingUser = $this->getUser();
|
||||
$orgId = $request->query->get('orgId');
|
||||
|
||||
if ($this->userService->hasAccessTo($actingUser, true) && $this->isGranted("ROLE_ADMIN")) {
|
||||
$orgId = $request->query->get('orgId');
|
||||
$page = max(1, (int)$request->query->get('page', 1));
|
||||
$size = max(1, (int)$request->query->get('size', 10));
|
||||
|
||||
$filters = $request->query->all('filter') ?? [];
|
||||
|
||||
$repo = $this->uoRepository;
|
||||
|
||||
// Base query
|
||||
$qb = $repo->createQueryBuilder('uo')
|
||||
->join('uo.users', 'u')
|
||||
->where('uo.organization = :orgId')
|
||||
->setParameter('orgId', $orgId);
|
||||
|
||||
// Apply filters
|
||||
if (!empty($filters['name'])) {
|
||||
$qb->andWhere('u.surname LIKE :name')
|
||||
->setParameter('name', '%' . $filters['name'] . '%');
|
||||
}
|
||||
if (!empty($filters['prenom'])) {
|
||||
$qb->andWhere('u.name LIKE :prenom')
|
||||
->setParameter('prenom', '%' . $filters['prenom'] . '%');
|
||||
}
|
||||
if (!empty($filters['email'])) {
|
||||
$qb->andWhere('u.email LIKE :email')
|
||||
->setParameter('email', '%' . $filters['email'] . '%');
|
||||
}
|
||||
|
||||
$countQb = clone $qb;
|
||||
$total = (int)$countQb->select('COUNT(uo.id)')->getQuery()->getSingleScalarResult();
|
||||
|
||||
$qb->orderBy('uo.isActive', 'DESC')
|
||||
->addOrderBy('CASE WHEN uo.statut = :invited THEN 0 ELSE 1 END', 'ASC')
|
||||
->setParameter('invited', 'INVITED');
|
||||
|
||||
$offset = ($page - 1) * $size;
|
||||
$rows = $qb->setFirstResult($offset)->setMaxResults($size)->getQuery()->getResult();
|
||||
$data = $this->userService->formatStatutForOrganizations($rows);
|
||||
|
||||
$lastPage = (int)ceil($total / $size);
|
||||
|
||||
return $this->json([
|
||||
'data' => $data,
|
||||
'last_page' => $lastPage,
|
||||
'total' => $total,
|
||||
]);
|
||||
$org = $this->organizationRepository->find($orgId);
|
||||
if (!$org) {
|
||||
$this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getUserIdentifier());
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
|
||||
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||
// Security Check
|
||||
if (!$this->isGranted("ROLE_ADMIN") && !$this->userService->isAdminOfOrganization($org)) {
|
||||
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||
}
|
||||
|
||||
// Params extraction
|
||||
$page = max(1, $request->query->getInt('page', 1));
|
||||
$size = max(1, $request->query->getInt('size', 10));
|
||||
$filters = $request->query->all('filter') ?? [];
|
||||
|
||||
// Get paginated results from Repository
|
||||
$paginator = $this->uoRepository->findByOrganizationWithFilters($org, $page, $size, $filters);
|
||||
$total = count($paginator);
|
||||
|
||||
// Format the data using your existing service method
|
||||
$data = $this->userService->formatStatutForOrganizations(iterator_to_array($paginator));
|
||||
|
||||
return $this->json([
|
||||
'data' => $data,
|
||||
'last_page' => (int)ceil($total / $size),
|
||||
'total' => $total,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route(path: '/organization/resend-invitation/{userId}', name: 'resend_invitation', methods: ['POST'])]
|
||||
public function resendInvitation(int $userId, Request $request): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted("ROLE_ADMIN");
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
if ($this->userService->hasAccessTo($actingUser, true)) {
|
||||
$orgId = $request->get('organizationId');
|
||||
$org = $this->organizationRepository->find($orgId);
|
||||
if (!$org) {
|
||||
$this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getId());
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
$this->denyAccessUnlessGranted("ROLE_USER");
|
||||
$actingUser = $this->getUser();
|
||||
$orgId = $request->request->get('organizationId');
|
||||
$org = $this->organizationRepository->find($orgId);
|
||||
if (!$org) {
|
||||
$this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getUserIdentifier());
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
if ($this->userService->isAdminOfOrganization($org)) {
|
||||
$user = $this->userRepository->find($userId);
|
||||
if (!$user) {
|
||||
$this->loggerService->logEntityNotFound('User', ['id' => $user->getId()], $actingUser->getId());
|
||||
$this->loggerService->logEntityNotFound('User', ['id' => $user->getId()], $actingUser->getUserIdentifier());
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
$token = $this->userService->generatePasswordToken($user, $org->getId());
|
||||
|
|
@ -945,7 +702,7 @@ class UserController extends AbstractController
|
|||
if (!$uo) {
|
||||
$this->loggerService->logEntityNotFound('UsersOrganization', [
|
||||
'user_id' => $user->getId(),
|
||||
'organization_id' => $orgId], $actingUser->getId());
|
||||
'organization_id' => $orgId], $actingUser->getUserIdentifier());
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
$uo->setModifiedAt(new \DateTimeImmutable());
|
||||
|
|
@ -959,7 +716,7 @@ class UserController extends AbstractController
|
|||
$this->loggerService->logCritical('Error while resending invitation', [
|
||||
'target_user_id' => $user->getId(),
|
||||
'organization_id' => $orgId,
|
||||
'acting_user_id' => $actingUser->getId(),
|
||||
'acting_user_id' => $actingUser->getUserIdentifier(),
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
return $this->json(['message' => 'Erreur lors de l\'envoie du mail.'], Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
|
|
@ -972,8 +729,8 @@ class UserController extends AbstractController
|
|||
#[Route(path: '/accept-invitation', name: 'accept', methods: ['GET'])]
|
||||
public function acceptInvitation(Request $request): Response
|
||||
{
|
||||
$token = $request->get('token');
|
||||
$userId = $request->get('id');
|
||||
$token = $request->query->get('token');
|
||||
$userId = $request->query->get('id');
|
||||
|
||||
if (!$token || !$userId) {
|
||||
$this->loggerService->logEntityNotFound('Token or UserId missing in accept invitation', [
|
||||
|
|
|
|||
|
|
@ -61,12 +61,22 @@ class Organizations
|
|||
#[ORM\OneToMany(targetEntity: UserOrganizatonApp::class, mappedBy: 'organization')]
|
||||
private Collection $userOrganizatonApps;
|
||||
|
||||
#[ORM\Column(length: 4, nullable: true)]
|
||||
private ?string $projectPrefix = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Project>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: Project::class, mappedBy: 'organization')]
|
||||
private Collection $projects;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->apps = new ArrayCollection();
|
||||
$this->actions = new ArrayCollection();
|
||||
$this->createdAt = new \DateTimeImmutable();
|
||||
$this->userOrganizatonApps = new ArrayCollection();
|
||||
$this->projects = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
|
|
@ -256,4 +266,46 @@ class Organizations
|
|||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getProjectPrefix(): ?string
|
||||
{
|
||||
return $this->projectPrefix;
|
||||
}
|
||||
|
||||
public function setProjectPrefix(?string $projectPrefix): static
|
||||
{
|
||||
$this->projectPrefix = $projectPrefix;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -41,6 +41,9 @@ class UsersOrganizations
|
|||
#[ORM\Column(nullable: true)]
|
||||
private ?\DateTimeImmutable $modifiedAt = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
private ?Roles $role = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->isActive = true; // Default value for isActive
|
||||
|
|
@ -147,4 +150,16 @@ class UsersOrganizations
|
|||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRole(): ?Roles
|
||||
{
|
||||
return $this->role;
|
||||
}
|
||||
|
||||
public function setRole(?Roles $role): static
|
||||
{
|
||||
$this->role = $role;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,26 +30,21 @@ class UserSubscriber implements EventSubscriberInterface
|
|||
$user = $event->getNewUser();
|
||||
$actingUser = $event->getActingUser();
|
||||
|
||||
// 1. Generate Token (If logic was moved here, otherwise assume UserService set it)
|
||||
// If the token generation logic is still in UserService, just send the email here.
|
||||
// If you moved generating the token here, do it now.
|
||||
|
||||
// 2. Send Email
|
||||
// Note: You might need to pass the token in the Event if it's not stored in the DB entity
|
||||
// or generate a new one here if appropriate.
|
||||
// 1. Send Email
|
||||
if ($user->getPasswordToken()) {
|
||||
$this->emailService->sendPasswordSetupEmail($user, $user->getPasswordToken());
|
||||
}
|
||||
|
||||
// 3. Log the creation
|
||||
$this->loggerService->logUserCreated($user->getId(), $actingUser->getId());
|
||||
// 2. Logic-based Logging (Moved from Service)
|
||||
if (in_array('ROLE_ADMIN', $actingUser->getRoles(), true)) {
|
||||
$this->loggerService->logSuperAdmin(
|
||||
$user->getId(),
|
||||
$actingUser->getId(),
|
||||
"Super Admin created new user: " . $user->getUserIdentifier()
|
||||
);
|
||||
}
|
||||
|
||||
// 4. Create the Audit Action
|
||||
$this->actionService->createAction(
|
||||
"Create new user",
|
||||
$actingUser,
|
||||
null,
|
||||
$user->getUserIdentifier()
|
||||
);
|
||||
// 3. General Audit Trail
|
||||
$this->actionService->createAction("USER_CREATED", $actingUser, null, $user->getUserIdentifier());
|
||||
}
|
||||
}
|
||||
|
|
@ -3,8 +3,11 @@
|
|||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Organizations;
|
||||
use App\Entity\User;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\ORM\Tools\Pagination\Paginator;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use App\Entity\UsersOrganizations;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<Organizations>
|
||||
|
|
@ -16,28 +19,37 @@ class OrganizationsRepository extends ServiceEntityRepository
|
|||
parent::__construct($registry, Organizations::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return Organizations[] Returns an array of Organizations objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
// return $this->createQueryBuilder('o')
|
||||
// ->andWhere('o.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->orderBy('o.id', 'ASC')
|
||||
// ->setMaxResults(10)
|
||||
// ->getQuery()
|
||||
// ->getResult()
|
||||
// ;
|
||||
// }
|
||||
public function findAdmissibleOrganizations(User $user, bool $isSuperAdmin, int $page, int $size, array $filters = []): Paginator
|
||||
{
|
||||
$qb = $this->createQueryBuilder('o')
|
||||
->where('o.isDeleted = :del')
|
||||
->setParameter('del', false);
|
||||
|
||||
// public function findOneBySomeField($value): ?Organizations
|
||||
// {
|
||||
// return $this->createQueryBuilder('o')
|
||||
// ->andWhere('o.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
// 1. Security Logic: If not Super Admin, join UsersOrganizations to filter
|
||||
if (!$isSuperAdmin) {
|
||||
$qb->innerJoin(UsersOrganizations::class, 'uo', 'WITH', 'uo.organization = o')
|
||||
->andWhere('uo.users = :user')
|
||||
->andWhere('uo.role = :roleAdmin')
|
||||
->andWhere('uo.isActive = true')
|
||||
->setParameter('user', $user)
|
||||
// You can pass the actual Role entity or the string name depending on your mapping
|
||||
->setParameter('roleAdmin', $this->_em->getRepository(\App\Entity\Roles::class)->findOneBy(['name' => 'ADMIN']));
|
||||
}
|
||||
|
||||
// 2. Filters
|
||||
if (!empty($filters['name'])) {
|
||||
$qb->andWhere('o.name LIKE :name')
|
||||
->setParameter('name', '%' . $filters['name'] . '%');
|
||||
}
|
||||
if (!empty($filters['email'])) {
|
||||
$qb->andWhere('o.email LIKE :email')
|
||||
->setParameter('email', '%' . $filters['email'] . '%');
|
||||
}
|
||||
|
||||
// 3. Pagination
|
||||
$qb->setFirstResult(($page - 1) * $size)
|
||||
->setMaxResults($size);
|
||||
|
||||
return new Paginator($qb);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -4,11 +4,11 @@ namespace App\Repository;
|
|||
|
||||
use App\Entity\User;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\ORM\Tools\Pagination\Paginator;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||
use App\Entity\UsersOrganizations;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<User>
|
||||
|
|
@ -35,21 +35,33 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns active users that are NOT in any UsersOrganizations mapping.
|
||||
* Returns User entities.
|
||||
*
|
||||
* @return User[]
|
||||
* @param int $page
|
||||
* @param int $size
|
||||
* @param array $filters
|
||||
* @return Paginator
|
||||
*/
|
||||
public function findUsersWithoutOrganization(): array
|
||||
public function findActiveUsersForTabulator(int $page, int $size, array $filters = []): Paginator
|
||||
{
|
||||
$qb = $this->createQueryBuilder('u')
|
||||
->select('u')
|
||||
->leftJoin(UsersOrganizations::class, 'uo', 'WITH', 'uo.users = u')
|
||||
->andWhere('u.isDeleted = :uDeleted')
|
||||
->andWhere('uo.id IS NULL')
|
||||
->orderBy('u.surname', 'ASC')
|
||||
->setParameter('uDeleted', false);
|
||||
->where('u.isDeleted = :del')
|
||||
->setParameter('del', false);
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
if (!empty($filters['name'])) {
|
||||
$qb->andWhere('u.surname LIKE :name')
|
||||
->setParameter('name', '%' . $filters['name'] . '%');
|
||||
}
|
||||
if (!empty($filters['prenom'])) {
|
||||
$qb->andWhere('u.name LIKE :prenom')
|
||||
->setParameter('prenom', '%' . $filters['prenom'] . '%');
|
||||
}
|
||||
if (!empty($filters['email'])) {
|
||||
$qb->andWhere('u.email LIKE :email')
|
||||
->setParameter('email', '%' . $filters['email'] . '%');
|
||||
}
|
||||
|
||||
$qb->setFirstResult(($page - 1) * $size)
|
||||
->setMaxResults($size);
|
||||
|
||||
return new Paginator($qb);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@ use App\Entity\Organizations;
|
|||
use App\Entity\User;
|
||||
use App\Entity\UsersOrganizations;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\ORM\Tools\Pagination\Paginator;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<UsersOrganizations>
|
||||
*/
|
||||
|
|
@ -19,121 +21,64 @@ class UsersOrganizationsRepository extends ServiceEntityRepository
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns active user-organization mappings with joined User and Organization.
|
||||
* Only active and non-deleted users and organizations are included.
|
||||
*
|
||||
* @return UsersOrganizations[]
|
||||
* Checks if an acting user has administrative rights over a target user
|
||||
* based on shared organizational memberships.
|
||||
*/
|
||||
public function findUsersWithOrganization(array $organizationIds = null): array
|
||||
public function isUserAdminOfTarget(User $actingUser, User $targetUser, $adminRole): bool
|
||||
{
|
||||
$qb = $this->createQueryBuilder('uo_acting');
|
||||
|
||||
return (bool) $qb
|
||||
->select('COUNT(uo_acting.id)')
|
||||
// We join the same table again to find the target user in the same organization
|
||||
->innerJoin(
|
||||
UsersOrganizations::class,
|
||||
'uo_target',
|
||||
'WITH',
|
||||
'uo_target.organization = uo_acting.organization'
|
||||
)
|
||||
->where('uo_acting.users = :actingUser')
|
||||
->andWhere('uo_acting.role = :role')
|
||||
->andWhere('uo_acting.isActive = true')
|
||||
->andWhere('uo_target.users = :targetUser')
|
||||
->andWhere('uo_target.statut = :status')
|
||||
->setParameter('actingUser', $actingUser)
|
||||
->setParameter('targetUser', $targetUser)
|
||||
->setParameter('role', $adminRole)
|
||||
->setParameter('status', 'ACCEPTED')
|
||||
->getQuery()
|
||||
->getSingleScalarResult() > 0;
|
||||
}
|
||||
|
||||
public function findByOrganizationWithFilters(Organizations $org, int $page, int $size, array $filters = []): Paginator
|
||||
{
|
||||
$qb = $this->createQueryBuilder('uo')
|
||||
->addSelect('u', 'o')
|
||||
->leftJoin('uo.users', 'u')
|
||||
->leftJoin('uo.organization', 'o')
|
||||
->andWhere('u.isActive = :uActive')
|
||||
->andWhere('u.isDeleted = :uDeleted')
|
||||
->andWhere('o.isActive = :oActive')
|
||||
->andWhere('o.isDeleted = :oDeleted')
|
||||
->orderBy('o.name', 'ASC')
|
||||
->addOrderBy('u.surname', 'ASC')
|
||||
->setParameter('uActive', true)
|
||||
->setParameter('uDeleted', false)
|
||||
->setParameter('oActive', true)
|
||||
->setParameter('oDeleted', false);
|
||||
if (!empty($organizationIds)) {
|
||||
$qb->andWhere('o.id IN (:orgIds)')
|
||||
->setParameter('orgIds', $organizationIds);
|
||||
->innerJoin('uo.users', 'u')
|
||||
->where('uo.organization = :org')
|
||||
->setParameter('org', $org);
|
||||
|
||||
// Apply filters
|
||||
if (!empty($filters['name'])) {
|
||||
$qb->andWhere('u.surname LIKE :name')
|
||||
->setParameter('name', '%' . $filters['name'] . '%');
|
||||
}
|
||||
if (!empty($filters['prenom'])) {
|
||||
$qb->andWhere('u.name LIKE :prenom')
|
||||
->setParameter('prenom', '%' . $filters['prenom'] . '%');
|
||||
}
|
||||
if (!empty($filters['email'])) {
|
||||
$qb->andWhere('u.email LIKE :email')
|
||||
->setParameter('email', '%' . $filters['email'] . '%');
|
||||
}
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
// Apply complex sorting
|
||||
$qb->orderBy('uo.isActive', 'DESC')
|
||||
->addOrderBy("CASE WHEN uo.statut = 'INVITED' THEN 0 ELSE 1 END", 'ASC');
|
||||
|
||||
/**
|
||||
* Same as above, filtered by a list of organization IDs.
|
||||
*
|
||||
* @param int[] $organizationIds
|
||||
* @return UsersOrganizations[]
|
||||
*/
|
||||
public function findActiveWithUserAndOrganizationByOrganizationIds(array $organizationIds): array
|
||||
{
|
||||
if (empty($organizationIds)) {
|
||||
return [];
|
||||
}
|
||||
// Pagination
|
||||
$qb->setFirstResult(($page - 1) * $size)
|
||||
->setMaxResults($size);
|
||||
|
||||
$qb = $this->createQueryBuilder('uo')
|
||||
->addSelect('u', 'o')
|
||||
->leftJoin('uo.users', 'u')
|
||||
->leftJoin('uo.organization', 'o')
|
||||
->where('uo.isActive = :uoActive')
|
||||
->andWhere('u.isActive = :uActive')
|
||||
->andWhere('u.isDeleted = :uDeleted')
|
||||
->andWhere('o.isActive = :oActive')
|
||||
->andWhere('o.isDeleted = :oDeleted')
|
||||
->andWhere('o.id IN (:orgIds)')
|
||||
->orderBy('o.name', 'ASC')
|
||||
->addOrderBy('u.surname', 'ASC')
|
||||
->setParameter('uoActive', true)
|
||||
->setParameter('uActive', true)
|
||||
->setParameter('uDeleted', false)
|
||||
->setParameter('oActive', true)
|
||||
->setParameter('oDeleted', false)
|
||||
->setParameter('orgIds', $organizationIds);
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find 10 newest Users in an Organization.
|
||||
*
|
||||
* @param Organizations $organization
|
||||
* @return User[]
|
||||
*/
|
||||
public function findNewestUO(Organizations $organization): array
|
||||
{
|
||||
$qb = $this->createQueryBuilder('uo')
|
||||
->select('uo', 'u')
|
||||
->leftJoin('uo.users', 'u')
|
||||
->where('uo.organization = :org')
|
||||
->andWhere('uo.isActive = :uoActive')
|
||||
->andWhere('u.isActive = :uActive')
|
||||
->andWhere('u.isDeleted = :uDeleted')
|
||||
->orderBy('u.createdAt', 'DESC')
|
||||
->setMaxResults(10)
|
||||
->setParameter('org', $organization)
|
||||
->setParameter('uoActive', true)
|
||||
->setParameter('uActive', true)
|
||||
->setParameter('uDeleted', false);
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all the admins of an Organization.
|
||||
* limited to 10 results.
|
||||
*
|
||||
* @param Organizations $organization
|
||||
* @return User[]
|
||||
*/
|
||||
public function findAdminsInOrganization(Organizations $organization): array
|
||||
{
|
||||
$qb = $this->createQueryBuilder('uo')
|
||||
->select('uo', 'u')
|
||||
->leftJoin('uo.users', 'u')
|
||||
->leftJoin('uo.userOrganizatonApps', 'uoa')
|
||||
->leftJoin('uoa.role', 'r')
|
||||
->where('uo.organization = :org')
|
||||
->andWhere('uo.isActive = :uoActive')
|
||||
->andWhere('u.isActive = :uActive')
|
||||
->andWhere('u.isDeleted = :uDeleted')
|
||||
->andWhere('r.name = :roleAdmin')
|
||||
->orderBy('u.surname', 'ASC')
|
||||
->setMaxResults(10)
|
||||
->setParameter('org', $organization)
|
||||
->setParameter('uoActive', true)
|
||||
->setParameter('uActive', true)
|
||||
->setParameter('uDeleted', false)
|
||||
->setParameter('roleAdmin', 'ADMIN');
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
return new Paginator($qb);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ readonly class LoggerService
|
|||
|
||||
|
||||
// User Management Logs
|
||||
public function logUserCreated(int $userId, int $actingUserId): void
|
||||
public function logUserCreated(int|string $userId, int|string $actingUserId): void
|
||||
{
|
||||
$this->userManagementLogger->notice("New user created: $userId", [
|
||||
'target_user_id' => $userId,
|
||||
|
|
@ -34,7 +34,7 @@ readonly class LoggerService
|
|||
}
|
||||
|
||||
// Organization Management Logs
|
||||
public function logUserOrganizationLinkCreated(int $userId, int $orgId, int $actingUserId, ?int $uoId): void
|
||||
public function logUserOrganizationLinkCreated(int|string $userId, int $orgId, int|string $actingUserId, ?int $uoId): void
|
||||
{
|
||||
$this->organizationManagementLogger->notice('User-Organization link created', [
|
||||
'target_user_id' => $userId,
|
||||
|
|
@ -46,7 +46,7 @@ readonly class LoggerService
|
|||
]);
|
||||
}
|
||||
|
||||
public function logExistingUserAddedToOrg(int $userId, int $orgId, int $actingUserId, int $uoId): void
|
||||
public function logExistingUserAddedToOrg(int|string $userId, int $orgId, int|string $actingUserId, int $uoId): void
|
||||
{
|
||||
$this->organizationManagementLogger->notice('Existing user added to organization', [
|
||||
'target_user_id' => $userId,
|
||||
|
|
@ -59,7 +59,7 @@ readonly class LoggerService
|
|||
}
|
||||
|
||||
// Email Notification Logs
|
||||
public function logEmailSent(int $userId, ?int $orgId, string $message): void
|
||||
public function logEmailSent(int|string $userId, ?int $orgId, string $message): void
|
||||
{
|
||||
$this->emailNotificationLogger->notice($message, [
|
||||
'target_user_id' => $userId,
|
||||
|
|
@ -69,7 +69,7 @@ readonly class LoggerService
|
|||
]);
|
||||
}
|
||||
|
||||
public function logExistingUserNotificationSent(int $userId, int $orgId): void
|
||||
public function logExistingUserNotificationSent(int|string $userId, int $orgId): void
|
||||
{
|
||||
$this->emailNotificationLogger->notice("Existing user notification email sent to $userId", [
|
||||
'target_user_id' => $userId,
|
||||
|
|
@ -87,7 +87,7 @@ readonly class LoggerService
|
|||
]));
|
||||
}
|
||||
|
||||
public function logSuperAdmin(int $userId, int $actingUserId, string $message, ?int $orgId = null): void
|
||||
public function logSuperAdmin(int|string $userId, int|string $actingUserId, string $message, ?int $orgId = null): void
|
||||
{
|
||||
$this->adminActionsLogger->notice($message, [
|
||||
'target_user_id' => $userId,
|
||||
|
|
@ -116,7 +116,7 @@ readonly class LoggerService
|
|||
}
|
||||
|
||||
// Security Logs
|
||||
public function logAccessDenied(?int $actingUserId): void
|
||||
public function logAccessDenied(int|string $actingUserId): void
|
||||
{
|
||||
$this->securityLogger->warning('Access denied', [
|
||||
'acting_user_id' => $actingUserId,
|
||||
|
|
@ -133,7 +133,7 @@ readonly class LoggerService
|
|||
}
|
||||
|
||||
|
||||
public function logUserAction(int $targetId, int $actingUserId, string $message): void
|
||||
public function logUserAction(int $targetId, int|string $actingUserId, string $message): void
|
||||
{
|
||||
$this->userManagementLogger->notice($message, [
|
||||
'target_user_id'=> $targetId,
|
||||
|
|
@ -143,7 +143,7 @@ readonly class LoggerService
|
|||
]);
|
||||
}
|
||||
|
||||
public function logAdminAction(int $targetId, int $actingUserId, int $organizationId, string $message): void
|
||||
public function logAdminAction(int $targetId, int|string $actingUserId, int $organizationId, string $message): void
|
||||
{
|
||||
$this->adminActionsLogger->notice($message, [
|
||||
'target_id' => $targetId,
|
||||
|
|
@ -154,7 +154,7 @@ readonly class LoggerService
|
|||
]);
|
||||
}
|
||||
|
||||
public function logEntityNotFound(string $entityType, array $criteria, ?int $actingUserId): void
|
||||
public function logEntityNotFound(string $entityType, array $criteria, int|string $actingUserId): void
|
||||
{
|
||||
$this->errorLogger->error('Entity not found', array_merge($criteria, [
|
||||
'entity_type' => $entityType,
|
||||
|
|
@ -192,7 +192,7 @@ readonly class LoggerService
|
|||
]);
|
||||
}
|
||||
|
||||
public function logOrganizationInformation(int $organizationId, int $actingUserId, string $message): void
|
||||
public function logOrganizationInformation(int $organizationId, int|string $actingUserId, string $message): void
|
||||
{
|
||||
$this->organizationManagementLogger->info($message, [
|
||||
'organization_id' => $organizationId,
|
||||
|
|
@ -202,7 +202,7 @@ readonly class LoggerService
|
|||
]);
|
||||
}
|
||||
|
||||
public function logRoleEntityAssignment(int $userId, int $organizationId, int $roleId, int $actingUserId, string $message): void
|
||||
public function logRoleEntityAssignment(int|string $userId, int $organizationId, int $roleId, int|string $actingUserId, string $message): void
|
||||
{
|
||||
$this->accessControlLogger->info($message, [
|
||||
'target_user_id' => $userId,
|
||||
|
|
@ -252,7 +252,7 @@ readonly class LoggerService
|
|||
]));
|
||||
}
|
||||
|
||||
public function logApplicationInformation(string $string, array $array, int $actingUser)
|
||||
public function logApplicationInformation(string $string, array $array, int|string $actingUser)
|
||||
{
|
||||
$this->accessControlLogger->info($string, array_merge($array, [
|
||||
'acting_user_id' => $actingUser,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ use App\Entity\Actions;
|
|||
use App\Entity\Organizations;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UsersOrganizations;
|
||||
use App\Repository\RolesRepository;
|
||||
use App\Service\ActionService;
|
||||
use App\Service\LoggerService;
|
||||
use \App\Service\UserOrganizationAppService;
|
||||
|
|
@ -20,7 +21,7 @@ readonly class UserOrganizationService
|
|||
{
|
||||
|
||||
public function __construct(
|
||||
private userOrganizationAppService $userOrganizationAppService, private EntityManagerInterface $entityManager, private ActionService $actionService, private LoggerService $loggerService,
|
||||
private userOrganizationAppService $userOrganizationAppService, private EntityManagerInterface $entityManager, private ActionService $actionService, private LoggerService $loggerService, private RolesRepository $rolesRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -55,6 +56,17 @@ readonly class UserOrganizationService
|
|||
|
||||
}
|
||||
|
||||
public function getAdminOrganizationsForUser(User $user): array
|
||||
{
|
||||
$adminRole = $this->rolesRepository->findOneBy(['name' => "ADMIN"]); // Assuming 'ADMIN' is the role name for administrators
|
||||
$uos = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['users' => $user, 'role' => $adminRole, 'isActive' => true]);
|
||||
$adminOrgs = [];
|
||||
foreach ($uos as $uo) {
|
||||
$adminOrgs[] = $uo->getOrganization();
|
||||
}
|
||||
return $adminOrgs;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ use App\Entity\Roles;
|
|||
use App\Entity\User;
|
||||
use App\Entity\UserOrganizatonApp;
|
||||
use App\Entity\UsersOrganizations;
|
||||
use App\Repository\RolesRepository;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
|
@ -33,7 +34,7 @@ class UserService
|
|||
private readonly ActionService $actionService,
|
||||
private readonly EmailService $emailService,
|
||||
private readonly OrganizationsService $organizationsService,
|
||||
private readonly EventDispatcherInterface $eventDispatcher
|
||||
private readonly EventDispatcherInterface $eventDispatcher, private readonly RolesRepository $rolesRepository
|
||||
)
|
||||
{
|
||||
|
||||
|
|
@ -48,6 +49,23 @@ class UserService
|
|||
return bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
/** Check if the user is admin in any organization.
|
||||
* Return true if the user is admin in at least one organization, false otherwise.
|
||||
*
|
||||
* @param User $user
|
||||
* @return bool
|
||||
* @throws Exception
|
||||
*/
|
||||
// TODO: pas sur de l'utiliser, à vérifier
|
||||
public function isAdminInAnyOrganization(User $user): bool
|
||||
{
|
||||
$roleAdmin = $this->rolesRepository->findOneBy(['name' => 'ADMIN']);
|
||||
$uoAdmin = $this->entityManager->getRepository(UsersOrganizations::class)->findOneBy([
|
||||
'users' => $user,
|
||||
'isActive' => true,
|
||||
'role'=> $roleAdmin]);
|
||||
return $uoAdmin !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user is currently connected.
|
||||
|
|
@ -75,26 +93,30 @@ class UserService
|
|||
}
|
||||
|
||||
/**
|
||||
* Check if the user have the rights to access the page
|
||||
* Self check can be skipped when checking access for the current user
|
||||
* Determines if the currently logged-in user has permission to manage or view a target User.
|
||||
* * Access is granted if:
|
||||
* 1. The current user is a Super Admin.
|
||||
* 2. The current user is the target user itself.
|
||||
* 3. The current user is an active Admin of an organization the target user belongs to.
|
||||
*
|
||||
* @param User $user
|
||||
* @param bool $skipSelfCheck
|
||||
* @return bool
|
||||
* @throws Exception
|
||||
* @param User $user The target User object we are checking access against.
|
||||
* * @return bool True if access is permitted, false otherwise.
|
||||
* @throws Exception If database or security context issues occur.
|
||||
*/
|
||||
public function hasAccessTo(User $user, bool $skipSelfCheck = false): bool
|
||||
public function hasAccessTo(User $user): bool
|
||||
{
|
||||
if ($this->security->isGranted('ROLE_SUPER_ADMIN')) {
|
||||
if ($this->security->isGranted('ROLE_ADMIN')) {
|
||||
return true;
|
||||
}
|
||||
if (!$skipSelfCheck && $user->getUserIdentifier() === $this->security->getUser()->getUserIdentifier()) {
|
||||
// S'il s'agit de son propre compte, on lui donne accès
|
||||
if ($user->getUserIdentifier() === $this->security->getUser()->getUserIdentifier()) {
|
||||
return true;
|
||||
}
|
||||
$userOrganizations = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['users' => $user]);
|
||||
if ($userOrganizations) {
|
||||
foreach ($userOrganizations as $uo) {
|
||||
if ($this->isAdminOfOrganization($uo->getOrganization()) && $uo->getStatut() === "ACCEPTED" && $uo->isActive()) {
|
||||
//l'utilisateur doit être actif dans l'org, avoir le statut ACCEPTED (double vérif) et être admin de l'org
|
||||
if ($uo->getStatut() === "ACCEPTED" && $uo->isActive() && $this->isAdminOfOrganization($uo->getOrganization())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -103,11 +125,36 @@ class UserService
|
|||
|
||||
}
|
||||
|
||||
/* Return if the current user is an admin of the target user.
|
||||
* This is true if the current user is an admin of at least one organization that the target user belongs to.
|
||||
*
|
||||
* @param User $user
|
||||
* @return bool
|
||||
* @throws Exception
|
||||
*/
|
||||
public function isAdminOfUser(User $user): bool
|
||||
{
|
||||
$actingUser = $this->security->getUser();
|
||||
|
||||
if (!$actingUser instanceof User) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reuse the cached/fetched role
|
||||
$adminRole = $this->rolesRepository->findOneBy(['name' => 'ADMIN']);
|
||||
|
||||
if (!$adminRole) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->entityManager
|
||||
->getRepository(UsersOrganizations::class)
|
||||
->isUserAdminOfTarget($actingUser, $user, $adminRole);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user is an admin of the organization
|
||||
* A user is considered an admin of an organization if they have the 'ROLE_ADMIN' AND have the link to the
|
||||
* entity role 'ROLE_ADMIN' in the UsersOrganizationsApp entity
|
||||
* (if he is admin for any application of the organization).
|
||||
* Check if the acting user is an admin of the organization
|
||||
* A user is considered an admin of an organization if they have an active UsersOrganizations link with the role of ADMIN for that organization.
|
||||
*
|
||||
* @param Organizations $organizations
|
||||
* @return bool
|
||||
|
|
@ -115,20 +162,15 @@ class UserService
|
|||
*/
|
||||
public function isAdminOfOrganization(Organizations $organizations): bool
|
||||
{
|
||||
$actingUser = $this->getUserByIdentifier($this->security->getUser()->getUserIdentifier());
|
||||
$uo = $this->entityManager->getRepository(UsersOrganizations::class)->findOneBy(['users' => $actingUser, 'organization' => $organizations]);
|
||||
$actingUser =$this->security->getUser();
|
||||
$roleAdmin = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN']);
|
||||
if ($uo) {
|
||||
$uoa = $this->entityManager->getRepository(UserOrganizatonApp::class)->findOneBy(['userOrganization' => $uo,
|
||||
'role' => $roleAdmin,
|
||||
'isActive' => true]);
|
||||
if ($uoa && $this->security->isGranted('ROLE_ADMIN')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
$uoAdmin = $this->entityManager->getRepository(UsersOrganizations::class)->findOneBy(['users' => $actingUser,
|
||||
'organization' => $organizations,
|
||||
'role'=> $roleAdmin,
|
||||
'isActive' => true]);
|
||||
return $uoAdmin !== null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user by their identifier.
|
||||
|
|
@ -499,14 +541,7 @@ class UserService
|
|||
$user->setIsActive(true);
|
||||
$this->entityManager->persist($user);
|
||||
}
|
||||
$uo = new UsersOrganizations();
|
||||
$uo->setUsers($user);
|
||||
$uo->setOrganization($organization);
|
||||
$uo->setStatut("INVITED");
|
||||
$uo->setIsActive(false);
|
||||
$uo->setModifiedAt(new \DateTimeImmutable('now'));
|
||||
$this->entityManager->persist($uo);
|
||||
$this->entityManager->flush();
|
||||
$uo = $this->linkUserToOrganization($user, $organization);
|
||||
|
||||
return $uo->getId();
|
||||
}
|
||||
|
|
@ -554,12 +589,11 @@ class UserService
|
|||
public function addExistingUserToOrganization(
|
||||
User $existingUser,
|
||||
Organizations $org,
|
||||
User $actingUser,
|
||||
): int
|
||||
{
|
||||
try {
|
||||
$uoId = $this->handleExistingUser($existingUser, $org);
|
||||
|
||||
$actingUser = $this->getUserByIdentifier($this->security->getUser()->getUserIdentifier());
|
||||
$this->loggerService->logExistingUserAddedToOrg(
|
||||
$existingUser->getId(),
|
||||
$org->getId(),
|
||||
|
|
@ -593,20 +627,16 @@ class UserService
|
|||
try {
|
||||
$this->formatUserData($user, $picture, true);
|
||||
|
||||
// Generate token here if it's part of the user persistence flow
|
||||
$token = $this->generatePasswordToken($user);
|
||||
$user->setisActive(false);
|
||||
$this->generatePasswordToken($user); // Set token on the entity
|
||||
|
||||
$this->entityManager->persist($user);
|
||||
$this->entityManager->flush();
|
||||
|
||||
|
||||
$this->eventDispatcher->dispatch(new UserCreatedEvent($user, $actingUser));
|
||||
|
||||
} catch (\Exception $e) {
|
||||
// Error logging remains here because the event won't fire if exception occurs
|
||||
$this->loggerService->logError('Error creating new user: ' . $e->getMessage(), [
|
||||
'target_user_email' => $user->getEmail(),
|
||||
'acting_user_id' => $actingUser->getId(),
|
||||
]);
|
||||
$this->loggerService->logError('Error creating user: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
|
@ -617,15 +647,17 @@ class UserService
|
|||
public function linkUserToOrganization(
|
||||
User $user,
|
||||
Organizations $org,
|
||||
User $actingUser,
|
||||
): UsersOrganizations
|
||||
{
|
||||
$actingUser = $this->getUserByIdentifier($this->security->getUser()->getUserIdentifier());
|
||||
try {
|
||||
$roleUser = $this->rolesRepository->findOneBy(['name' => 'USER']);
|
||||
$uo = new UsersOrganizations();
|
||||
$uo->setUsers($user);
|
||||
$uo->setOrganization($org);
|
||||
$uo->setStatut("INVITED");
|
||||
$uo->setIsActive(false);
|
||||
$uo->setRole($roleUser);
|
||||
$uo->setModifiedAt(new \DateTimeImmutable('now'));
|
||||
$this->entityManager->persist($uo);
|
||||
$this->entityManager->flush();
|
||||
|
|
@ -644,6 +676,15 @@ class UserService
|
|||
$org,
|
||||
"Added {$user->getUserIdentifier()} to {$org->getName()}"
|
||||
);
|
||||
$auRoles = $actingUser->getRoles();
|
||||
if (in_array('ROLE_ADMIN', $auRoles, true)) {
|
||||
$this->loggerService->logSuperAdmin(
|
||||
$user->getId(),
|
||||
$actingUser->getId(),
|
||||
"Admin linked user to organization during creation",
|
||||
$org->getId()
|
||||
);
|
||||
}
|
||||
|
||||
$this->sendNewUserNotifications($user, $org, $actingUser);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
{% set current_route = app.request.attributes.get('_route') %}
|
||||
|
||||
<ul class="nav">
|
||||
{% if is_granted("ROLE_SUPER_ADMIN") %}
|
||||
{% if is_granted("ROLE_ADMIN") %}
|
||||
{# 2. Check if route is 'app_index' #}
|
||||
<li class="nav-item {{ current_route == 'app_index' ? 'active' : '' }}">
|
||||
<a class="nav-link" href="{{ path('app_index') }}">
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
</a>
|
||||
</li>
|
||||
|
||||
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
||||
{% if is_granted('ROLE_ADMIN') %}
|
||||
<li class="nav-item {{ current_route starts with 'user_' ? 'active' : '' }}">
|
||||
<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>
|
||||
|
|
@ -30,13 +30,13 @@
|
|||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="nav-item {{ current_route starts with 'organization_' ? 'active' : '' }}">
|
||||
{% if is_granted('ROLE_ADMIN') %}
|
||||
{% if can_view_org_menu() %}
|
||||
<li class="nav-item {{ current_route starts with 'organization_' ? 'active' : '' }}">
|
||||
<a class="nav-link" href="{{ path('organization_index') }}">
|
||||
<i class="icon-grid menu-icon"> {{ ux_icon('bi:buildings', {height: '15px', width: '15px'}) }}</i>
|
||||
<span class="menu-title">Organizations</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block body %}
|
||||
{% set isSA = is_granted('ROLE_SUPER_ADMIN')%}
|
||||
<div class="w-100 h-100 p-5 m-auto">
|
||||
{% for type, messages in app.flashes %}
|
||||
{% for message in messages %}
|
||||
|
|
@ -18,7 +19,7 @@
|
|||
<h1 class="mb-4 ms-3">{{ organization.name|title }} - Dashboard</h1>
|
||||
</div>
|
||||
<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
|
||||
l'organisation</a>
|
||||
<form method="POST" action="{{ path('organization_delete', {'id': organization.id}) }}"
|
||||
|
|
@ -101,20 +102,61 @@
|
|||
</div>
|
||||
|
||||
{# APPLICATION ROW #}
|
||||
{# TODO: Weird gap not going away #}
|
||||
<div class="row mb-3 ">
|
||||
{% for application in applications %}
|
||||
<div class="col-6 mb-3">
|
||||
{% include 'application/appSmall.html.twig' with {
|
||||
application: application
|
||||
} %}
|
||||
{# TODO:remove app acces and replace wioth project overview#}
|
||||
<div class="row mb-3 card no-header-bg"
|
||||
data-controller="project"
|
||||
data-project-list-project-value="true"
|
||||
data-project-org-id-value="{{ organization.id }}"
|
||||
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>
|
||||
{% endfor %}
|
||||
</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>
|
||||
</div>
|
||||
{# Activities col #}
|
||||
<div class="col-3 m-auto">
|
||||
<div class="card border-0"
|
||||
<div class="card "
|
||||
data-controller="organization"
|
||||
data-organization-activities-value = "true"
|
||||
data-organization-id-value="{{ organization.id }}">
|
||||
|
|
@ -122,7 +164,7 @@
|
|||
<div class="card-header d-flex justify-content-between align-items-center border-0">
|
||||
<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
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -148,6 +190,7 @@
|
|||
|
||||
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
{% block title %}User Profile{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
||||
{% if is_granted('ROLE_ADMIN') %}
|
||||
<div class="w-100 h-100 p-5 m-auto">
|
||||
{% for type, messages in app.flashes %}
|
||||
{% for message in messages %}
|
||||
|
|
@ -40,7 +40,7 @@
|
|||
|
||||
{% else %}
|
||||
<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>
|
||||
<p>Vous n'avez pas les permissions nécessaires pour voir la liste des utilisateurs.</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
{% block body %}
|
||||
|
||||
<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 message in messages %}
|
||||
<div class="alert alert-{{ type }}">
|
||||
|
|
@ -11,20 +11,22 @@
|
|||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
||||
{% if is_granted("ROLE_ADMIN") %}
|
||||
<div class="card-header border-0 d-flex justify-content-between align-items-center ">
|
||||
<div class="card-title">
|
||||
<h1>Gestion Utilisateur</h1>
|
||||
</div>
|
||||
<div class="card-title"><h1>Gestion Utilisateur</h1></div>
|
||||
<div class="d-flex gap-2">
|
||||
{% if is_granted("ROLE_SUPER_ADMIN") %}
|
||||
<a href="{{ path('user_delete', {'id': user.id}) }}"
|
||||
class="btn btn-secondary">Supprimer</a>
|
||||
{% if is_granted("ROLE_ADMIN") %}
|
||||
<button
|
||||
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 %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card-body">
|
||||
|
||||
|
|
@ -34,103 +36,107 @@
|
|||
<div class="card border-0 no-header-bg ">
|
||||
<div class="card-header">
|
||||
<div class="card-title">
|
||||
<h1>Vos applications</h1>
|
||||
<h1>Information d'organisation</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row g-2">
|
||||
{% for app in apps %}
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header d-flex gap-2">
|
||||
{% if app.logoMiniUrl %}
|
||||
<img src="{{ asset(application.entity.logoMiniUrl) }}" alt="Logo {{ app.name }}"
|
||||
class="rounded-circle" style="width:50px; height:50px;">
|
||||
{% endif %}
|
||||
<div class="card-title">
|
||||
<h1>{{ app.name|title }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<p>
|
||||
<b>Description :</b>
|
||||
{{ app.descriptionSmall|default('Aucune description disponible.')|raw }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{# find appGroup once, used in both editable and read-only branches #}
|
||||
{% set appGroup = data.uoas[app.id]|default(null) %}
|
||||
|
||||
{% if canEdit %}
|
||||
<form method="POST"
|
||||
action="{{ path('user_application_role', { id: data.singleUo.id }) }}">
|
||||
<div class="form-group mb-3">
|
||||
<label for="roles-{{ app.id }}"><b>Rôles :</b></label>
|
||||
<div class="form-check">
|
||||
{% if appGroup %}
|
||||
{% for role in data.rolesArray %}
|
||||
<input class="form-check-input" type="checkbox"
|
||||
name="roles[]"
|
||||
value="{{ role.id }}"
|
||||
id="role-{{ role.id }}-app-{{ app.id }}"
|
||||
{% if role.id in appGroup.selectedRoleIds %}checked{% endif %}>
|
||||
<label class="form-check"
|
||||
for="role-{{ role.id }}-app-{{ app.id }}">
|
||||
{% if role.name == 'USER' %}
|
||||
Accès
|
||||
{% else %}
|
||||
{{ role.name|capitalize }}
|
||||
{% endif %}
|
||||
</label>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="text-muted">Aucun rôle défini pour cette application.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<button type="submit" name="appId" value="{{ app.id }}"
|
||||
class="btn btn-primary mt-2">
|
||||
Sauvegarder
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="form-group mb-3">
|
||||
<label for="roles-{{ app.id }}"><b>Rôles :</b></label>
|
||||
<div class="form-check">
|
||||
{% if appGroup %}
|
||||
{% for role in data.rolesArray %}
|
||||
<input class="form-check-input" type="checkbox"
|
||||
disabled
|
||||
name="roles[]"
|
||||
value="{{ role.id }}"
|
||||
id="role-{{ role.id }}-app-{{ app.id }}"
|
||||
{% if role.id in appGroup.selectedRoleIds %}checked{% endif %}>
|
||||
<label class="form-check"
|
||||
for="role-{{ role.id }}-app-{{ app.id }}">
|
||||
{% if role.name == 'USER' %}
|
||||
Accès
|
||||
{% else %}
|
||||
{{ role.name|capitalize }}
|
||||
{% endif %}
|
||||
</label>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="text-muted">Aucun rôle défini pour cette application.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</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="row g-2">#}
|
||||
{# {% for app in apps %}#}
|
||||
{# <div class="col-12 col-md-6">#}
|
||||
{# <div class="card h-100">#}
|
||||
{# <div class="card-header d-flex gap-2">#}
|
||||
{# {% if app.logoMiniUrl %}#}
|
||||
{# <img src="{{ asset(appli.entity.logoMiniUrl) }}" alt="Logo {{ app.name }}"#}
|
||||
{# class="rounded-circle" style="width:50px; height:50px;">#}
|
||||
{# {% endif %}#}
|
||||
{# <div class="card-title">#}
|
||||
{# <h1>{{ app.name|title }}</h1>#}
|
||||
{# </div>#}
|
||||
{# </div>#}
|
||||
|
||||
{# <div class="card-body">#}
|
||||
{# <div class="row">#}
|
||||
{# <p>#}
|
||||
{# <b>Description :</b>#}
|
||||
{# {{ app.descriptionSmall|default('Aucune description disponible.')|raw }}#}
|
||||
{# </p>#}
|
||||
{# </div>#}
|
||||
|
||||
{# #}{# find appGroup once, used in both editable and read-only branches #}
|
||||
{# {% set appGroup = data.uoas[app.id]|default(null) %}#}
|
||||
|
||||
{# {% if canEdit %}#}
|
||||
{# <form method="POST"#}
|
||||
{# action="{{ path('user_application_role', { id: data.singleUo.id }) }}">#}
|
||||
{# <div class="form-group mb-3">#}
|
||||
{# <label for="roles-{{ app.id }}"><b>Rôles :</b></label>#}
|
||||
{# <div class="form-check">#}
|
||||
{# {% if appGroup %}#}
|
||||
{# {% for role in data.rolesArray %}#}
|
||||
{# <input class="form-check-input" type="checkbox"#}
|
||||
{# name="roles[]"#}
|
||||
{# value="{{ role.id }}"#}
|
||||
{# id="role-{{ role.id }}-app-{{ app.id }}"#}
|
||||
{# {% if role.id in appGroup.selectedRoleIds %}checked{% endif %}>#}
|
||||
{# <label class="form-check"#}
|
||||
{# for="role-{{ role.id }}-app-{{ app.id }}">#}
|
||||
{# {% if role.name == 'USER' %}#}
|
||||
{# Accès#}
|
||||
{# {% else %}#}
|
||||
{# {{ role.name|capitalize }}#}
|
||||
{# {% endif %}#}
|
||||
{# </label>#}
|
||||
{# {% endfor %}#}
|
||||
{# {% else %}#}
|
||||
{# <p class="text-muted">Aucun rôle défini pour cette application.</p>#}
|
||||
{# {% endif %}#}
|
||||
{# </div>#}
|
||||
{# <button type="submit" name="appId" value="{{ app.id }}"#}
|
||||
{# class="btn btn-primary mt-2">#}
|
||||
{# Sauvegarder#}
|
||||
{# </button>#}
|
||||
{# </div>#}
|
||||
{# </form>#}
|
||||
{# {% else %}#}
|
||||
{# <div class="form-group mb-3">#}
|
||||
{# <label for="roles-{{ app.id }}"><b>Rôles :</b></label>#}
|
||||
{# <div class="form-check">#}
|
||||
{# {% if appGroup %}#}
|
||||
{# {% for role in data.rolesArray %}#}
|
||||
{# <input class="form-check-input" type="checkbox"#}
|
||||
{# disabled#}
|
||||
{# name="roles[]"#}
|
||||
{# value="{{ role.id }}"#}
|
||||
{# id="role-{{ role.id }}-app-{{ app.id }}"#}
|
||||
{# {% if role.id in appGroup.selectedRoleIds %}checked{% endif %}>#}
|
||||
{# <label class="form-check"#}
|
||||
{# for="role-{{ role.id }}-app-{{ app.id }}">#}
|
||||
{# {% if role.name == 'USER' %}#}
|
||||
{# Accès#}
|
||||
{# {% else %}#}
|
||||
{# {{ role.name|capitalize }}#}
|
||||
{# {% endif %}#}
|
||||
{# </label>#}
|
||||
{# {% endfor %}#}
|
||||
{# {% else %}#}
|
||||
{# <p class="text-muted">Aucun rôle défini pour cette application.</p>#}
|
||||
{# {% endif %}#}
|
||||
{# </div>#}
|
||||
{# </div>#}
|
||||
{# {% endif %}#}
|
||||
{# </div>#}
|
||||
{# </div>#}
|
||||
{# </div>#}
|
||||
{# {% endfor %}#}
|
||||
{# </div>#}
|
||||
{# </div>#}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -12,18 +12,13 @@
|
|||
</div>
|
||||
|
||||
<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}) }}"
|
||||
class="btn btn-primary">Modifier</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card-body ">
|
||||
<div class="card-body ms-4">
|
||||
<p><b>Email: </b>{{ user.email }}</p>
|
||||
<p><b>Dernière connection: </b>{{ user.lastConnection|date('d/m/Y') }}
|
||||
à {{ user.lastConnection|date('H:m:s') }} </p>
|
||||
|
|
|
|||
Loading…
Reference in New Issue