diff --git a/.gitignore b/.gitignore
index dba7697..8d4195a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
###> symfony/framework-bundle ###
/.env.local
+/.env.test
/.env.local.php
/.env.*.local
/config/secrets/prod/prod.decrypt.private.php
@@ -15,11 +16,6 @@
.phpunit.result.cache
###< phpunit/phpunit ###
-###> symfony/phpunit-bridge ###
-.phpunit.result.cache
-/phpunit.xml
-###< symfony/phpunit-bridge ###
-
###> symfony/asset-mapper ###
/public/assets/
/assets/vendor/
diff --git a/.idea/Easy_solution.iml b/.idea/Easy_solution.iml
index c9acc92..5c18800 100644
--- a/.idea/Easy_solution.iml
+++ b/.idea/Easy_solution.iml
@@ -18,6 +18,7 @@
+
diff --git a/.idea/php.xml b/.idea/php.xml
index d547306..26549ad 100644
--- a/.idea/php.xml
+++ b/.idea/php.xml
@@ -179,6 +179,8 @@
+
+
diff --git a/HELPER.MD b/HELPER.MD
index 0daee14..51e463c 100644
--- a/HELPER.MD
+++ b/HELPER.MD
@@ -35,4 +35,7 @@
- Chaque élément est une carte afin de donner un style uniforme :
``` html
-```
\ No newline at end of file
+```
+
+
+php bin/console messenger:consume async -vv
\ No newline at end of file
diff --git a/assets/controllers/organization_controller.js b/assets/controllers/organization_controller.js
index 446de27..6271eb8 100644
--- a/assets/controllers/organization_controller.js
+++ b/assets/controllers/organization_controller.js
@@ -4,20 +4,36 @@ import {TabulatorFull as Tabulator} from 'tabulator-tables';
import {eyeIconLink, TABULATOR_FR_LANG} from "../js/global.js";
export default class extends Controller {
- static values = {aws: String};
+ static values = {aws: String,
+ id: String,
+ activities: Boolean,
+ table: Boolean,
+ sadmin: Boolean,
+ user: Number
+ };
+ static targets = ["activityList", "emptyMessage"]
connect() {
- this.table();
+ if(this.activitiesValue){
+ this.loadActivities();
+ setInterval(() => {
+ this.loadActivities();
+ }, 60000); // Refresh every 60 seconds
+ }
+ if (this.tableValue && this.sadminValue) {
+ this.table();
+ }
+
}
table(){
const table = new Tabulator("#tabulator-org", {
// Register locales here
langs: TABULATOR_FR_LANG,
-
+ placeholder: "Aucun résultat trouvé pour cette recherche",
locale: "fr", //'en' for English, 'fr' for French (en is default, no need to include it)
- ajaxURL: "/organization/data",
+ ajaxURL: `/organization/data/${this.userValue}`,
ajaxConfig: "GET",
pagination: true,
paginationMode: "remote",
@@ -82,4 +98,59 @@ export default class extends Controller {
}],
});
}
+
+ async loadActivities() {
+ try {
+ // 1. Fetch the data using the ID from values
+ const response = await fetch(`/actions/organization/${this.idValue}/activities-ajax`);
+
+ if (!response.ok) {
+ throw new Error('Network response was not ok');
+ }
+
+ const activities = await response.json();
+
+ // 2. Render
+ this.renderActivities(activities);
+
+ } catch (error) {
+ console.error('Error fetching activities:', error);
+ this.activityListTarget.innerHTML = `
Erreur lors du chargement.
`;
+ }
+ }
+
+ renderActivities(activities) {
+ // Clear the loading spinner
+ this.activityListTarget.innerHTML = '';
+
+ if (activities.length === 0) {
+ // Show empty message
+ this.activityListTarget.innerHTML = this.emptyMessageTarget.innerHTML;
+ return;
+ }
+
+ // Loop through JSON and build HTML
+ const html = activities.map(activity => {
+ return `
+
+
+
+
+
+
+
${activity.userName}
+
${activity.actionType}
+
+
+
+ `;
+ }).join('');
+
+ this.activityListTarget.innerHTML = html;
+ }
}
\ No newline at end of file
diff --git a/assets/controllers/user_controller.js b/assets/controllers/user_controller.js
index e8fd81f..44847d0 100644
--- a/assets/controllers/user_controller.js
+++ b/assets/controllers/user_controller.js
@@ -58,6 +58,7 @@ export default class extends Controller {
table() {
const columns = [
{
+ placeholder: "Aucun utilisateur trouvé",
title: "",
field: "isConnected",
width: 40, // small column
@@ -365,7 +366,8 @@ export default class extends Controller {
vertAlign: "middle",
headerSort: false,
formatter: (cell) => {
- const url = cell.getValue();
+ const url = cell.getValue() + '?organizationId=' + this.orgIdValue;
+ console.log(url);
if (url) {
return eyeIconLink(url);
}
diff --git a/composer.json b/composer.json
index 2c84f3e..63d7d8a 100644
--- a/composer.json
+++ b/composer.json
@@ -39,6 +39,7 @@
"symfony/process": "7.2.*",
"symfony/property-access": "7.2.*",
"symfony/property-info": "7.2.*",
+ "symfony/rate-limiter": "7.2.*",
"symfony/runtime": "7.2.*",
"runtime/frankenphp-symfony": "^0.2.0",
"symfony/security-bundle": "7.2.*",
@@ -107,7 +108,8 @@
}
},
"require-dev": {
- "phpunit/phpunit": "^9.5",
+ "dama/doctrine-test-bundle": "^8.3",
+ "phpunit/phpunit": "^11.0",
"symfony/browser-kit": "7.2.*",
"symfony/css-selector": "7.2.*",
"symfony/debug-bundle": "7.2.*",
diff --git a/composer.lock b/composer.lock
index 0e19886..ecc326a 100644
--- a/composer.lock
+++ b/composer.lock
@@ -7752,6 +7752,80 @@
],
"time": "2024-09-26T08:57:56+00:00"
},
+ {
+ "name": "symfony/rate-limiter",
+ "version": "v7.2.9",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/rate-limiter.git",
+ "reference": "daae5da398aca84809aa6088371314a9cb88b42e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/rate-limiter/zipball/daae5da398aca84809aa6088371314a9cb88b42e",
+ "reference": "daae5da398aca84809aa6088371314a9cb88b42e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "symfony/options-resolver": "^6.4|^7.0"
+ },
+ "require-dev": {
+ "psr/cache": "^1.0|^2.0|^3.0",
+ "symfony/lock": "^6.4|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\RateLimiter\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Wouter de Jong",
+ "email": "wouter@wouterj.nl"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides a Token Bucket implementation to rate limit input and output in your application",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "limiter",
+ "rate-limiter"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/rate-limiter/tree/v7.2.9"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-07-10T08:29:33+00:00"
+ },
{
"name": "symfony/routing",
"version": "v7.2.9",
@@ -9993,6 +10067,75 @@
}
],
"packages-dev": [
+ {
+ "name": "dama/doctrine-test-bundle",
+ "version": "v8.3.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/dmaicher/doctrine-test-bundle.git",
+ "reference": "9bc47e02a0d67cbfef6773837249f71e65c95bf6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/dmaicher/doctrine-test-bundle/zipball/9bc47e02a0d67cbfef6773837249f71e65c95bf6",
+ "reference": "9bc47e02a0d67cbfef6773837249f71e65c95bf6",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/dbal": "^3.3 || ^4.0",
+ "doctrine/doctrine-bundle": "^2.11.0",
+ "php": ">= 8.1",
+ "psr/cache": "^2.0 || ^3.0",
+ "symfony/cache": "^6.4 || ^7.2 || ^8.0",
+ "symfony/framework-bundle": "^6.4 || ^7.2 || ^8.0"
+ },
+ "conflict": {
+ "phpunit/phpunit": "<10.0"
+ },
+ "require-dev": {
+ "behat/behat": "^3.0",
+ "friendsofphp/php-cs-fixer": "^3.27",
+ "phpstan/phpstan": "^2.0",
+ "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
+ "symfony/process": "^6.4 || ^7.2 || ^8.0",
+ "symfony/yaml": "^6.4 || ^7.2 || ^8.0"
+ },
+ "type": "symfony-bundle",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "8.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "DAMA\\DoctrineTestBundle\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "David Maicher",
+ "email": "mail@dmaicher.de"
+ }
+ ],
+ "description": "Symfony bundle to isolate doctrine database tests and improve test performance",
+ "keywords": [
+ "doctrine",
+ "isolation",
+ "performance",
+ "symfony",
+ "testing",
+ "tests"
+ ],
+ "support": {
+ "issues": "https://github.com/dmaicher/doctrine-test-bundle/issues",
+ "source": "https://github.com/dmaicher/doctrine-test-bundle/tree/v8.3.1"
+ },
+ "time": "2025-08-05T17:55:02+00:00"
+ },
{
"name": "masterminds/html5",
"version": "2.10.0",
@@ -10122,16 +10265,16 @@
},
{
"name": "nikic/php-parser",
- "version": "v5.6.0",
+ "version": "v5.7.0",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
- "reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56"
+ "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/221b0d0fdf1369c71047ad1d18bb5880017bbc56",
- "reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82",
+ "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82",
"shasum": ""
},
"require": {
@@ -10150,7 +10293,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "5.0-dev"
+ "dev-master": "5.x-dev"
}
},
"autoload": {
@@ -10174,9 +10317,9 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
- "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.0"
+ "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0"
},
- "time": "2025-07-27T20:03:57+00:00"
+ "time": "2025-12-06T11:56:16+00:00"
},
{
"name": "phar-io/manifest",
@@ -10298,35 +10441,35 @@
},
{
"name": "phpunit/php-code-coverage",
- "version": "9.2.32",
+ "version": "11.0.11",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
- "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5"
+ "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5",
- "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4",
+ "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-libxml": "*",
"ext-xmlwriter": "*",
- "nikic/php-parser": "^4.19.1 || ^5.1.0",
- "php": ">=7.3",
- "phpunit/php-file-iterator": "^3.0.6",
- "phpunit/php-text-template": "^2.0.4",
- "sebastian/code-unit-reverse-lookup": "^2.0.3",
- "sebastian/complexity": "^2.0.3",
- "sebastian/environment": "^5.1.5",
- "sebastian/lines-of-code": "^1.0.4",
- "sebastian/version": "^3.0.2",
+ "nikic/php-parser": "^5.4.0",
+ "php": ">=8.2",
+ "phpunit/php-file-iterator": "^5.1.0",
+ "phpunit/php-text-template": "^4.0.1",
+ "sebastian/code-unit-reverse-lookup": "^4.0.1",
+ "sebastian/complexity": "^4.0.1",
+ "sebastian/environment": "^7.2.0",
+ "sebastian/lines-of-code": "^3.0.1",
+ "sebastian/version": "^5.0.2",
"theseer/tokenizer": "^1.2.3"
},
"require-dev": {
- "phpunit/phpunit": "^9.6"
+ "phpunit/phpunit": "^11.5.2"
},
"suggest": {
"ext-pcov": "PHP extension that provides line coverage",
@@ -10335,7 +10478,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "9.2.x-dev"
+ "dev-main": "11.0.x-dev"
}
},
"autoload": {
@@ -10364,40 +10507,52 @@
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
- "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32"
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.11"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage",
+ "type": "tidelift"
}
],
- "time": "2024-08-22T04:23:01+00:00"
+ "time": "2025-08-27T14:37:49+00:00"
},
{
"name": "phpunit/php-file-iterator",
- "version": "3.0.6",
+ "version": "5.1.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-file-iterator.git",
- "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf"
+ "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf",
- "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/118cfaaa8bc5aef3287bf315b6060b1174754af6",
+ "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6",
"shasum": ""
},
"require": {
- "php": ">=7.3"
+ "php": ">=8.2"
},
"require-dev": {
- "phpunit/phpunit": "^9.3"
+ "phpunit/phpunit": "^11.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.0-dev"
+ "dev-main": "5.0-dev"
}
},
"autoload": {
@@ -10424,7 +10579,8 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
- "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6"
+ "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.0"
},
"funding": [
{
@@ -10432,28 +10588,28 @@
"type": "github"
}
],
- "time": "2021-12-02T12:48:52+00:00"
+ "time": "2024-08-27T05:02:59+00:00"
},
{
"name": "phpunit/php-invoker",
- "version": "3.1.1",
+ "version": "5.0.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-invoker.git",
- "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67"
+ "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
- "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2",
+ "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2",
"shasum": ""
},
"require": {
- "php": ">=7.3"
+ "php": ">=8.2"
},
"require-dev": {
"ext-pcntl": "*",
- "phpunit/phpunit": "^9.3"
+ "phpunit/phpunit": "^11.0"
},
"suggest": {
"ext-pcntl": "*"
@@ -10461,7 +10617,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.1-dev"
+ "dev-main": "5.0-dev"
}
},
"autoload": {
@@ -10487,7 +10643,8 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/php-invoker/issues",
- "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1"
+ "security": "https://github.com/sebastianbergmann/php-invoker/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1"
},
"funding": [
{
@@ -10495,32 +10652,32 @@
"type": "github"
}
],
- "time": "2020-09-28T05:58:55+00:00"
+ "time": "2024-07-03T05:07:44+00:00"
},
{
"name": "phpunit/php-text-template",
- "version": "2.0.4",
+ "version": "4.0.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-text-template.git",
- "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28"
+ "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
- "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964",
+ "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964",
"shasum": ""
},
"require": {
- "php": ">=7.3"
+ "php": ">=8.2"
},
"require-dev": {
- "phpunit/phpunit": "^9.3"
+ "phpunit/phpunit": "^11.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0-dev"
+ "dev-main": "4.0-dev"
}
},
"autoload": {
@@ -10546,7 +10703,8 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/php-text-template/issues",
- "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4"
+ "security": "https://github.com/sebastianbergmann/php-text-template/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1"
},
"funding": [
{
@@ -10554,32 +10712,32 @@
"type": "github"
}
],
- "time": "2020-10-26T05:33:50+00:00"
+ "time": "2024-07-03T05:08:43+00:00"
},
{
"name": "phpunit/php-timer",
- "version": "5.0.3",
+ "version": "7.0.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-timer.git",
- "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2"
+ "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
- "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3",
+ "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3",
"shasum": ""
},
"require": {
- "php": ">=7.3"
+ "php": ">=8.2"
},
"require-dev": {
- "phpunit/phpunit": "^9.3"
+ "phpunit/phpunit": "^11.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "5.0-dev"
+ "dev-main": "7.0-dev"
}
},
"autoload": {
@@ -10605,7 +10763,8 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/php-timer/issues",
- "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3"
+ "security": "https://github.com/sebastianbergmann/php-timer/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1"
},
"funding": [
{
@@ -10613,54 +10772,52 @@
"type": "github"
}
],
- "time": "2020-10-26T13:16:10+00:00"
+ "time": "2024-07-03T05:09:35+00:00"
},
{
"name": "phpunit/phpunit",
- "version": "9.6.23",
+ "version": "11.5.46",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "43d2cb18d0675c38bd44982a5d1d88f6d53d8d95"
+ "reference": "75dfe79a2aa30085b7132bb84377c24062193f33"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/43d2cb18d0675c38bd44982a5d1d88f6d53d8d95",
- "reference": "43d2cb18d0675c38bd44982a5d1d88f6d53d8d95",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/75dfe79a2aa30085b7132bb84377c24062193f33",
+ "reference": "75dfe79a2aa30085b7132bb84377c24062193f33",
"shasum": ""
},
"require": {
- "doctrine/instantiator": "^1.5.0 || ^2",
"ext-dom": "*",
"ext-json": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
"ext-xml": "*",
"ext-xmlwriter": "*",
- "myclabs/deep-copy": "^1.13.1",
+ "myclabs/deep-copy": "^1.13.4",
"phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1",
- "php": ">=7.3",
- "phpunit/php-code-coverage": "^9.2.32",
- "phpunit/php-file-iterator": "^3.0.6",
- "phpunit/php-invoker": "^3.1.1",
- "phpunit/php-text-template": "^2.0.4",
- "phpunit/php-timer": "^5.0.3",
- "sebastian/cli-parser": "^1.0.2",
- "sebastian/code-unit": "^1.0.8",
- "sebastian/comparator": "^4.0.8",
- "sebastian/diff": "^4.0.6",
- "sebastian/environment": "^5.1.5",
- "sebastian/exporter": "^4.0.6",
- "sebastian/global-state": "^5.0.7",
- "sebastian/object-enumerator": "^4.0.4",
- "sebastian/resource-operations": "^3.0.4",
- "sebastian/type": "^3.2.1",
- "sebastian/version": "^3.0.2"
+ "php": ">=8.2",
+ "phpunit/php-code-coverage": "^11.0.11",
+ "phpunit/php-file-iterator": "^5.1.0",
+ "phpunit/php-invoker": "^5.0.1",
+ "phpunit/php-text-template": "^4.0.1",
+ "phpunit/php-timer": "^7.0.1",
+ "sebastian/cli-parser": "^3.0.2",
+ "sebastian/code-unit": "^3.0.3",
+ "sebastian/comparator": "^6.3.2",
+ "sebastian/diff": "^6.0.2",
+ "sebastian/environment": "^7.2.1",
+ "sebastian/exporter": "^6.3.2",
+ "sebastian/global-state": "^7.0.2",
+ "sebastian/object-enumerator": "^6.0.1",
+ "sebastian/type": "^5.1.3",
+ "sebastian/version": "^5.0.2",
+ "staabm/side-effects-detector": "^1.0.5"
},
"suggest": {
- "ext-soap": "To be able to generate mocks based on WSDL files",
- "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
+ "ext-soap": "To be able to generate mocks based on WSDL files"
},
"bin": [
"phpunit"
@@ -10668,7 +10825,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "9.6-dev"
+ "dev-main": "11.5-dev"
}
},
"autoload": {
@@ -10700,7 +10857,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
- "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.23"
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.46"
},
"funding": [
{
@@ -10724,32 +10881,32 @@
"type": "tidelift"
}
],
- "time": "2025-05-02T06:40:34+00:00"
+ "time": "2025-12-06T08:01:15+00:00"
},
{
"name": "sebastian/cli-parser",
- "version": "1.0.2",
+ "version": "3.0.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/cli-parser.git",
- "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b"
+ "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b",
- "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b",
+ "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180",
+ "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180",
"shasum": ""
},
"require": {
- "php": ">=7.3"
+ "php": ">=8.2"
},
"require-dev": {
- "phpunit/phpunit": "^9.3"
+ "phpunit/phpunit": "^11.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.0-dev"
+ "dev-main": "3.0-dev"
}
},
"autoload": {
@@ -10772,7 +10929,8 @@
"homepage": "https://github.com/sebastianbergmann/cli-parser",
"support": {
"issues": "https://github.com/sebastianbergmann/cli-parser/issues",
- "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2"
+ "security": "https://github.com/sebastianbergmann/cli-parser/security/policy",
+ "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2"
},
"funding": [
{
@@ -10780,32 +10938,32 @@
"type": "github"
}
],
- "time": "2024-03-02T06:27:43+00:00"
+ "time": "2024-07-03T04:41:36+00:00"
},
{
"name": "sebastian/code-unit",
- "version": "1.0.8",
+ "version": "3.0.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/code-unit.git",
- "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120"
+ "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120",
- "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64",
+ "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64",
"shasum": ""
},
"require": {
- "php": ">=7.3"
+ "php": ">=8.2"
},
"require-dev": {
- "phpunit/phpunit": "^9.3"
+ "phpunit/phpunit": "^11.5"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.0-dev"
+ "dev-main": "3.0-dev"
}
},
"autoload": {
@@ -10828,7 +10986,8 @@
"homepage": "https://github.com/sebastianbergmann/code-unit",
"support": {
"issues": "https://github.com/sebastianbergmann/code-unit/issues",
- "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8"
+ "security": "https://github.com/sebastianbergmann/code-unit/security/policy",
+ "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3"
},
"funding": [
{
@@ -10836,32 +10995,32 @@
"type": "github"
}
],
- "time": "2020-10-26T13:08:54+00:00"
+ "time": "2025-03-19T07:56:08+00:00"
},
{
"name": "sebastian/code-unit-reverse-lookup",
- "version": "2.0.3",
+ "version": "4.0.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
- "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5"
+ "reference": "183a9b2632194febd219bb9246eee421dad8d45e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
- "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e",
+ "reference": "183a9b2632194febd219bb9246eee421dad8d45e",
"shasum": ""
},
"require": {
- "php": ">=7.3"
+ "php": ">=8.2"
},
"require-dev": {
- "phpunit/phpunit": "^9.3"
+ "phpunit/phpunit": "^11.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0-dev"
+ "dev-main": "4.0-dev"
}
},
"autoload": {
@@ -10883,7 +11042,8 @@
"homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
"support": {
"issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
- "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3"
+ "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy",
+ "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1"
},
"funding": [
{
@@ -10891,34 +11051,39 @@
"type": "github"
}
],
- "time": "2020-09-28T05:30:19+00:00"
+ "time": "2024-07-03T04:45:54+00:00"
},
{
"name": "sebastian/comparator",
- "version": "4.0.8",
+ "version": "6.3.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
- "reference": "fa0f136dd2334583309d32b62544682ee972b51a"
+ "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a",
- "reference": "fa0f136dd2334583309d32b62544682ee972b51a",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/85c77556683e6eee4323e4c5468641ca0237e2e8",
+ "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8",
"shasum": ""
},
"require": {
- "php": ">=7.3",
- "sebastian/diff": "^4.0",
- "sebastian/exporter": "^4.0"
+ "ext-dom": "*",
+ "ext-mbstring": "*",
+ "php": ">=8.2",
+ "sebastian/diff": "^6.0",
+ "sebastian/exporter": "^6.0"
},
"require-dev": {
- "phpunit/phpunit": "^9.3"
+ "phpunit/phpunit": "^11.4"
+ },
+ "suggest": {
+ "ext-bcmath": "For comparing BcMath\\Number objects"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "4.0-dev"
+ "dev-main": "6.3-dev"
}
},
"autoload": {
@@ -10957,41 +11122,54 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/comparator/issues",
- "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8"
+ "security": "https://github.com/sebastianbergmann/comparator/security/policy",
+ "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.2"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator",
+ "type": "tidelift"
}
],
- "time": "2022-09-14T12:41:17+00:00"
+ "time": "2025-08-10T08:07:46+00:00"
},
{
"name": "sebastian/complexity",
- "version": "2.0.3",
+ "version": "4.0.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/complexity.git",
- "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a"
+ "reference": "ee41d384ab1906c68852636b6de493846e13e5a0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a",
- "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a",
+ "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0",
+ "reference": "ee41d384ab1906c68852636b6de493846e13e5a0",
"shasum": ""
},
"require": {
- "nikic/php-parser": "^4.18 || ^5.0",
- "php": ">=7.3"
+ "nikic/php-parser": "^5.0",
+ "php": ">=8.2"
},
"require-dev": {
- "phpunit/phpunit": "^9.3"
+ "phpunit/phpunit": "^11.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0-dev"
+ "dev-main": "4.0-dev"
}
},
"autoload": {
@@ -11014,7 +11192,8 @@
"homepage": "https://github.com/sebastianbergmann/complexity",
"support": {
"issues": "https://github.com/sebastianbergmann/complexity/issues",
- "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3"
+ "security": "https://github.com/sebastianbergmann/complexity/security/policy",
+ "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1"
},
"funding": [
{
@@ -11022,33 +11201,33 @@
"type": "github"
}
],
- "time": "2023-12-22T06:19:30+00:00"
+ "time": "2024-07-03T04:49:50+00:00"
},
{
"name": "sebastian/diff",
- "version": "4.0.6",
+ "version": "6.0.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/diff.git",
- "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc"
+ "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc",
- "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544",
+ "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544",
"shasum": ""
},
"require": {
- "php": ">=7.3"
+ "php": ">=8.2"
},
"require-dev": {
- "phpunit/phpunit": "^9.3",
+ "phpunit/phpunit": "^11.0",
"symfony/process": "^4.2 || ^5"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "4.0-dev"
+ "dev-main": "6.0-dev"
}
},
"autoload": {
@@ -11080,7 +11259,8 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/diff/issues",
- "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6"
+ "security": "https://github.com/sebastianbergmann/diff/security/policy",
+ "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2"
},
"funding": [
{
@@ -11088,27 +11268,27 @@
"type": "github"
}
],
- "time": "2024-03-02T06:30:58+00:00"
+ "time": "2024-07-03T04:53:05+00:00"
},
{
"name": "sebastian/environment",
- "version": "5.1.5",
+ "version": "7.2.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/environment.git",
- "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed"
+ "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed",
- "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4",
+ "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4",
"shasum": ""
},
"require": {
- "php": ">=7.3"
+ "php": ">=8.2"
},
"require-dev": {
- "phpunit/phpunit": "^9.3"
+ "phpunit/phpunit": "^11.3"
},
"suggest": {
"ext-posix": "*"
@@ -11116,7 +11296,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "5.1-dev"
+ "dev-main": "7.2-dev"
}
},
"autoload": {
@@ -11135,7 +11315,7 @@
}
],
"description": "Provides functionality to handle HHVM/PHP environments",
- "homepage": "http://www.github.com/sebastianbergmann/environment",
+ "homepage": "https://github.com/sebastianbergmann/environment",
"keywords": [
"Xdebug",
"environment",
@@ -11143,42 +11323,55 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/environment/issues",
- "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5"
+ "security": "https://github.com/sebastianbergmann/environment/security/policy",
+ "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/environment",
+ "type": "tidelift"
}
],
- "time": "2023-02-03T06:03:51+00:00"
+ "time": "2025-05-21T11:55:47+00:00"
},
{
"name": "sebastian/exporter",
- "version": "4.0.6",
+ "version": "6.3.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git",
- "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72"
+ "reference": "70a298763b40b213ec087c51c739efcaa90bcd74"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72",
- "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74",
+ "reference": "70a298763b40b213ec087c51c739efcaa90bcd74",
"shasum": ""
},
"require": {
- "php": ">=7.3",
- "sebastian/recursion-context": "^4.0"
+ "ext-mbstring": "*",
+ "php": ">=8.2",
+ "sebastian/recursion-context": "^6.0"
},
"require-dev": {
- "ext-mbstring": "*",
- "phpunit/phpunit": "^9.3"
+ "phpunit/phpunit": "^11.3"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "4.0-dev"
+ "dev-main": "6.3-dev"
}
},
"autoload": {
@@ -11220,46 +11413,56 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/exporter/issues",
- "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6"
+ "security": "https://github.com/sebastianbergmann/exporter/security/policy",
+ "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.2"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter",
+ "type": "tidelift"
}
],
- "time": "2024-03-02T06:33:00+00:00"
+ "time": "2025-09-24T06:12:51+00:00"
},
{
"name": "sebastian/global-state",
- "version": "5.0.7",
+ "version": "7.0.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/global-state.git",
- "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9"
+ "reference": "3be331570a721f9a4b5917f4209773de17f747d7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9",
- "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9",
+ "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7",
+ "reference": "3be331570a721f9a4b5917f4209773de17f747d7",
"shasum": ""
},
"require": {
- "php": ">=7.3",
- "sebastian/object-reflector": "^2.0",
- "sebastian/recursion-context": "^4.0"
+ "php": ">=8.2",
+ "sebastian/object-reflector": "^4.0",
+ "sebastian/recursion-context": "^6.0"
},
"require-dev": {
"ext-dom": "*",
- "phpunit/phpunit": "^9.3"
- },
- "suggest": {
- "ext-uopz": "*"
+ "phpunit/phpunit": "^11.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "5.0-dev"
+ "dev-main": "7.0-dev"
}
},
"autoload": {
@@ -11278,13 +11481,14 @@
}
],
"description": "Snapshotting of global state",
- "homepage": "http://www.github.com/sebastianbergmann/global-state",
+ "homepage": "https://www.github.com/sebastianbergmann/global-state",
"keywords": [
"global state"
],
"support": {
"issues": "https://github.com/sebastianbergmann/global-state/issues",
- "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7"
+ "security": "https://github.com/sebastianbergmann/global-state/security/policy",
+ "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2"
},
"funding": [
{
@@ -11292,33 +11496,33 @@
"type": "github"
}
],
- "time": "2024-03-02T06:35:11+00:00"
+ "time": "2024-07-03T04:57:36+00:00"
},
{
"name": "sebastian/lines-of-code",
- "version": "1.0.4",
+ "version": "3.0.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/lines-of-code.git",
- "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5"
+ "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5",
- "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5",
+ "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a",
+ "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a",
"shasum": ""
},
"require": {
- "nikic/php-parser": "^4.18 || ^5.0",
- "php": ">=7.3"
+ "nikic/php-parser": "^5.0",
+ "php": ">=8.2"
},
"require-dev": {
- "phpunit/phpunit": "^9.3"
+ "phpunit/phpunit": "^11.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.0-dev"
+ "dev-main": "3.0-dev"
}
},
"autoload": {
@@ -11341,7 +11545,8 @@
"homepage": "https://github.com/sebastianbergmann/lines-of-code",
"support": {
"issues": "https://github.com/sebastianbergmann/lines-of-code/issues",
- "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4"
+ "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy",
+ "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1"
},
"funding": [
{
@@ -11349,34 +11554,34 @@
"type": "github"
}
],
- "time": "2023-12-22T06:20:34+00:00"
+ "time": "2024-07-03T04:58:38+00:00"
},
{
"name": "sebastian/object-enumerator",
- "version": "4.0.4",
+ "version": "6.0.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/object-enumerator.git",
- "reference": "5c9eeac41b290a3712d88851518825ad78f45c71"
+ "reference": "f5b498e631a74204185071eb41f33f38d64608aa"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71",
- "reference": "5c9eeac41b290a3712d88851518825ad78f45c71",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa",
+ "reference": "f5b498e631a74204185071eb41f33f38d64608aa",
"shasum": ""
},
"require": {
- "php": ">=7.3",
- "sebastian/object-reflector": "^2.0",
- "sebastian/recursion-context": "^4.0"
+ "php": ">=8.2",
+ "sebastian/object-reflector": "^4.0",
+ "sebastian/recursion-context": "^6.0"
},
"require-dev": {
- "phpunit/phpunit": "^9.3"
+ "phpunit/phpunit": "^11.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "4.0-dev"
+ "dev-main": "6.0-dev"
}
},
"autoload": {
@@ -11398,7 +11603,8 @@
"homepage": "https://github.com/sebastianbergmann/object-enumerator/",
"support": {
"issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
- "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4"
+ "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy",
+ "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1"
},
"funding": [
{
@@ -11406,32 +11612,32 @@
"type": "github"
}
],
- "time": "2020-10-26T13:12:34+00:00"
+ "time": "2024-07-03T05:00:13+00:00"
},
{
"name": "sebastian/object-reflector",
- "version": "2.0.4",
+ "version": "4.0.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/object-reflector.git",
- "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7"
+ "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
- "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9",
+ "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9",
"shasum": ""
},
"require": {
- "php": ">=7.3"
+ "php": ">=8.2"
},
"require-dev": {
- "phpunit/phpunit": "^9.3"
+ "phpunit/phpunit": "^11.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0-dev"
+ "dev-main": "4.0-dev"
}
},
"autoload": {
@@ -11453,7 +11659,8 @@
"homepage": "https://github.com/sebastianbergmann/object-reflector/",
"support": {
"issues": "https://github.com/sebastianbergmann/object-reflector/issues",
- "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4"
+ "security": "https://github.com/sebastianbergmann/object-reflector/security/policy",
+ "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1"
},
"funding": [
{
@@ -11461,32 +11668,32 @@
"type": "github"
}
],
- "time": "2020-10-26T13:14:26+00:00"
+ "time": "2024-07-03T05:01:32+00:00"
},
{
"name": "sebastian/recursion-context",
- "version": "4.0.5",
+ "version": "6.0.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/recursion-context.git",
- "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1"
+ "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1",
- "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc",
+ "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc",
"shasum": ""
},
"require": {
- "php": ">=7.3"
+ "php": ">=8.2"
},
"require-dev": {
- "phpunit/phpunit": "^9.3"
+ "phpunit/phpunit": "^11.3"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "4.0-dev"
+ "dev-main": "6.0-dev"
}
},
"autoload": {
@@ -11516,94 +11723,53 @@
"homepage": "https://github.com/sebastianbergmann/recursion-context",
"support": {
"issues": "https://github.com/sebastianbergmann/recursion-context/issues",
- "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5"
+ "security": "https://github.com/sebastianbergmann/recursion-context/security/policy",
+ "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.3"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
- }
- ],
- "time": "2023-02-03T06:07:39+00:00"
- },
- {
- "name": "sebastian/resource-operations",
- "version": "3.0.4",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/resource-operations.git",
- "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e",
- "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e",
- "shasum": ""
- },
- "require": {
- "php": ">=7.3"
- },
- "require-dev": {
- "phpunit/phpunit": "^9.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-main": "3.0-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
+ },
{
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de"
- }
- ],
- "description": "Provides a list of PHP built-in functions that operate on resources",
- "homepage": "https://www.github.com/sebastianbergmann/resource-operations",
- "support": {
- "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4"
- },
- "funding": [
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
{
- "url": "https://github.com/sebastianbergmann",
- "type": "github"
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context",
+ "type": "tidelift"
}
],
- "time": "2024-03-14T16:00:52+00:00"
+ "time": "2025-08-13T04:42:22+00:00"
},
{
"name": "sebastian/type",
- "version": "3.2.1",
+ "version": "5.1.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/type.git",
- "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7"
+ "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7",
- "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7",
+ "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449",
+ "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449",
"shasum": ""
},
"require": {
- "php": ">=7.3"
+ "php": ">=8.2"
},
"require-dev": {
- "phpunit/phpunit": "^9.5"
+ "phpunit/phpunit": "^11.3"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.2-dev"
+ "dev-main": "5.1-dev"
}
},
"autoload": {
@@ -11626,37 +11792,50 @@
"homepage": "https://github.com/sebastianbergmann/type",
"support": {
"issues": "https://github.com/sebastianbergmann/type/issues",
- "source": "https://github.com/sebastianbergmann/type/tree/3.2.1"
+ "security": "https://github.com/sebastianbergmann/type/security/policy",
+ "source": "https://github.com/sebastianbergmann/type/tree/5.1.3"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/type",
+ "type": "tidelift"
}
],
- "time": "2023-02-03T06:13:03+00:00"
+ "time": "2025-08-09T06:55:48+00:00"
},
{
"name": "sebastian/version",
- "version": "3.0.2",
+ "version": "5.0.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/version.git",
- "reference": "c6c1022351a901512170118436c764e473f6de8c"
+ "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c",
- "reference": "c6c1022351a901512170118436c764e473f6de8c",
+ "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874",
+ "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874",
"shasum": ""
},
"require": {
- "php": ">=7.3"
+ "php": ">=8.2"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.0-dev"
+ "dev-main": "5.0-dev"
}
},
"autoload": {
@@ -11679,7 +11858,8 @@
"homepage": "https://github.com/sebastianbergmann/version",
"support": {
"issues": "https://github.com/sebastianbergmann/version/issues",
- "source": "https://github.com/sebastianbergmann/version/tree/3.0.2"
+ "security": "https://github.com/sebastianbergmann/version/security/policy",
+ "source": "https://github.com/sebastianbergmann/version/tree/5.0.2"
},
"funding": [
{
@@ -11687,7 +11867,59 @@
"type": "github"
}
],
- "time": "2020-09-28T06:39:44+00:00"
+ "time": "2024-10-09T05:16:32+00:00"
+ },
+ {
+ "name": "staabm/side-effects-detector",
+ "version": "1.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/staabm/side-effects-detector.git",
+ "reference": "d8334211a140ce329c13726d4a715adbddd0a163"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163",
+ "reference": "d8334211a140ce329c13726d4a715adbddd0a163",
+ "shasum": ""
+ },
+ "require": {
+ "ext-tokenizer": "*",
+ "php": "^7.4 || ^8.0"
+ },
+ "require-dev": {
+ "phpstan/extension-installer": "^1.4.3",
+ "phpstan/phpstan": "^1.12.6",
+ "phpunit/phpunit": "^9.6.21",
+ "symfony/var-dumper": "^5.4.43",
+ "tomasvotruba/type-coverage": "1.0.0",
+ "tomasvotruba/unused-public": "1.0.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "lib/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "A static analysis tool to detect side effects in PHP code",
+ "keywords": [
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/staabm/side-effects-detector/issues",
+ "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/staabm",
+ "type": "github"
+ }
+ ],
+ "time": "2024-10-20T05:08:20+00:00"
},
{
"name": "symfony/browser-kit",
@@ -12233,16 +12465,16 @@
},
{
"name": "theseer/tokenizer",
- "version": "1.2.3",
+ "version": "1.3.1",
"source": {
"type": "git",
"url": "https://github.com/theseer/tokenizer.git",
- "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2"
+ "reference": "b7489ce515e168639d17feec34b8847c326b0b3c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
- "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
+ "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c",
+ "reference": "b7489ce515e168639d17feec34b8847c326b0b3c",
"shasum": ""
},
"require": {
@@ -12271,7 +12503,7 @@
"description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
"support": {
"issues": "https://github.com/theseer/tokenizer/issues",
- "source": "https://github.com/theseer/tokenizer/tree/1.2.3"
+ "source": "https://github.com/theseer/tokenizer/tree/1.3.1"
},
"funding": [
{
@@ -12279,7 +12511,7 @@
"type": "github"
}
],
- "time": "2024-03-03T12:36:25+00:00"
+ "time": "2025-11-17T20:03:58+00:00"
}
],
"aliases": [],
@@ -12295,4 +12527,4 @@
},
"platform-dev": {},
"plugin-api-version": "2.6.0"
-}
+}
\ No newline at end of file
diff --git a/config/bundles.php b/config/bundles.php
index d17bbf8..c967433 100644
--- a/config/bundles.php
+++ b/config/bundles.php
@@ -19,4 +19,5 @@ return [
Symfony\Bundle\MercureBundle\MercureBundle::class => ['all' => true],
Knp\Bundle\TimeBundle\KnpTimeBundle::class => ['all' => true],
Aws\Symfony\AwsBundle::class => ['all' => true],
+ DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
];
diff --git a/config/packages/dama_doctrine_test_bundle.yaml b/config/packages/dama_doctrine_test_bundle.yaml
new file mode 100644
index 0000000..3482cba
--- /dev/null
+++ b/config/packages/dama_doctrine_test_bundle.yaml
@@ -0,0 +1,5 @@
+when@test:
+ dama_doctrine_test:
+ enable_static_connection: true
+ enable_static_meta_data_cache: true
+ enable_static_query_cache: true
diff --git a/config/packages/monolog.yaml b/config/packages/monolog.yaml
index c533f37..79ac6b4 100644
--- a/config/packages/monolog.yaml
+++ b/config/packages/monolog.yaml
@@ -9,28 +9,91 @@ monolog:
- security
- php
- error
+ - aws_management
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
when@dev:
- monolog:
- handlers:
- main:
- type: stream
- path: "%kernel.logs_dir%/%kernel.environment%.log"
- level: debug
- channels: ["!event"]
- # uncomment to get logging in your browser
- # you may have to allow bigger header sizes in your Web server configuration
- #firephp:
- # type: firephp
- # level: info
- #chromephp:
- # type: chromephp
- # level: info
- console:
- type: console
- process_psr_3_messages: false
- channels: ["!event", "!doctrine", "!console"]
+ monolog:
+ handlers:
+ critical_errors:
+ type: fingers_crossed
+ action_level: critical
+ handler: error_nested
+ buffer_size: 50
+
+ error_nested:
+ type: rotating_file
+ path: "%kernel.logs_dir%/error.log"
+ level: debug
+ max_files: 30
+
+ error:
+ type: rotating_file
+ path: "%kernel.logs_dir%/error.log"
+ level: error # logs error, critical, alert, emergency
+ max_files: 30
+ channels: [ error ]
+ php_errors:
+ type: rotating_file
+ path: "%kernel.logs_dir%/php_error.log"
+ level: warning # warnings, errors, fatals…
+ max_files: 30
+ channels: [ php ]
+ # User Management
+ user_management:
+ type: rotating_file
+ path: "%kernel.logs_dir%/user_management.log"
+ level: debug
+ channels: [ user_management ]
+ max_files: 30
+
+ # Authentication
+ authentication:
+ type: rotating_file
+ path: "%kernel.logs_dir%/authentication.log"
+ level: debug
+ channels: [ authentication ]
+ max_files: 30
+
+ # Organization Management
+ organization_management:
+ type: rotating_file
+ path: "%kernel.logs_dir%/organization_management.log"
+ level: debug
+ channels: [ organization_management ]
+ max_files: 30
+
+ # Access Control
+ access_control:
+ type: rotating_file
+ path: "%kernel.logs_dir%/access_control.log"
+ level: debug
+ channels: [ access_control ]
+ max_files: 30
+
+ # Email Notifications
+ email_notifications:
+ type: rotating_file
+ path: "%kernel.logs_dir%/email_notifications.log"
+ level: debug
+ channels: [ email_notifications ]
+ max_files: 30
+
+ # Admin Actions
+ admin_actions:
+ type: rotating_file
+ path: "%kernel.logs_dir%/admin_actions.log"
+ level: debug
+ channels: [ admin_actions ]
+ max_files: 30
+
+ # Security
+ security:
+ type: rotating_file
+ path: "%kernel.logs_dir%/security.log"
+ level: debug
+ channels: [ security ]
+ max_files: 30
when@test:
monolog:
@@ -57,7 +120,7 @@ when@prod:
error_nested:
type: rotating_file
- path: "%kernel.logs_dir%/error.log"
+ path: "%kernel.logs_dir%/critical.log"
level: debug
max_files: 30
@@ -76,12 +139,18 @@ when@prod:
channels: [ php ]
# User Management
user_management:
- type: stream
+ type: rotating_file
path: "%kernel.logs_dir%/user_management.log"
level: info
channels: [user_management]
max_files: 30
-
+ #AWS
+ aws_management:
+ type: rotating_file
+ path: "%kernel.logs_dir%/aws_management.log"
+ level: info
+ channels: [aws_management]
+ max_files: 30
# Authentication
authentication:
type: rotating_file
diff --git a/config/packages/security.yaml b/config/packages/security.yaml
index 18e4e11..58afd30 100644
--- a/config/packages/security.yaml
+++ b/config/packages/security.yaml
@@ -42,6 +42,9 @@ security:
user_checker: App\Security\UserChecker
lazy: true
provider: app_user_provider
+ login_throttling:
+ max_attempts: 3
+ interval: '1 minute'
form_login:
login_path: app_login
check_path: app_login
diff --git a/config/packages/translation.yaml b/config/packages/translation.yaml
index b3f8f9c..0af682d 100644
--- a/config/packages/translation.yaml
+++ b/config/packages/translation.yaml
@@ -1,5 +1,5 @@
framework:
- default_locale: en
+ default_locale: fr
translator:
default_path: '%kernel.project_dir%/translations'
fallbacks:
diff --git a/migrations/Version20260105152103.php b/migrations/Version20260105152103.php
new file mode 100644
index 0000000..067cc16
--- /dev/null
+++ b/migrations/Version20260105152103.php
@@ -0,0 +1,32 @@
+addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_EMAIL ON organizations (email)');
+ }
+
+ 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('DROP INDEX UNIQ_IDENTIFIER_EMAIL');
+ }
+}
diff --git a/migrations/Version20260106080636.php b/migrations/Version20260106080636.php
new file mode 100644
index 0000000..3f3a5e2
--- /dev/null
+++ b/migrations/Version20260106080636.php
@@ -0,0 +1,32 @@
+addSql('CREATE UNIQUE INDEX UNIQ_ORGANIZATION_EMAIL ON organizations (email)');
+ }
+
+ 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('DROP INDEX UNIQ_ORGANIZATION_EMAIL');
+ }
+}
diff --git a/migrations/Version20260106084653.php b/migrations/Version20260106084653.php
new file mode 100644
index 0000000..ab52dc6
--- /dev/null
+++ b/migrations/Version20260106084653.php
@@ -0,0 +1,32 @@
+addSql('ALTER TABLE organizations ALTER logo_url DROP NOT 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 ALTER logo_url SET NOT NULL');
+ }
+}
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 6c4bfed..3949e0e 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,20 +1,15 @@
-
-
+ colors="true"
+ cacheDirectory=".phpunit.cache">
+
-
-
@@ -23,16 +18,9 @@
-
+
src
-
-
-
-
-
-
-
-
-
+
+
\ No newline at end of file
diff --git a/src/Controller/ActionController.php b/src/Controller/ActionController.php
new file mode 100644
index 0000000..b01b613
--- /dev/null
+++ b/src/Controller/ActionController.php
@@ -0,0 +1,34 @@
+entityManager->getRepository(Actions::class)->findBy(
+ ['Organization' => $organization],
+ ['date' => 'DESC'],
+ 15
+ );
+ $formattedActivities = $this->actionService->formatActivities($actions);
+
+ return new JsonResponse($formattedActivities);
+ }
+}
diff --git a/src/Controller/ApplicationController.php b/src/Controller/ApplicationController.php
index 13d3cbf..0b4e776 100644
--- a/src/Controller/ApplicationController.php
+++ b/src/Controller/ApplicationController.php
@@ -5,18 +5,20 @@ namespace App\Controller;
use App\Entity\Apps;
use App\Entity\Organizations;
use App\Service\ActionService;
+use App\Service\LoggerService;
use App\Service\UserService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
+use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
#[Route(path: '/application', name: 'application_')]
class ApplicationController extends AbstractController
{
- public function __construct(private readonly EntityManagerInterface $entityManager, private readonly UserService $userService, private readonly ActionService $actionService)
+ public function __construct(private readonly EntityManagerInterface $entityManager, private readonly UserService $userService, private readonly ActionService $actionService, private readonly LoggerService $loggerService)
{
}
@@ -37,7 +39,11 @@ class ApplicationController extends AbstractController
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
$application = $this->entityManager->getRepository(Apps::class)->find($id);
if (!$application) {
- $this->addFlash('error', "L'application n'existe pas ou n'est pas reconnu.");
+ $this->loggerService->logEntityNotFound('Application', [
+ 'applicationId' => $id,
+ 'message' => "Application not found for editing."
+ ], $actingUser->getId());
+ $this->addFlash('danger', "L'application n'existe pas ou n'est pas reconnu.");
return $this->redirectToRoute('application_index');
}
$applicationData = [
@@ -50,12 +56,26 @@ class ApplicationController extends AbstractController
if ($request->isMethod('POST')) {
- $data = $request->request->all();
- $application->setName($data['name']);
- $application->setDescription($data['description']);
- $application->setDescriptionSmall($data['descriptionSmall']);
- $this->entityManager->persist($application);
- $this->actionService->createAction("Modification de l'application ", $actingUser, null, $application->getId());
+ try{
+ $data = $request->request->all();
+ $application->setName($data['name']);
+ $application->setDescription($data['description']);
+ $application->setDescriptionSmall($data['descriptionSmall']);
+ $this->entityManager->persist($application);
+ $this->actionService->createAction("Modification de l'application ", $actingUser, null, $application->getId());
+ $this->loggerService->logApplicationInformation('Application Edited', [
+ 'applicationId' => $application->getId(),
+ 'applicationName' => $application->getName(),
+ 'message' => "Application edited successfully."
+ ], $actingUser->getId());
+ }catch (\Exception $e){
+ $this->loggerService->logError('Application Edit Failed', [
+ 'applicationId' => $application->getId(),
+ 'applicationName' => $application->getName(),
+ 'error' => $e->getMessage(),
+ 'message' => "Failed to edit application."
+ ], $actingUser);
+ }
return $this->redirectToRoute('application_index');
}
@@ -66,36 +86,82 @@ class ApplicationController extends AbstractController
}
#[Route(path: '/authorize/{id}', name: 'authorize', methods: ['POST'])]
- public function authorize(int $id, Request $request)
+ public function authorize(int $id, Request $request): Response
{
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
- $application = $this->entityManager->getRepository(Apps::class)->find($id);
- if (!$application) {
- throw $this->createNotFoundException("L'application n'existe pas.");
+ 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 (\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);
}
- $orgId = $request->get('organizationId');
- $organization = $this->entityManager->getRepository(Organizations::Class)->find($orgId);
- $application->addOrganization($organization);
- $this->actionService->createAction("Authorization d'accès", $actingUser, $organization, $application->getName());
- return new Response('', Response::HTTP_OK);
}
- #[Route(path: '/remove/{id}', name: 'remove', methods: ['POST'])]
- public function remove(int $id, Request $request)
+ #[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);
diff --git a/src/Controller/NotificationController.php b/src/Controller/NotificationController.php
index 3c7ac0c..9195577 100644
--- a/src/Controller/NotificationController.php
+++ b/src/Controller/NotificationController.php
@@ -28,7 +28,7 @@ class NotificationController extends AbstractController
#[Route(path: '/', name: 'index', methods: ['GET'])]
public function index(): JsonResponse
{
- $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
+ $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
$notifications = $this->notificationRepository->findRecentByUser($user, 50);
diff --git a/src/Controller/OAuth2Controller.php b/src/Controller/OAuth2Controller.php
index c614716..2427772 100644
--- a/src/Controller/OAuth2Controller.php
+++ b/src/Controller/OAuth2Controller.php
@@ -3,6 +3,8 @@
namespace App\Controller;
use App\Service\AccessTokenService;
+use App\Service\LoggerService;
+use App\Service\UserService;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -18,17 +20,20 @@ class OAuth2Controller extends AbstractController
{
-
+ public function __construct(private readonly LoggerService $loggerService, private readonly UserService $userService)
+ {
+ }
#[Route('/oauth2/userinfo', name: 'userinfo', methods: ['GET'])]
public function userinfo(Request $request): JsonResponse
{
$user = $this->getUser();
-// dd($user);
if (!$user) {
+ $this->loggerService->logAccessDenied($user->getId());
return new JsonResponse(['error' => 'Unauthorized'], 401);
}
+ $this->loggerService->logUserAction($user->getId(), $user->getId(), 'Accessed userinfo endpoint');
return new JsonResponse([
'id' => $user->getId(),
'name' => $user->getName(),
@@ -66,7 +71,7 @@ class OAuth2Controller extends AbstractController
if (!$userIdentifier) {
return new JsonResponse(["ERROR" => "User identifier is required"], Response::HTTP_BAD_REQUEST);
}
- $accessTokenService->revokeTokens($userIdentifier);
+ $accessTokenService->revokeUserTokens($userIdentifier);
$logger->info("Revoke tokens successfully");
return new JsonResponse(["SUCCESS" => "Tokens revoked successfully"], Response::HTTP_OK);
diff --git a/src/Controller/OrganizationController.php b/src/Controller/OrganizationController.php
index 0d74bd2..9691197 100644
--- a/src/Controller/OrganizationController.php
+++ b/src/Controller/OrganizationController.php
@@ -12,14 +12,20 @@ use App\Form\OrganizationForm;
use App\Repository\OrganizationsRepository;
use App\Service\ActionService;
use App\Service\AwsService;
+use App\Service\LoggerService;
use App\Service\OrganizationsService;
use App\Service\UserOrganizationService;
use App\Service\UserService;
+use Doctrine\DBAL\Exception\NonUniqueFieldNameException;
+use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\ORM\EntityManagerInterface;
+use Doctrine\ORM\NonUniqueResultException;
use Exception;
+use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Attribute\Route;
use App\Entity\Organizations;
use Symfony\Component\HttpFoundation\Response;
@@ -37,7 +43,7 @@ class OrganizationController extends AbstractController
private readonly ActionService $actionService,
private readonly UserOrganizationService $userOrganizationService,
private readonly OrganizationsRepository $organizationsRepository,
- private readonly AwsService $awsService)
+ private readonly AwsService $awsService, private readonly LoggerService $loggerService, private readonly LoggerInterface $logger)
{
}
@@ -45,45 +51,30 @@ class OrganizationController extends AbstractController
public function index(): Response
{
$this->denyAccessUnlessGranted('ROLE_ADMIN');
- $user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
-
- if ($this->isGranted("ROLE_SUPER_ADMIN")) {
- $organizations = $this->organizationsRepository->findBy(['isDeleted' => false]);
-
-
- } else {
- //get all the UO of the user
- $uos = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['users' => $user]);
- $organizations = [];
- $roleAdmin = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN']);
- foreach ($uos as $uo) {
- $uoaAdmin = $this->entityManager->getRepository(UserOrganizatonApp::class)->findOneBy(['userOrganization' => $uo, 'role' => $roleAdmin]);
- if ($uoaAdmin) {
- $organizations[] = $uo->getOrganization();
+ $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()]);
}
}
- if (count($organizations) === 1 && $organizations[0]->isActive() === true) {
- return $this->redirectToRoute('organization_show', ['id' => $organizations[0]->getId()]);
- }
-
+ return $this->render('organization/index.html.twig', [
+ 'hasOrganizations' => $orgCount > 0
+ ]);
}
- // Map the entities for tabulator
- $organizationsData = array_map(function ($org) {
- return [
- 'id' => $org->getId(),
- 'name' => $org->getName(),
- 'email' => $org->getEmail(),
- 'logoUrl' => $org->getLogoUrl() ? $org->getLogoUrl() : null,
- 'active' => $org->isActive(),
- 'showUrl' => $this->generateUrl('organization_show', ['id' => $org->getId()]),
- ];
- }, $organizations);
- return $this->render('organization/index.html.twig', [
- 'organizationsData' => $organizationsData,
- ]);
+ $this->loggerService->logAccessDenied($actingUser->getId());
+ throw new AccessDeniedHttpException('Access denied');
}
- #[Route(path: '/new', name: 'new', methods: ['GET', 'POST'])]
+ #[Route(path: '/create', name: 'create', methods: ['GET', 'POST'])]
public function new(Request $request): Response
{
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
@@ -100,10 +91,14 @@ class OrganizationController extends AbstractController
try {
$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->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', 'Error creating organization: ' . $e->getMessage());
+ $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()]);
}
}
return $this->render('organization/new.html.twig', [
@@ -124,21 +119,34 @@ class OrganizationController extends AbstractController
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
$organization = $this->organizationsRepository->find($id);
if (!$organization) {
- $this->addFlash('error', self::NOT_FOUND);
+ $this->loggerService->logEntityNotFound('Organization', [
+ 'org_id' => $id,
+ 'message' => 'Organization not found for edit'], $actingUser->getId()
+ );
+ $this->addFlash('error', '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
- $user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
- $uo = $this->entityManager->getRepository(UsersOrganizations::class)->findOneBy(['users' => $user, 'organization' => $organization]);
+ $uo = $this->entityManager->getRepository(UsersOrganizations::class)->findOneBy(['users' => $actingUser, 'organization' => $organization]);
if (!$uo) {
- $this->addFlash('error', self::ACCESS_DENIED);
+ $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->addFlash('error', self::ACCESS_DENIED);
+ $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');
}
}
@@ -152,10 +160,16 @@ class OrganizationController extends AbstractController
try {
$this->entityManager->persist($organization);
$this->entityManager->flush();
+ $this->loggerService->logOrganizationInformation($organization->getId(), $actingUser->getId(), "Organization Edited");
+ if ($this->isGranted("ROLE_SUPER_ADMIN")) {
+ $this->loggerService->logSuperAdmin($actingUser->getId(), $actingUser->getId(), "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', 'Error editing organization: ' . $e->getMessage());
+ }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()]);
}
}
return $this->render('organization/edit.html.twig', [
@@ -171,43 +185,31 @@ class OrganizationController extends AbstractController
$organization = $this->organizationsRepository->find($id);
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
if (!$organization) {
- $this->addFlash('error', self::NOT_FOUND);
+ $this->loggerService->logEntityNotFound('Organization', [
+ 'org_id' => $id,
+ 'message' => 'Organization not found for view'
+ ], $actingUser->getId());
+ $this->addFlash('error', 'Erreur, l\'organization est introuvable.');
return $this->redirectToRoute('organization_index');
}
//check if the user is admin of the organization
- if (!$this->isGranted("ROLE_SUPER_ADMIN") && !$this->userService->isAdminOfOrganization($organization)) {
- $this->createNotFoundException(self::NOT_FOUND);
+ if (!$this->userService->isAdminOfOrganization($organization) && !$this->isGranted("ROLE_SUPER_ADMIN")) {
+ $this->loggerService->logAccessDenied($actingUser->getId());
+ $this->addFlash('error', 'Erreur, accès refusé.');
+ throw new AccessDeniedHttpException('Access denied');
}
- $newUO = $this->entityManager->getRepository(UsersOrganizations::class)->findNewestUO($organization);
- $newUsers = [];
- foreach ($newUO as $uo) {
- $newUsers[] = $uo->getUsers();
- }
- $adminUO = $this->entityManager->getRepository(UsersOrganizations::class)->findAdminsInOrganization($organization);
- $adminUsers = [];
- foreach ($adminUO as $uo) {
- $adminUsers[] = $uo->getUsers();
- }
- $uos = $this->entityManager
- ->getRepository(UsersOrganizations::class)
- ->findBy(['organization' => $organization]);
-
- $users = $this->userService->formatOrgUsers($uos);
$allApps = $this->entityManager->getRepository(Apps::class)->findAll(); // appsAll
$orgApps = $organization->getApps()->toArray(); // apps
$apps = $this->organizationsService->appsAccess($allApps, $orgApps);
- $actions = $this->entityManager->getRepository(Actions::class)->findBy(['Organization' => $organization], limit: 15);
+ $actions = $this->entityManager->getRepository(Actions::class)->findBy(['Organization' => $organization], orderBy: ['date' => 'DESC'], limit: 15);
$activities = $this->actionService->formatActivities($actions);
$this->actionService->createAction("View Organization", $actingUser, $organization, $organization->getName());
return $this->render('organization/show.html.twig', [
'organization' => $organization,
- 'newUsers' => $newUsers,
- 'adminUsers' => $adminUsers,
- 'users' => $users,
'applications' => $apps,
'activities' => $activities,
]);
@@ -220,15 +222,32 @@ class OrganizationController extends AbstractController
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
$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.');
throw $this->createNotFoundException(self::NOT_FOUND);
}
- $organization->setIsActive(false);
- $organization->setIsDeleted(true);
- // Deactivate all associated UsersOrganizations
- $this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, null, $organization);
+ try {
+ $organization->setIsActive(false);
+ $organization->setIsDeleted(true);
+ // Deactivate all associated UsersOrganizations
+ $this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, null, $organization);
+
+ $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');
+ if ($this->isGranted("ROLE_SUPER_ADMIN")) {
+ $this->loggerService->logSuperAdmin($actingUser->getId(), $actingUser->getId(),'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->entityManager->persist($organization);
- $this->actionService->createAction("Delete Organization", $actingUser, $organization, $organization->getName());
return $this->redirectToRoute('organization_index');
}
@@ -239,12 +258,20 @@ class OrganizationController extends AbstractController
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
$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.');
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->addFlash('success', 'Organisation désactivé avec succès.');
return $this->redirectToRoute('organization_index');
}
@@ -255,16 +282,24 @@ class OrganizationController extends AbstractController
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
$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.');
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->actionService->createAction("Activate Organization", $actingUser, $organization, $organization->getName());
+ $this->addFlash('success', 'Organisation activée avec succès.');
return $this->redirectToRoute('organization_index');
}
// API endpoint to fetch organization data for Tabulator
- #[Route(path: '/data', name: 'data', methods: ['GET'])]
+ #[Route(path: '/data/{id}', name: 'data', methods: ['GET'])]
public function data(Request $request): JsonResponse
{
$this->denyAccessUnlessGranted('ROLE_ADMIN');
@@ -276,8 +311,6 @@ class OrganizationController extends AbstractController
$filters = $request->query->all('filter');
- $user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
-
$qb = $this->organizationsRepository->createQueryBuilder('o')
->where('o.isDeleted = :del')->setParameter('del', false);
@@ -289,6 +322,17 @@ class OrganizationController extends AbstractController
$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]);
+ foreach ($uo as $item) {
+ if($this->userService->isAdminOfOrganization($item->getOrganization())) {
+ $qb->andWhere('o.id = :orgId')
+ ->setParameter('orgId', $item->getOrganization()->getId());
+ }
+ }
+ }
+
// Count total
$countQb = clone $qb;
@@ -311,7 +355,6 @@ class OrganizationController extends AbstractController
];
}, $rows);
- // Tabulator expects: data, last_page (total pages), or total row count depending on config
$lastPage = (int)ceil($total / $size);
return $this->json([
diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php
index f51f040..9b2a988 100644
--- a/src/Controller/SecurityController.php
+++ b/src/Controller/SecurityController.php
@@ -5,6 +5,7 @@ namespace App\Controller;
use App\Repository\UserRepository;
use App\Repository\UsersOrganizationsRepository;
use App\Service\AccessTokenService;
+use App\Service\LoggerService;
use App\Service\OrganizationsService;
use App\Service\UserService;
use Doctrine\ORM\EntityManagerInterface;
@@ -30,7 +31,7 @@ class SecurityController extends AbstractController
private readonly UsersOrganizationsRepository $uoRepository,
private readonly LoggerInterface $logger,
private readonly EntityManagerInterface $entityManager,
- private readonly OrganizationsService $organizationsService)
+ private readonly OrganizationsService $organizationsService, private readonly LoggerService $loggerService, private readonly Security $security)
{
$this->cguUserService = $cguUserService;
}
@@ -50,14 +51,16 @@ class SecurityController extends AbstractController
#[Route(path: '/sso_logout', name: 'sso_logout')]
public function ssoLogout(RequestStack $stack, LoggerInterface $logger, AccessTokenService $accessTokenService, Security $security): Response
{
- try{
- if( $stack->getSession()->invalidate()){
- $accessTokenService->revokeTokens($security->getUser()->getUserIdentifier());
+ try {
+ $user = $this->userService->getUserByIdentifier($this->security->getUser()->getUserIdentifier());
+ $id = $user->getId();
+ if ($stack->getSession()->invalidate()) {
+ $accessTokenService->revokeUserTokens($security->getUser()->getUserIdentifier());
$security->logout(false);
- $logger->info("Logout successfully");
- return $this->redirect('/');
+ $this->loggerService->logUserConnection('User logged out', ['user_id' => $id]);
+ return $this->redirect('/');
}
- }catch (\Exception $e){
+ } catch (\Exception $e) {
$logger->log(LogLevel::ERROR, 'Error invalidating session: ' . $e->getMessage());
}
return $this->redirectToRoute('app_index');
@@ -69,6 +72,7 @@ class SecurityController extends AbstractController
if ($request->isMethod('POST')) {
if (!$request->request->has('decline')) {
$this->cguUserService->acceptLatestCgu($this->getUser());
+ $this->loggerService->logCGUAcceptance($this->getUser()->getId());
}
return $this->redirectToRoute('oauth2_authorize', $request->query->all());
@@ -83,12 +87,24 @@ class SecurityController extends AbstractController
$error = $request->get('error');
$user = $this->userRepository->find($id);
if (!$user) {
+ $this->loggerService->logEntityNotFound('User', ['user_id' => $id,
+ 'error' => $error ?? null,
+ 'message' => 'user not found for password setup'], $id);
throw $this->createNotFoundException(self::NOT_FOUND);
}
$token = $request->get('token');
- if (empty($token) || !$this->userService->isPasswordTokenValid($user, $token)) {
+ if (empty($token)) {
$error = 'Le lien de définition du mot de passe est invalide ou a expiré. Veuillez en demander un nouveau.';
- $this->logger->warning($user->getUserIdentifier(). " tried to use an invalid or expired password setup token.");
+ $this->loggerService->logTokenError('Token empty while trying to setup password', ['token' => $token,
+ 'token_empty' => true,
+ 'user_id' => $id,
+ 'message' => 'empty token provided for password setup']);
+ }
+
+ if (!$this->userService->isPasswordTokenValid($user, $token)) {
+ $error = 'Le lien de définition du mot de passe est invalide ou a expiré. Veuillez en demander un nouveau.';
+ $this->loggerService->logTokenError('invalid or expired token for password setup', ['user_id' => $id,
+ 'token' => $token,]);
}
return $this->render('security/password_setup.html.twig', [
'id' => $id,
@@ -98,36 +114,43 @@ class SecurityController extends AbstractController
}
#[Route('/password_reset/{id}', name: 'password_reset', methods: ['POST'])]
- public function password_reset(int $id): Response
+ public function password_reset(int $id, Request $request): Response
{
$user = $this->userRepository->find($id);
if (!$user) {
+ $this->loggerService->logEntityNotFound('User', ['user_id' => $id,
+ 'message' => 'user not found for password reset'], $id);
throw $this->createNotFoundException(self::NOT_FOUND);
}
$newPassword = $_POST['_password'] ?? null;
$confirmPassword = $_POST['_passwordConfirm'] ?? null;
if ($newPassword !== $confirmPassword) {
$error = 'Les mots de passe ne correspondent pas. Veuillez réessayer.';
- $this->logger->warning($user->getUserIdentifier(). " provided non-matching passwords during password reset.");
+ $this->loggerService->logUserAction($id, $id, 'Password confirmation does not match during password reset.');
return $this->redirectToRoute('password_setup', [
'id' => $id,
'token' => $_POST['token'] ?? '',
- 'error'=> $error]);
+ 'error' => $error]);
}
if (!$this->userService->isPasswordStrong($newPassword)) {
$error = 'Le mot de passe ne respecte pas les critères de sécurité. Veuillez en choisir un autre.';
- $this->logger->warning($user->getUserIdentifier(). " provided a weak password during password reset.");
- return $this->redirectToRoute('password_setup', ['id' => $id, 'token' => $_POST['token'] ?? '', 'error'=> $error]);
+ $this->loggerService->logUserAction($id, $id, ' provided a weak password during password reset.');
+ return $this->redirectToRoute('password_setup', ['id' => $id, 'token' => $_POST['token'] ?? '', 'error' => $error]);
}
$this->userService->updateUserPassword($user, $newPassword);
- $orgId = $this->userService->getOrgFromToken( $_POST['token']);
+ $this->loggerService->logUserAction($id, $id, 'Password reset user successfully.');
+ $orgId = $this->userService->getOrgFromToken($_POST['token']);
$uo = $this->uoRepository->findOneBy(['users' => $user, 'organization' => $orgId]);
- if($uo){
+ if ($uo) {
$uo->setStatut("ACCEPTED");
$uo->setIsActive(true);
$this->entityManager->persist($uo);
$this->entityManager->flush();
- $data = ['user' => $user, 'organization' => $uo->getOrganization()];
+ $this->loggerService->logOrganizationInformation($orgId, $user->getId(), 'User accepted organization invitation during password reset.');
+ $this->loggerService->logUserAction($id, $id, "User accepted organization invitation successfully with uo link id : {$uo->getId()}");
+ $data = ['user' => $user,
+ 'organization' => $uo->getOrganization(),
+ ];
$this->organizationsService->notifyOrganizationAdmins($data, "USER_ACCEPTED");
}
diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php
index 31a8488..1f4ef43 100644
--- a/src/Controller/UserController.php
+++ b/src/Controller/UserController.php
@@ -13,26 +13,25 @@ use App\Repository\OrganizationsRepository;
use App\Repository\RolesRepository;
use App\Repository\UserRepository;
use App\Repository\UsersOrganizationsRepository;
+use App\Service\AccessTokenService;
use App\Service\ActionService;
use App\Service\AwsService;
use App\Service\EmailService;
+use App\Service\LoggerService;
use App\Service\OrganizationsService;
use App\Service\UserOrganizationAppService;
use App\Service\UserOrganizationService;
use App\Service\UserService;
use Doctrine\ORM\EntityManagerInterface;
-use mysql_xdevapi\Exception;
use Psr\Log\LoggerInterface;
-use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
-use Symfony\Component\Mailer\Mailer;
-use Symfony\Component\Mailer\MailerInterface;
-use Symfony\Component\Mime\Email;
use Symfony\Component\Routing\Attribute\Route;
+use Symfony\Component\Security\Core\Exception\AccessDeniedException;
#[Route(path: '/user', name: 'user_')]
class UserController extends AbstractController
@@ -48,19 +47,17 @@ class UserController extends AbstractController
private readonly UserOrganizationService $userOrganizationService,
private readonly UserRepository $userRepository,
private readonly UsersOrganizationsRepository $uoRepository,
- private readonly OrganizationsRepository $organizationRepository,
- private readonly LoggerInterface $userManagementLogger,
- private readonly LoggerInterface $organizationManagementLogger,
- private readonly LoggerInterface $accessControlLogger,
- private readonly LoggerInterface $EmailNotificationLogger,
- private readonly LoggerInterface $adminActionsLogger,
- private readonly LoggerInterface $errorLogger,
- private readonly LoggerInterface $SecurityLogger,
- private readonly EmailService $emailService,
- private readonly AwsService $awsService,
- private readonly OrganizationsService $organizationsService,
- private readonly AppsRepository $appsRepository,
- private readonly RolesRepository $rolesRepository,
+ private readonly OrganizationsRepository $organizationRepository,
+ private readonly LoggerInterface $userManagementLogger,
+ private readonly LoggerInterface $organizationManagementLogger,
+ private readonly LoggerInterface $errorLogger,
+ private readonly LoggerInterface $securityLogger,
+ private readonly LoggerService $loggerService,
+ private readonly EmailService $emailService,
+ private readonly AwsService $awsService,
+ private readonly OrganizationsService $organizationsService,
+ private readonly AppsRepository $appsRepository,
+ private readonly RolesRepository $rolesRepository, private readonly AccessTokenService $accessTokenService,
)
{
}
@@ -76,13 +73,20 @@ class UserController extends AbstractController
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
// Vérification des droits d'accès supplémentaires
- if (!$this->userService->hasAccessTo($actingUser)) {
- throw $this->createAccessDeniedException(self::ACCESS_DENIED);
- }
+
// 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.");
+ 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.");
+ throw new AccessDeniedHttpException (self::ACCESS_DENIED);
+ }
try {
// Paramètre optionnel de contexte organisationnel
$orgId = $request->query->get('organizationId');
@@ -105,6 +109,11 @@ class UserController extends AbstractController
]);
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);
}
@@ -118,8 +127,15 @@ class UserController extends AbstractController
'users' => $user,
'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 une organisation.");
+ throw $this->createNotFoundException(self::NOT_FOUND);
+ }
}
-
// Charger les liens UserOrganizationApp (UOA) actifs pour les UO trouvées
// Load user-organization-app roles (can be empty)
$uoa = $this->entityManager
@@ -128,7 +144,6 @@ class UserController extends AbstractController
'userOrganization' => $uoList,
'isActive' => true,
]);
-
// Group UOA by app and ensure every app has a group
$data['uoas'] = $this->userOrganizationAppService
->groupUserOrganizationAppsByApplication(
@@ -150,12 +165,13 @@ class UserController extends AbstractController
// -------------------------------------------------------------------
// Calcul du flag de modification : utilisateur admin ET exactement 1 UO
- $canEdit = $this->userService->canEditRolesCheck($actingUser, $user, $organization, $this->isGranted('ROLE_ADMIN'));
+ $canEdit = $this->userService->canEditRolesCheck($actingUser, $user,$this->isGranted('ROLE_ADMIN'), $singleUo, $organization);
} catch (\Exception $e) {
- // En cas d'erreur, désactiver l'édition et logger l'exception
- $canEdit = false;
$this->errorLogger->error($e->getMessage());
+ $this->addFlash('error', '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,
@@ -171,63 +187,60 @@ class UserController extends AbstractController
public function edit(int $id, Request $request): Response
{
$this->denyAccessUnlessGranted('ROLE_USER');
- try{
+ $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
+ $user = $this->userRepository->find($id);
+ if (!$user) {
+ $this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getId());
+ $this->addFlash('error', "L'utilisateur demandé n'existe pas.");
+ throw $this->createNotFoundException(self::NOT_FOUND);
+ }
+ try {
+ if ($this->userService->hasAccessTo($user)) {
- $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
- if ($this->userService->hasAccessTo($actingUser)) {
- $user = $this->userRepository->find($id);
- if (!$user) {
- $this->userManagementLogger->notice('User not found for edit', [
- 'target_user_id' => $user->getId(),
- 'acting_user_id' => $actingUser->getId(),
- 'ip' => $request->getClientIp(),
- 'timestamp' => (new \DateTimeImmutable('now'))->format(DATE_ATOM),
- ]);
- throw $this->createNotFoundException(self::NOT_FOUND);
- }
$form = $this->createForm(UserForm::class, $user);
$form->handleRequest($request);
- $this->userManagementLogger->notice('Format test', [
- 'target_user_id' => $user->getId(),
- 'acting_user_id' => $actingUser->getId(),
- 'ip' => $request->getClientIp(),
- 'timestamp' => (new \DateTimeImmutable('now'))->format(DATE_ATOM),
- ]);
-
if ($form->isSubmitted() && $form->isValid()) {
// Handle user edit
$picture = $form->get('pictureUrl')->getData();
- $this->userService->formatNewUserData($user, $picture);
+ $this->userService->formatUserData($user, $picture);
$user->setModifiedAt(new \DateTimeImmutable('now'));
$this->entityManager->persist($user);
$this->entityManager->flush();
//log and action
- $this->userManagementLogger->notice('User information edited', [
- 'target_user_id' => $user->getId(),
- 'acting_user_id' => $actingUser->getId(),
- 'organization_id' => $request->get('organizationId'),
- 'ip' => $request->getClientIp(),
- 'timestamp' => (new \DateTimeImmutable('now'))->format(DATE_ATOM),
- ]);
- if ($request->get('organizationId')) {
- $org = $this->organizationRepository->find($request->get('organizationId'));
+ $this->loggerService->logUserAction($user->getId(), $actingUser->getId(), '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->organizationManagementLogger->info('User edited within organization context', [
- 'target_user_id' => $user->getId(),
- 'organization_id' => $org->getId(),
- 'acting_user' => $actingUser->getUserIdentifier(),
- 'ip' => $request->getClientIp(),
- ]);
- return $this->redirectToRoute('user_show', ['id' => $user->getId(), 'organizationId' => $request->get('organizationId')]);
+ $this->loggerService->logUserAction($user->getId(), $actingUser->getId(), 'User information edited');
+ if ($this->isGranted('ROLE_SUPER_ADMIN')) {
+ $this->loggerService->logSuperAdmin(
+ $user->getId(),
+ $actingUser->getId(),
+ "Super Admin accessed user edit page",
+ );
+ }
+ $this->addFlash('success', 'Information modifié avec success.');
+ return $this->redirectToRoute('user_show', ['id' => $user->getId(), 'organizationId' => $orgId]);
}
- } else {
- $this->actionService->createAction("Edit user information", $actingUser, null, $user->getUserIdentifier());
- return $this->redirectToRoute('user_show', ['id' => $user->getId()]);
+ $this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getId());
+ $this->addFlash('error', "L'organisation n'existe pas.");
+ throw $this->createNotFoundException(self::NOT_FOUND);
}
+ if ($this->isGranted('ROLE_SUPER_ADMIN')) {
+ $this->loggerService->logSuperAdmin(
+ $user->getId(),
+ $actingUser->getId(),
+ "Super Admin accessed user edit page",
+ );
+ }
+ $this->addFlash('success', 'Information modifié avec success.');
+ $this->actionService->createAction("Edit user information", $actingUser, null, $user->getUserIdentifier());
+ return $this->redirectToRoute('user_show', ['id' => $user->getId()]);
}
return $this->render('user/edit.html.twig', [
@@ -236,15 +249,16 @@ class UserController extends AbstractController
'organizationId' => $request->get('organizationId')
]);
}
- }catch (\Exception $e){
+ $this->loggerService->logAccessDenied($actingUser->getId());
+ $this->addFlash('error', "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->errorLogger->critical($e->getMessage());
}
- $this->SecurityLogger->warning('Access denied on user edit', [
- 'target_user_id' => $id,
- 'acting_user' => $actingUser?->getId(),
- 'ip' => $request->getClientIp(),
- ]);
+ // Default deny access. shouldn't reach here normally.
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
+
}
#[Route('/new', name: 'new', methods: ['GET', 'POST'])]
@@ -253,70 +267,128 @@ class UserController extends AbstractController
$this->denyAccessUnlessGranted('ROLE_ADMIN');
try {
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
- if ($this->userService->hasAccessTo($actingUser)) {
- $user = new User();
- $form = $this->createForm(UserForm::class, $user);
- $form->handleRequest($request);
- $orgId = $request->get('organizationId');
- if ($orgId){
- $org = $this->organizationRepository->find($orgId) ?? throw new NotFoundHttpException(sprintf('%s not found', $orgId));
- }
-
- if ($form->isSubmitted() && $form->isValid()) {
- $existingUser = $this->userRepository->findOneBy(['email' => $user->getEmail()]);
- if ($existingUser && $orgId) {
- $this->userService->handleExistingUser($existingUser, $org);
-
- $this->actionService->createAction("Create new user", $existingUser, $org, "Added user to organization" . $existingUser->getUserIdentifier() . " for organization " . $org->getName());
- $this->logger->notice("User added to organization " . $org->getName());
- $this->emailService->sendExistingUserNotificationEmail($existingUser, $org);
- $this->logger->notice("Existing user notification email sent to " . $existingUser->getUserIdentifier());
- $data = ['user' => $existingUser, 'organization' => $org];
- $this->organizationsService->notifyOrganizationAdmins($data, 'USER_INVITED');
- return $this->redirectToRoute('organization_show', ['id' => $orgId]);
- }
-
-
- // Handle file upload
- $picture = $form->get('pictureUrl')->getData();
- $this->userService->formatNewUserData($user, $picture, true);
-
- if ($orgId) {
- $uo = new UsersOrganizations();
- $uo->setUsers($user);
- $uo->setOrganization($org);
- $uo->setStatut("INVITED");
- $uo->setIsActive(false);
- $uo->setModifiedAt(new \DateTimeImmutable('now'));
- $this->entityManager->persist($uo);
- $this->actionService->createAction("Create new user", $user, $org, "Added user to organization" . $user->getUserIdentifier() . " for organization " . $org->getName());
- $this->logger->notice("User added to organization " . $org->getName());
- $this->emailService->sendPasswordSetupEmail($user, $orgId);
- $this->logger->notice("Password setup email sent to " . $user->getUserIdentifier());
- $data = ['user' => $uo->getUsers(), 'organization' => $uo->getOrganization()];
- $this->organizationsService->notifyOrganizationAdmins($data, 'USER_INVITED');
- }
- $this->actionService->createAction("Create new user", $actingUser, null, $user->getUserIdentifier());
- $this->logger->notice("User created " . $user->getUserIdentifier());
- $this->entityManager->persist($user);
- $this->entityManager->flush();
-
- if ($orgId) {
- return $this->redirectToRoute('organization_show', ['organizationId' => $orgId]);
- }
- return $this->redirectToRoute('user_index');
- }
+ if (!$this->userService->hasAccessTo($actingUser)) {
+ $this->loggerService->logAccessDenied($actingUser->getId());
+ throw $this->createAccessDeniedException(self::ACCESS_DENIED);
}
+
+ $user = new User();
+ $form = $this->createForm(UserForm::class, $user);
+ $form->handleRequest($request);
+
+ $orgId = $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.");
+ 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é.");
+ throw $this->createAccessDeniedException(self::ACCESS_DENIED);
+ }
+ }elseif($this->isGranted('ROLE_ADMIN')) {
+ $this->loggerService->logAccessDenied($actingUser->getId());
+ $this->addFlash('error', "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
+ if ($existingUser && $org) {
+ $this->userService->addExistingUserToOrganization(
+ $existingUser,
+ $org,
+ $actingUser,
+ );
+
+ if ($this->isGranted('ROLE_SUPER_ADMIN')) {
+ $this->loggerService->logSuperAdmin(
+ $existingUser->getId(),
+ $actingUser->getId(),
+ "Super Admin linked user to organization",
+ $org->getId(),
+ );
+ }
+ $this->addFlash('success', 'Utilisateur ajouté avec succès à l\'organisation. ');
+ return $this->redirectToRoute('organization_show', ['id' => $orgId]);
+ }
+
+ //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,
+// ]);
+// }
+
+ $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');
+ }
+
return $this->render('user/new.html.twig', [
'user' => $user,
'form' => $form->createView(),
- 'organizationId' => $orgId
+ 'organizationId' => $orgId,
]);
+
} catch (\Exception $e) {
- $this->logger->error($e->getMessage());
+ $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 .');
return $this->redirectToRoute('organization_show', ['id' => $orgId]);
}
+ $this->addFlash('error', 'Une erreur est survenue lors de la création de l\'utilisateur.');
return $this->redirectToRoute('user_index');
}
}
@@ -326,44 +398,98 @@ class UserController extends AbstractController
public function activeStatus(int $id, Request $request): JsonResponse
{
$this->denyAccessUnlessGranted('ROLE_ADMIN');
+
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
- try{
- if ($this->userService->hasAccessTo($actingUser, true)) {
- $user = $this->userRepository->find($id);
- if (!$user) {
- throw $this->createNotFoundException(self::NOT_FOUND);
- }
- $status = $request->get('status');
- if ($status === 'deactivate') {
- $user->setIsActive(false);
- $this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, $user);
- if ($this->userService->isUserConnected($user->getUserIdentifier())) {
- $this->userService->revokeUserTokens($user->getUserIdentifier());
- }
- $user->setModifiedAt(new \DateTimeImmutable('now'));
- $this->entityManager->persist($user);
- $this->entityManager->flush();
- $this->logger->notice("User deactivated " . $user->getUserIdentifier());
- $this->actionService->createAction("Deactivate user", $actingUser, null, $user->getUserIdentifier());
- return new JsonResponse(['status' => 'deactivated'], Response::HTTP_OK);
+ $status = $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());
+
+ throw $this->createNotFoundException(self::NOT_FOUND);
+ }
+
+ // Deactivate
+ if ($status === 'deactivate') {
+ $user->setIsActive(false);
+
+ $this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, $user);
+
+ if ($this->userService->isUserConnected($user->getUserIdentifier())) {
+ $this->accessTokenService->revokeUserTokens($user->getUserIdentifier());
}
- if ($status === 'activate') {
- $user->setIsActive(true);
- $user->setModifiedAt(new \DateTimeImmutable('now'));
- $this->logger->notice("User activated " . $user->getUserIdentifier());
- $this->actionService->createAction("Activate user", $actingUser, null, $user->getUserIdentifier());
- return new JsonResponse(['status' => 'activated'], Response::HTTP_OK);
+ $user->setModifiedAt(new \DateTimeImmutable('now'));
+ $this->entityManager->persist($user);
+ $this->entityManager->flush();
+ $this->loggerService->logUserAction($user->getId(), $actingUser->getId(), 'User deactivated');
+
+ if ($this->isGranted('ROLE_SUPER_ADMIN')) {
+ $this->loggerService->logSuperAdmin(
+ $user->getId(),
+ $actingUser->getId(),
+ 'Super admin deactivated user'
+ );
}
+
+ $this->actionService->createAction('Deactivate user', $actingUser, null, $user->getUserIdentifier());
+
+ return new JsonResponse(['status' => 'deactivated'], Response::HTTP_OK);
}
- }catch (\Exception $e){
- $this->logger->error($e->getMessage());
+
+ // Activate
+ if ($status === 'activate') {
+ $user->setIsActive(true);
+ $user->setModifiedAt(new \DateTimeImmutable('now'));
+ $this->entityManager->persist($user);
+ $this->entityManager->flush();
+ $this->loggerService->logUserAction($user->getId(), $actingUser->getId(), 'User activated');
+
+
+ if ($this->isGranted('ROLE_SUPER_ADMIN')) {
+ $this->loggerService->logSuperAdmin(
+ $user->getId(),
+ $actingUser->getId(),
+ 'Super admin activated user'
+ );
+ }
+
+ $this->actionService->createAction('Activate user', $actingUser, null, $user->getUserIdentifier());
+
+ return new JsonResponse(['status' => 'activated'], Response::HTTP_OK);
+ }
+
+ // Invalid status
+ $this->loggerService->logError('Invalid status provided for activeStatus', [
+ 'requested_status' => $status,
+ 'target_user_id' => $id,
+ ]);
+
+ return new JsonResponse(['error' => 'Status invalide'], Response::HTTP_BAD_REQUEST);
+
+ } catch (\Throwable $e) {
+ // Application-level error logging → error.log (via error channel)
+ $this->errorLogger->critical($e->getMessage());
+
+ // Preserve 403/404 semantics, 500 for everything else
+ if ($e instanceof NotFoundHttpException || $e instanceof AccessDeniedException) {
+ throw $e;
+ }
+
+ return new JsonResponse(['error' => 'Une erreur est survenue'], Response::HTTP_INTERNAL_SERVER_ERROR);
}
- throw $this->createNotFoundException(self::NOT_FOUND);
}
#[Route('/organization/activateStatus/{id}', name: 'activate_organization', methods: ['GET', 'POST'])]
- public function activateStatusOrganization(int $id, Request $request): JsonResponse{
+ public function activateStatusOrganization(int $id, Request $request): JsonResponse
+ {
$this->denyAccessUnlessGranted('ROLE_ADMIN');
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
try {
@@ -371,15 +497,19 @@ class UserController extends AbstractController
$orgId = $request->get('organizationId');
$org = $this->organizationRepository->find($orgId);
if (!$org) {
+ $this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getId());
throw $this->createNotFoundException(self::NOT_FOUND);
}
$user = $this->userRepository->find($id);
if (!$user) {
+ $this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getId());
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());
throw $this->createNotFoundException(self::NOT_FOUND);
}
$status = $request->get('status');
@@ -391,55 +521,115 @@ class UserController extends AbstractController
$data = ['user' => $user,
'organization' => $org];
$this->organizationsService->notifyOrganizationAdmins($data, "USER_DEACTIVATED");
- $this->logger->notice("User Organizaton deactivated " . $user->getUserIdentifier());
+ $this->loggerService->logOrganizationInformation($org->getId(), $actingUser->getId(), "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);
}
- if($status === "activate"){
+ if ($status === "activate") {
$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->actionService->createAction("Activate user in organization", $actingUser, $org, $org->getName() . " for user " . $user->getUserIdentifier());
$data = ['user' => $user,
'organization' => $org];
$this->organizationsService->notifyOrganizationAdmins($data, "USER_ACTIVATED");
return new JsonResponse(['status' => 'activated'], Response::HTTP_OK);
}
+ //invalid status
+ $this->loggerService->logError('Invalid status provided for activateStatusOrganization', [
+ 'requested_status' => $status,
+ 'target_user_id' => $id,
+ 'organization_id' => $orgId,
+ ]);
+ throw $this->createNotFoundException(self::NOT_FOUND);
}
- }catch (\Exception $exception){
- $this->logger->error($exception->getMessage());
+ } catch (\Exception $exception) {
+ $this->loggerService->logCritical($exception->getMessage());
}
throw $this->createNotFoundException(self::NOT_FOUND);
}
-//TODO : MONOLOG + remove picture from bucket
+//TODO :remove picture from bucket
#[Route('/delete/{id}', name: 'delete', methods: ['GET', 'POST'])]
public function delete(int $id, Request $request): Response
{
- $this->denyAccessUnlessGranted("ROLE_SUPER_ADMIN");
- $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
- $user = $this->userRepository->find($id);
- if (!$user) {
- throw $this->createNotFoundException(self::NOT_FOUND);
- }
- $user->setIsActive(false);
- $user->setModifiedAt(new \DateTimeImmutable('now'));
- $this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, $user);
- $user->setIsDeleted(true);
- if ($this->userService->isUserConnected($user)) {
- $this->userService->revokeUserTokens($user->getUserIdentifier());
- }
- $this->entityManager->persist($user);
- $this->entityManager->flush();
- $this->actionService->createAction("Delete user", $actingUser, null, $user->getUserIdentifier());
- $data = ['user' => $user,
- 'organization' => null];
- $this->organizationsService->notifyOrganizationAdmins($data, "USER_DELETED");
+ $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
- return new Response('', Response::HTTP_NO_CONTENT); //204
+ $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
+
+ 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.");
+ throw $this->createNotFoundException(self::NOT_FOUND);
+ }
+
+ // Soft delete the user
+
+ $user->setIsActive(false);
+ $user->setIsDeleted(true);
+ $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');
+
+ // Revoke tokens if connected
+ if ($this->userService->isUserConnected($user->getUserIdentifier())) {
+ $this->accessTokenService->revokeUserTokens($user->getUserIdentifier());
+ }
+
+ $this->entityManager->flush();
+
+ // User management log
+ $this->loggerService->logUserAction($user->getId(), $actingUser->getId(), 'User deleted');
+
+ // Super admin log (standardized style)
+ if ($this->isGranted('ROLE_SUPER_ADMIN')) {
+ $this->loggerService->logSuperAdmin(
+ $user->getId(),
+ $actingUser->getId(),
+ 'Super admin deleted user'
+ );
+ }
+
+ $this->actionService->createAction('Delete user', $actingUser, null, $user->getUserIdentifier());
+
+ // Notify organization admins (user may belong to multiple organizations)
+ try {
+ $data = [
+ 'user' => $user,
+ 'organization' => null,
+ ];
+ $this->organizationsService->notifyOrganizationAdmins($data, 'USER_DELETED');
+
+
+ } catch (\Throwable $e) {
+ $this->loggerService->logCritical($e->getMessage(), [
+ 'target_user_id' => $id,
+ 'acting_user_id' => $actingUser?->getId(),
+ ]);
+ }
+ $this->addFlash('success', 'Utilisateur supprimé avec succès.');
+ return $this->redirectToRoute('user_index');
+
+ } catch (\Exception $e) {
+ // Route-level error logging → error.log
+ $this->loggerService->logCritical('error while deleting user', [
+ 'target_user_id' => $id,
+ 'acting_user_id' => $actingUser?->getId(),
+ 'error' => $e->getMessage(),
+ ]);
+ if ($e instanceof NotFoundHttpException) {
+ throw $e; // keep 404 semantics
+ }
+ $this->addFlash('error', 'Erreur lors de la suppression de l\'utilisateur\.');
+ return $this->redirectToRoute('user_index');
+ }
}
- //TODO : MONOLOG
#[Route(path: '/application/roles/{id}', name: 'application_role', methods: ['GET', 'POST'])]
public function applicationRole(int $id, Request $request): Response
{
@@ -447,19 +637,29 @@ class UserController extends AbstractController
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
if ($this->userService->hasAccessTo($actingUser, true)) {
- $uo = $this->userOrganizationService->getByIdOrFail($id);
+ $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) {
- throw $this->createNotFoundException('Default role not found');
+ $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 {
@@ -476,6 +676,7 @@ class UserController extends AbstractController
}
$user = $uo->getUsers();
+ $this->addFlash('success', 'Rôles mis à jour avec succès.');
return $this->redirectToRoute('user_show', [
'user' => $user,
'id' => $user->getId(),
@@ -542,7 +743,6 @@ class UserController extends AbstractController
'statut' => $user->isActive(),
];
}, $rows);
-
$lastPage = (int)ceil($total / $size);
return $this->json([
@@ -555,6 +755,7 @@ class UserController extends AbstractController
#[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]);
@@ -562,6 +763,9 @@ class UserController extends AbstractController
'users' => $totalUsers
]);
}
+
+ //shouldn't be reached normally
+ $this->loggerService->logAccessDenied($actingUser->getId());
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
}
@@ -715,27 +919,38 @@ class UserController extends AbstractController
$orgId = $request->get('organizationId');
$org = $this->organizationRepository->find($orgId);
if (!$org) {
+ $this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getId());
throw $this->createNotFoundException(self::NOT_FOUND);
}
$user = $this->userRepository->find($userId);
if (!$user) {
+ $this->loggerService->logEntityNotFound('User', ['id' => $user->getId()], $actingUser->getId());
throw $this->createNotFoundException(self::NOT_FOUND);
}
$uo = $this->uoRepository->findOneBy(['users' => $user,
'organization' => $org,
'statut' => "INVITED"]);
if (!$uo) {
+ $this->loggerService->logEntityNotFound('UsersOrganization', [
+ 'user_id' => $user->getId(),
+ 'organization_id' => $orgId], $actingUser->getId());
throw $this->createNotFoundException(self::NOT_FOUND);
}
$uo->setModifiedAt(new \DateTimeImmutable());
try {
$data = ['user' => $uo->getUsers(), 'organization' => $uo->getOrganization()];
- $this->emailService->sendPasswordSetupEmail($user, $orgId);
+ $token = $this->userService->generatePasswordToken($user, $org->getId());
+ $this->emailService->sendPasswordSetupEmail($user, $token);
$this->logger->info("Invitation email resent to user " . $user->getUserIdentifier() . " for organization " . $org->getName());
$this->organizationsService->notifyOrganizationAdmins($data, 'USER_INVITED');
return $this->json(['message' => 'Invitation envoyée avec success.'], Response::HTTP_OK);
} catch (\Exception $e) {
- $this->logger->error("Error resending invitation email to user " . $user->getUserIdentifier() . " for organization " . $org->getName() . ": " . $e->getMessage());
+ $this->loggerService->logCritical('Error while resending invitation', [
+ 'target_user_id' => $user->getId(),
+ 'organization_id' => $orgId,
+ 'acting_user_id' => $actingUser->getId(),
+ 'error' => $e->getMessage(),
+ ]);
return $this->json(['message' => 'Erreur lors de l\'envoie du mail.'], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
@@ -749,29 +964,46 @@ class UserController extends AbstractController
$userId = $request->get('id');
if (!$token || !$userId) {
+ $this->loggerService->logEntityNotFound('Token or UserId missing in accept invitation', [
+ 'token' => $token,
+ 'user_id' => $userId
+ ],
+ null);
throw $this->createNotFoundException('Invalid invitation link.');
}
$user = $this->userRepository->find($userId);
if (!$user) {
+ $this->loggerService->logEntityNotFound('User not found in accept invitation', [
+ 'user_id' => $userId
+ ],null);
throw $this->createNotFoundException(self::NOT_FOUND);
}
if (!$this->userService->isPasswordTokenValid($user, $token)) {
+ $this->loggerService->logError('Token or UserId mismatch in accept invitation', [
+ 'token' => $token,
+ 'user_id' => $userId
+ ]);
throw $this->createNotFoundException('Invalid or expired invitation token.');
}
$orgId = $this->userService->getOrgFromToken($token);
- $uo = $this->uoRepository->findOneBy(['users' => $user, 'organization' => $orgId]);
- if (!$uo || $uo->getStatut() !== 'INVITED') {
- $this->logger->warning("User " . $user->getUserIdentifier() . " tried to accept an invitation but no pending invitation was found for organization ID " . $orgId);
- throw $this->createNotFoundException('No pending invitation found for this user and organization.');
+ if ($orgId) {
+ $uo = $this->uoRepository->findOneBy(['users' => $user, 'organization' => $orgId]);
+ if (!$uo || $uo->getStatut() !== 'INVITED') {
+ $this->loggerService->logEntityNotFound('UsersOrganization not found or not in INVITED status in accept invitation', [
+ 'user_id' => $user->getId(),
+ 'organization_id' => $orgId
+ ], null);
+ throw $this->createNotFoundException('No pending invitation found for this user and organization.');
+ }
+ $uo->setModifiedAt(new \DateTimeImmutable());
+ $uo->setStatut("ACCEPTED");
+ $uo->setIsActive(true);
+ $this->entityManager->persist($uo);
+ $this->entityManager->flush();
+ $this->loggerService->logUserAction($user->getId(), null, "User accepted invitation for organization id : {$orgId}");
+ $this->loggerService->logOrganizationInformation($orgId, $user->getId(), "User accepted invitation with uo id : {$uo->getId()}");
}
- $uo->setModifiedAt(new \DateTimeImmutable());
- $uo->setStatut("ACCEPTED");
- $uo->setIsActive(true);
- $this->entityManager->persist($uo);
- $this->entityManager->flush();
- $this->logger->info("User " . $user->getUserIdentifier() . " accepted invitation for organization ID " . $orgId);
-
- return $this->render('user/show.html.twig', ['user' => $user, 'orgId' => $orgId]);
+ return $this->render('security/login.html.twig');
}
}
diff --git a/src/Entity/Apps.php b/src/Entity/Apps.php
index 1492d3b..99c6f7e 100644
--- a/src/Entity/Apps.php
+++ b/src/Entity/Apps.php
@@ -59,6 +59,7 @@ class Apps
{
$this->userOrganizatonApps = new ArrayCollection();
$this->organization = new ArrayCollection();
+ $this->setIsActive(true);
}
public function getId(): ?int
diff --git a/src/Entity/Organizations.php b/src/Entity/Organizations.php
index bff31ba..883cb74 100644
--- a/src/Entity/Organizations.php
+++ b/src/Entity/Organizations.php
@@ -4,10 +4,13 @@ namespace App\Entity;
use App\Repository\OrganizationsRepository;
use Doctrine\Common\Collections\ArrayCollection;
+use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: OrganizationsRepository::class)]
+#[ORM\UniqueConstraint(name: 'UNIQ_ORGANIZATION_EMAIL', fields: ['email'])]
+#[UniqueEntity(fields: ['email'], message: 'Une organisation avec cet email existe déjà.')]
class Organizations
{
#[ORM\Id]
@@ -24,7 +27,7 @@ class Organizations
#[ORM\Column(length: 255)]
private ?string $address = null;
- #[ORM\Column(length: 255)]
+ #[ORM\Column(length: 255, nullable: true)]
private ?string $logo_url = null;
#[ORM\Column(options: ['default' => 'CURRENT_TIMESTAMP'])]
diff --git a/src/Entity/User.php b/src/Entity/User.php
index ca9d110..8709389 100644
--- a/src/Entity/User.php
+++ b/src/Entity/User.php
@@ -9,7 +9,6 @@ use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
-use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: '`user`')]
diff --git a/src/Event/UserCreatedEvent.php b/src/Event/UserCreatedEvent.php
new file mode 100644
index 0000000..3f706f0
--- /dev/null
+++ b/src/Event/UserCreatedEvent.php
@@ -0,0 +1,25 @@
+newUser;
+ }
+
+ public function getActingUser(): User
+ {
+ return $this->actingUser;
+ }
+}
diff --git a/src/EventSubscriber/UserSubscriber.php b/src/EventSubscriber/UserSubscriber.php
new file mode 100644
index 0000000..3e27e14
--- /dev/null
+++ b/src/EventSubscriber/UserSubscriber.php
@@ -0,0 +1,55 @@
+ 'onUserCreated',
+ ];
+ }
+
+ public function onUserCreated(UserCreatedEvent $event): void
+ {
+ $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.
+ if ($user->getPasswordToken()) {
+ $this->emailService->sendPasswordSetupEmail($user, $user->getPasswordToken());
+ }
+
+ // 3. Log the creation
+ $this->loggerService->logUserCreated($user->getId(), $actingUser->getId());
+
+ // 4. Create the Audit Action
+ $this->actionService->createAction(
+ "Create new user",
+ $actingUser,
+ null,
+ $user->getUserIdentifier()
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/Form/OrganizationForm.php b/src/Form/OrganizationForm.php
index 571b773..da84a2d 100644
--- a/src/Form/OrganizationForm.php
+++ b/src/Form/OrganizationForm.php
@@ -17,8 +17,8 @@ class OrganizationForm extends AbstractType
$builder
->add('email', EmailType::class, ['required' => true, 'label' => 'Email*'])
->add('name', TextType::class, ['required' => true, 'label' => 'Nom de l\'organisation*'])
- ->add('address', TextType::class, ['required' => false, 'label' => 'Adresse'])
- ->add('number', TextType::class, ['required' => false, 'label' => 'Numéro de téléphone'])
+ ->add('address', TextType::class, ['required' => true, 'label' => 'Adresse'])
+ ->add('number', TextType::class, ['required' => true, 'label' => 'Numéro de téléphone'])
->add('logoUrl', FileType::class, [
'required' => false,
'label' => 'Logo',
diff --git a/src/Service/AccessTokenService.php b/src/Service/AccessTokenService.php
index 9fe9540..b36a229 100644
--- a/src/Service/AccessTokenService.php
+++ b/src/Service/AccessTokenService.php
@@ -11,17 +11,38 @@ class AccessTokenService
private EntityManagerInterface $entityManager;
- public function __construct(EntityManagerInterface $entityManager)
+ public function __construct(EntityManagerInterface $entityManager,
+ private readonly LoggerService $loggerService)
{
$this->entityManager = $entityManager;
}
- public function revokeTokens(String $userIdentifier): void {
- $accessTokens = $this->entityManager->getRepository(AccessToken::class)->findBy(['userIdentifier' => $userIdentifier, 'revoked' => false]);
- foreach($accessTokens as $accessToken) {
- $accessToken->revoke();
- $this->entityManager->persist($accessToken);
- $this->entityManager->flush();
+ public function revokeUserTokens(string $userIdentifier): void
+ {
+ $tokens = $this->entityManager->getRepository(AccessToken::class)->findBy([
+ 'userIdentifier' => $userIdentifier,
+ 'revoked' => false
+ ]);
+ foreach ($tokens as $token) {
+ try{
+ $token->revoke();
+ $this->loggerService->logTokenRevocation(
+ 'Access token revoked for user',
+ [
+ 'user_identifier' => $userIdentifier,
+ 'token_id' => $token->getIdentifier(),
+ ]
+ );
+ }catch (\Exception $e){
+ $this->loggerService->logError(
+ 'Error revoking access token: ' . $e->getMessage(),
+ [
+ 'user_identifier' => $userIdentifier,
+ 'token_id' => $token->getIdentifier(),
+ ]
+ );
+ }
+
}
}
diff --git a/src/Service/ActionService.php b/src/Service/ActionService.php
index f0a7a44..7b750f9 100644
--- a/src/Service/ActionService.php
+++ b/src/Service/ActionService.php
@@ -40,11 +40,11 @@ readonly class ActionService
{
return array_map(function (Actions $activity) {
return [
- 'date' => $activity->getDate(),
+ 'date' => $activity->getDate()->format('d/m/Y H:i'),
'actionType' => $activity->getActionType(),
- 'users' => $activity->getUsers(),
- 'organization' => $activity->getOrganization(),
- 'description' => $activity->getDescription(),
+ 'userName' => $activity->getUsers()->getName(),
+// 'organization' => $activity->getOrganization(),
+// 'description' => $activity->getDescription(),
'color' => $this->getActivityColor($activity->getDate())
];
}, $activities);
diff --git a/src/Service/CguUserService.php b/src/Service/CguUserService.php
index 279c33e..6e491b8 100644
--- a/src/Service/CguUserService.php
+++ b/src/Service/CguUserService.php
@@ -2,6 +2,7 @@
namespace App\Service;
+use App\Service\LoggerService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use App\Entity\Cgu;
@@ -9,7 +10,7 @@ use App\Entity\CguUser;
class CguUserService
{
- public function __construct(private EntityManagerInterface $entityManager)
+ public function __construct(private EntityManagerInterface $entityManager, private readonly LoggerService $loggerService)
{
}
@@ -40,11 +41,20 @@ class CguUserService
$cguUser = $this->entityManager->getRepository(CguUser::class)->findOneBy(['users' => $user, 'cgu' => $latestCgu]);
if (!$cguUser) {
- // Create a new CguUser relation if it doesn't exist
- $cguUser = new CguUser();
- $cguUser->setUsers($user);
- $cguUser->setCgu($latestCgu);
- $this->entityManager->persist($cguUser);
+ try{
+ // Create a new CguUser relation if it doesn't exist
+ $cguUser = new CguUser();
+ $cguUser->setUsers($user);
+ $cguUser->setCgu($latestCgu);
+ $this->entityManager->persist($cguUser);
+ }catch (\Exception $e){
+ $this->loggerService->logError('CguUserService', [
+ 'acceptLatestCgu' => 'Failed to create CguUser relation',
+ 'exception' => $e,
+ 'targer_user_id' => $user->getId(),]);
+ throw new \RuntimeException('Failed to create CguUser relation: ' . $e->getMessage());
+ }
+
}
$cguUser->setIsAccepted(true);
diff --git a/src/Service/EmailService.php b/src/Service/EmailService.php
index d8c942e..f4f1b3e 100644
--- a/src/Service/EmailService.php
+++ b/src/Service/EmailService.php
@@ -4,6 +4,7 @@ namespace App\Service;
use App\Entity\Organizations;
use App\Entity\User;
+use App\Service\LoggerService;
use Psr\Log\LoggerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
@@ -14,15 +15,12 @@ class EmailService
{
public function __construct(
private readonly MailerInterface $mailer,
- private readonly UserService $userService,
private readonly LoggerInterface $logger,
- private UrlGeneratorInterface $urlGenerator
+ private UrlGeneratorInterface $urlGenerator, private readonly LoggerService $loggerService
) {}
- public function sendPasswordSetupEmail(User $user, int $orgId): void
+ public function sendPasswordSetupEmail(User $user, string $token): void
{
- $token = $this->userService->generatePasswordToken($user, $orgId);
-
// Generate absolute URL for the password setup route
$link = $this->urlGenerator->generate(
'password_setup',
@@ -46,15 +44,16 @@ class EmailService
]);
try {
+ $orgId = $this->getOrgFromToken($token);
$this->mailer->send($email);
+ $this->loggerService->logEmailSent($user->getId(), $orgId, 'Password setup email sent.');
} catch (\Symfony\Component\Mailer\Exception\TransportExceptionInterface $e) {
$this->logger->error('Failed to send password setup email: ' . $e->getMessage());
}
}
- public function sendExistingUserNotificationEmail(User $existingUser, Organizations $org): void
+ public function sendExistingUserNotificationEmail(User $existingUser, Organizations $org, $token): void
{
- $token = $this->userService->generatePasswordToken($existingUser, $org->getId());
$link = $this->urlGenerator->generate('user_accept',[
'id' => $existingUser->getId(),
'token' => $token
@@ -73,10 +72,26 @@ class EmailService
]);
try{
+ $orgId = $org->getId();
+ $this->loggerService->logEmailSent($existingUser->getId(), $orgId, 'Existing user notification email sent.');
$this->mailer->send($email);
} catch (TransportExceptionInterface $e) {
$this->logger->error('Failed to send existing user notification email: ' . $e->getMessage());
}
}
-}
\ No newline at end of file
+
+ private function getOrgFromToken(string $token): ?int
+ {
+ if (str_starts_with($token, 'o')) {
+ $parts = explode('@', $token);
+ if (count($parts) === 2) {
+ $orgPart = substr($parts[0], 1); // Remove the leading 'o'
+ if (is_numeric($orgPart)) {
+ return (int)$orgPart;
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/Service/LoggerService.php b/src/Service/LoggerService.php
new file mode 100644
index 0000000..a610708
--- /dev/null
+++ b/src/Service/LoggerService.php
@@ -0,0 +1,263 @@
+userManagementLogger->notice("New user created: $userId", [
+ 'target_user_id' => $userId,
+ 'acting_user_id' => $actingUserId,
+ 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown',
+ 'timestamp' => $this->now(),
+ ]);
+ }
+
+ // Organization Management Logs
+ public function logUserOrganizationLinkCreated(int $userId, int $orgId, int $actingUserId, ?int $uoId): void
+ {
+ $this->organizationManagementLogger->notice('User-Organization link created', [
+ 'target_user_id' => $userId,
+ 'organization_id' => $orgId,
+ 'acting_user_id' => $actingUserId,
+ 'uo_id' => $uoId,
+ 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown',
+ 'timestamp' => $this->now(),
+ ]);
+ }
+
+ public function logExistingUserAddedToOrg(int $userId, int $orgId, int $actingUserId, int $uoId): void
+ {
+ $this->organizationManagementLogger->notice('Existing user added to organization', [
+ 'target_user_id' => $userId,
+ 'organization_id' => $orgId,
+ 'acting_user_id' => $actingUserId,
+ 'uo_id' => $uoId,
+ 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown',
+ 'timestamp' => $this->now(),
+ ]);
+ }
+
+ // Email Notification Logs
+ public function logEmailSent(int $userId, ?int $orgId, string $message): void
+ {
+ $this->emailNotificationLogger->notice($message, [
+ 'target_user_id' => $userId,
+ 'organization_id' => $orgId,
+ 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown',
+ 'timestamp' => $this->now(),
+ ]);
+ }
+
+ public function logExistingUserNotificationSent(int $userId, int $orgId): void
+ {
+ $this->emailNotificationLogger->notice("Existing user notification email sent to $userId", [
+ 'target_user_id' => $userId,
+ 'organization_id' => $orgId,
+ 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown',
+ 'timestamp' => $this->now(),
+ ]);
+ }
+
+ public function logAdminNotified(array $array): void
+ {
+ $this->emailNotificationLogger->notice('Organization admin notified', array_merge($array, [
+ 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown',
+ 'timestamp' => $this->now(),
+ ]));
+ }
+
+ public function logSuperAdmin(int $userId, int $actingUserId, string $message, ?int $orgId = null): void
+ {
+ $this->adminActionsLogger->notice($message, [
+ 'target_user_id' => $userId,
+ 'organization_id' => $orgId,
+ 'acting_user_id' => $actingUserId,
+ 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown',
+ 'timestamp' => $this->now(),
+ ]);
+ }
+
+ // Error Logs
+ public function logError(string $message, array $context = []): void
+ {
+ $this->errorLogger->error($message, array_merge($context, [
+ 'timestamp' => $this->now(),
+ 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown',
+ ]));
+ }
+
+ public function logCritical(string $message, array $context = []): void
+ {
+ $this->errorLogger->critical($message, array_merge($context, [
+ 'timestamp' => $this->now(),
+ 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown',
+ ]));
+ }
+
+ // Security Logs
+ public function logAccessDenied(?int $actingUserId): void
+ {
+ $this->securityLogger->warning('Access denied', [
+ 'acting_user_id' => $actingUserId,
+ 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown',
+ 'timestamp' => $this->now(),
+ 'page_accessed' => $_SERVER['REQUEST_URI'] ?? 'unknown',
+ ]);
+ }
+
+ // Helper
+ private function now(): string
+ {
+ return (new \DateTimeImmutable('now'))->format(DATE_ATOM);
+ }
+
+
+ public function logUserAction(int $targetId, int $actingUserId, string $message): void
+ {
+ $this->userManagementLogger->notice($message, [
+ 'target_user_id'=> $targetId,
+ 'acting_user_id'=> $actingUserId,
+ 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown',
+ 'timestamp' => $this->now(),
+ ]);
+ }
+
+ public function logAdminAction(int $targetId, int $actingUserId, int $organizationId, string $message): void
+ {
+ $this->adminActionsLogger->notice($message, [
+ 'target_id' => $targetId,
+ 'acting_user_id'=> $actingUserId,
+ 'organization_id'=> $organizationId,
+ 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown',
+ 'timestamp' => $this->now(),
+ ]);
+ }
+
+ public function logEntityNotFound(string $entityType, array $criteria, ?int $actingUserId): void
+ {
+ $this->errorLogger->error('Entity not found', array_merge($criteria, [
+ 'entity_type' => $entityType,
+ 'acting_user_id' => $actingUserId,
+ 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown',
+ 'timestamp' => $this->now(),
+ 'page_accessed' => $_SERVER['REQUEST_URI'] ?? 'unknown',
+ ]));
+ }
+
+ public function logAWSAction(string $action, array $details): void
+ {
+ $this->awsLogger->info("AWS action performed: $action", array_merge($details, [
+ 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown',
+ 'timestamp' => $this->now(),
+ ]));
+ }
+
+ public function logTokenRevocation(string $message, array $array): void
+ {
+ $this->securityLogger->warning($message, array_merge($array, [
+ 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown',
+ 'timestamp' => $this->now(),
+ ]));
+ }
+
+ public function logUOALinkDeactivated(int $uoaId, int $appId, int $roleId): void
+ {
+ $this->organizationManagementLogger->notice('UOA link deactivated', [
+ 'uoa_id' => $uoaId,
+ 'app_id' => $appId,
+ 'role_id' => $roleId,
+ 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown',
+ 'timestamp' => $this->now(),
+ ]);
+ }
+
+ public function logOrganizationInformation(int $organizationId, int $actingUserId, string $message): void
+ {
+ $this->organizationManagementLogger->info($message, [
+ 'organization_id' => $organizationId,
+ 'acting_user_id' => $actingUserId,
+ 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown',
+ 'timestamp' => $this->now(),
+ ]);
+ }
+
+ public function logRoleEntityAssignment(int $userId, int $organizationId, int $roleId, int $actingUserId, string $message): void
+ {
+ $this->accessControlLogger->info($message, [
+ 'target_user_id' => $userId,
+ 'organization_id' => $organizationId,
+ 'role_id' => $roleId,
+ 'acting_user_id' => $actingUserId,
+ 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown',
+ 'timestamp' => $this->now(),
+ ]);
+ }
+ public function logRoleAssignment(string $message, array $context): void
+ {
+ $this->accessControlLogger->info($message, [
+ 'context' => $context,
+ 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown',
+ 'timestamp' => $this->now(),
+ ]);
+ }
+
+ public function logUserConnection(string $message, array $array)
+ {
+ $this->securityLogger->info($message, array_merge($array, [
+ 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown',
+ 'timestamp' => $this->now(),
+ ]));
+ }
+
+ public function logCGUAcceptance(int $it)
+ {
+ $this->userManagementLogger->info("User accepted CGU", [
+ 'user_id' => $it,
+ 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown',
+ 'timestamp' => $this->now(),
+ ]);
+ $this->securityLogger->info("User accepted CGU", [
+ 'user_id' => $it,
+ 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown',
+ 'timestamp' => $this->now(),
+ ]);
+ }
+
+ public function logTokenError(string $message, array $context = []): void
+ {
+ $this->securityLogger->error($message, array_merge($context, [
+ 'timestamp' => $this->now(),
+ 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown',
+ ]));
+ }
+
+ public function logApplicationInformation(string $string, array $array, int $actingUser)
+ {
+ $this->accessControlLogger->info($string, array_merge($array, [
+ 'acting_user_id' => $actingUser,
+ 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown',
+ 'timestamp' => $this->now(),
+ ]));
+ }
+}
diff --git a/src/Service/NotificationService.php b/src/Service/NotificationService.php
index c824b1e..49c23cd 100644
--- a/src/Service/NotificationService.php
+++ b/src/Service/NotificationService.php
@@ -65,7 +65,7 @@ class NotificationService
$this->send(
recipient: $recipient,
type: self::TYPE_USER_DEACTIVATED,
- title: 'Membre retiré',
+ title: 'Membre désactivé',
message: sprintf('%s %s a été désactivé de %s', $removedUser->getName(), $removedUser->getSurname(), $organization->getName()),
data: [
'userId' => $removedUser->getId(),
diff --git a/src/Service/OrganizationsService.php b/src/Service/OrganizationsService.php
index ece09b3..25e2400 100644
--- a/src/Service/OrganizationsService.php
+++ b/src/Service/OrganizationsService.php
@@ -6,8 +6,11 @@ use App\Entity\Apps;
use App\Entity\Organizations;
use App\Entity\Roles;
use App\Entity\UserOrganizatonApp;
+use App\Entity\UsersOrganizations;
use App\Repository\UsersOrganizationsRepository;
+use App\Service\LoggerService;
use Doctrine\ORM\EntityManagerInterface;
+use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
class OrganizationsService
@@ -15,10 +18,11 @@ class OrganizationsService
private string $logoDirectory;
public function __construct(
- string $logoDirectory, private readonly AwsService $awsService,
- private readonly EntityManagerInterface $entityManager,
+ string $logoDirectory, private readonly AwsService $awsService,
+ private readonly EntityManagerInterface $entityManager,
private readonly UsersOrganizationsRepository $uoRepository,
- private readonly NotificationService $notificationService
+ private readonly NotificationService $notificationService,
+ private readonly LoggerInterface $emailNotificationLogger, private readonly LoggerService $loggerService,
)
{
$this->logoDirectory = $logoDirectory;
@@ -32,8 +36,18 @@ class OrganizationsService
try {
$this->awsService->PutDocObj($_ENV['S3_PORTAL_BUCKET'], $logoFile, $customFilename, $extension, 'logo/');
+ $this->loggerService->logAWSAction('Upload organization logo', [
+ 'organization_id' => $organization->getId(),
+ 'filename' => $customFilename,
+ 'bucket' => $_ENV['S3_PORTAL_BUCKET'],
+ ]);
$organization->setLogoUrl('logo/' . $customFilename);
} catch (FileException $e) {
+ $this->loggerService->logError('Failed to upload organization logo to S3', [
+ 'organization_id' => $organization->getId(),
+ 'error' => $e->getMessage(),
+ 'bucket' => $_ENV['S3_PORTAL_BUCKET'],
+ ]);
throw new FileException('Failed to upload logo to S3: ' . $e->getMessage());
}
}
@@ -85,6 +99,10 @@ class OrganizationsService
$newUser,
$data['organization']
);
+ $this->loggerService->logAdminNotified([
+ 'admin_user_id' =>$adminUO->getUsers()->getId(),
+ 'target_user_id' => $newUser->getId(),
+ 'organization_id' => $data['organization']->getId(),'case' =>$type]);
}
break;
case 'USER_INVITED':
@@ -95,7 +113,12 @@ class OrganizationsService
$invitedUser,
$data['organization']
);
+ $this->loggerService->logAdminNotified([
+ 'admin_user_id' =>$adminUO->getUsers()->getId(),
+ 'target_user_id' => $invitedUser->getId(),
+ 'organization_id' => $data['organization']->getId(),'case' =>$type]);
}
+
break;
case 'USER_DEACTIVATED':
if ($uoa && $adminUO->getUsers()->getId() !== $data['user']->getId() ) {
@@ -105,7 +128,12 @@ class OrganizationsService
$removedUser,
$data['organization']
);
+ $this->loggerService->logAdminNotified([
+ 'admin_user_id' =>$adminUO->getUsers()->getId(),
+ 'target_user_id' => $removedUser->getId(),
+ 'organization_id' => $data['organization']->getId(),'case' =>$type]);
}
+
break;
case 'USER_DELETED':
if ($uoa && $adminUO->getUsers()->getId() !== $data['user']->getId() ) {
@@ -115,6 +143,10 @@ class OrganizationsService
$removedUser,
$data['organization']
);
+ $this->loggerService->logAdminNotified([
+ 'admin_user_id' =>$adminUO->getUsers()->getId(),
+ 'target_user_id' => $removedUser->getId(),
+ 'organization_id' => $data['organization']->getId(),'case' =>$type]);
}
break;
case 'USER_ACTIVATED':
@@ -125,6 +157,10 @@ class OrganizationsService
$activatedUser,
$data['organization']
);
+ $this->loggerService->logAdminNotified([
+ 'admin_user_id' =>$adminUO->getUsers()->getId(),
+ 'target_user_id' => $activatedUser->getId(),
+ 'organization_id' => $data['organization']->getId(),'case' =>$type]);
}
break;
}
diff --git a/src/Service/UserOrganizationAppService.php b/src/Service/UserOrganizationAppService.php
index 67c9613..bf2615f 100644
--- a/src/Service/UserOrganizationAppService.php
+++ b/src/Service/UserOrganizationAppService.php
@@ -8,13 +8,15 @@ use App\Entity\User;
use App\Entity\UserOrganizatonApp;
use App\Entity\UsersOrganizations;
use App\Service\ActionService;
+use App\Service\LoggerService;
use App\Service\UserService;
use Doctrine\ORM\EntityManagerInterface;
+use Psr\Log\LoggerInterface;
use Symfony\Bundle\SecurityBundle\Security;
class UserOrganizationAppService
{
- public function __construct(private readonly EntityManagerInterface $entityManager, private readonly ActionService $actionService, private readonly Security $security, private readonly UserService $userService)
+ public function __construct(private readonly EntityManagerInterface $entityManager, private readonly ActionService $actionService, private readonly Security $security, private readonly UserService $userService, private readonly LoggerInterface $logger, private readonly LoggerService $loggerService)
{
}
@@ -79,10 +81,20 @@ class UserOrganizationAppService
$uoas = $this->entityManager->getRepository(UserOrganizatonApp::class)->findBy(['userOrganization' => $userOrganization, 'isActive' => true]);
}
foreach ($uoas as $uoa) {
- $uoa->setIsActive(false);
- $this->actionService->createAction("Deactivate UOA link", $userOrganization->getUsers(),
- $userOrganization->getOrganization(), "App: " . $uoa->getApplication()->getName() . ", Role: " . $uoa->getRole()->getName());
- $this->entityManager->persist($uoa);
+ try{
+ $uoa->setIsActive(false);
+ $this->actionService->createAction("Deactivate UOA link", $userOrganization->getUsers(),
+ $userOrganization->getOrganization(), "App: " . $uoa->getApplication()->getName() . ", Role: " . $uoa->getRole()->getName());
+ $this->entityManager->persist($uoa);
+ $this->loggerService->logUOALinkDeactivated($uoa->getId(), $uoa->getApplication()->getId(), $uoa->getRole()->getId());
+ }catch (\Exception $exception){
+ $this->loggerService->logCritical("Error deactivating UOA link", [
+ 'uoa_id' => $uoa->getId(),
+ 'app_id' => $uoa->getApplication()->getId(),
+ 'role_id' => $uoa->getRole()->getId(),
+ 'exception_message' => $exception->getMessage(),
+ ]);
+ }
}
}
@@ -128,6 +140,11 @@ class UserOrganizationAppService
if (!$uoa->isActive()) {
$uoa->setIsActive(true);
$this->entityManager->persist($uoa);
+ $this->loggerService->logOrganizationInformation(
+ $uo->getOrganization()->getId(),
+ $actingUser->getId(),
+ "Re-activated role '$roleName' for user '{$uo->getUsers()->getId()}' in application '{$application->getName()} with UOA ID {$uoa->getId()}'"
+ );
$this->actionService->createAction(
"Re-activate user role for application",
$actingUser,
@@ -148,7 +165,11 @@ class UserOrganizationAppService
if ($uoa->isActive()) {
$uoa->setIsActive(false);
$this->entityManager->persist($uoa);
-
+ $this->loggerService->logOrganizationInformation(
+ $uo->getOrganization()->getId(),
+ $actingUser->getId(),
+ "Deactivated role '$roleName' for user '{$uo->getUsers()->getId()}' in application '{$application->getName()}' with UOA ID {$uoa->getId()}'"
+ );
$this->actionService->createAction(
"Deactivate user role for application",
$actingUser,
@@ -185,6 +206,11 @@ class UserOrganizationAppService
$this->ensureAdminRoleForSuperAdmin($newUoa);
}
$this->entityManager->persist($newUoa);
+ $this->loggerService->logOrganizationInformation(
+ $uo->getOrganization()->getId(),
+ $actingUser->getId(),
+ "Created new role '{$role->getName()}' for user '{$uo->getUsers()->getId()}' in application '{$application->getName()}' with UOA ID {$newUoa->getId()}'"
+ );
$this->actionService->createAction("New user role for application",
$actingUser,
$uo->getOrganization(),
diff --git a/src/Service/UserOrganizationService.php b/src/Service/UserOrganizationService.php
index 8a323da..b5caca9 100644
--- a/src/Service/UserOrganizationService.php
+++ b/src/Service/UserOrganizationService.php
@@ -7,6 +7,7 @@ use App\Entity\Organizations;
use App\Entity\User;
use App\Entity\UsersOrganizations;
use App\Service\ActionService;
+use App\Service\LoggerService;
use \App\Service\UserOrganizationAppService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@@ -19,7 +20,7 @@ readonly class UserOrganizationService
{
public function __construct(
- private userOrganizationAppService $userOrganizationAppService, private EntityManagerInterface $entityManager, private ActionService $actionService,
+ private userOrganizationAppService $userOrganizationAppService, private EntityManagerInterface $entityManager, private ActionService $actionService, private LoggerService $loggerService,
) {
}
@@ -41,22 +42,19 @@ readonly class UserOrganizationService
$uos = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['organization' => $organizations, 'isActive' => true]);
}
//deactivate all UO links
- foreach ($uos as $uo) {
- $this->userOrganizationAppService->deactivateAllUserOrganizationsAppLinks($uo);
- $uo->setIsActive(false);
- $this->entityManager->persist($uo);
- $this->actionService->createAction("Deactivate UO link", $actingUser, $uo->getOrganization(), $uo->getOrganization()->getName() );
+ if (!empty($uos)) {
+ foreach ($uos as $uo) {
+ $this->userOrganizationAppService->deactivateAllUserOrganizationsAppLinks($uo);
+ $this->loggerService->logOrganizationInformation($uo->getOrganization()->getId(), $actingUser->getId(),
+ 'Uo link deactivated');
+ $uo->setIsActive(false);
+ $this->entityManager->persist($uo);
+ $this->actionService->createAction("Deactivate UO link", $actingUser, $uo->getOrganization(), $uo->getOrganization()->getName() );
+ }
}
+
}
- public function getByIdOrFail(int $id): UsersOrganizations
- {
- $uo = $this->entityManager->getRepository(UsersOrganizations::class)->find($id);
- if (!$uo) {
- throw new NotFoundHttpException("UserOrganization not found");
- }
- return $uo;
- }
}
diff --git a/src/Service/UserService.php b/src/Service/UserService.php
index ae5e908..3e7ae08 100644
--- a/src/Service/UserService.php
+++ b/src/Service/UserService.php
@@ -8,7 +8,6 @@ use App\Entity\Roles;
use App\Entity\User;
use App\Entity\UserOrganizatonApp;
use App\Entity\UsersOrganizations;
-use App\Service\AwsService;
use DateTimeImmutable;
use DateTimeZone;
use Doctrine\ORM\EntityManagerInterface;
@@ -16,18 +15,25 @@ use Doctrine\ORM\EntityNotFoundException;
use Exception;
use League\Bundle\OAuth2ServerBundle\Model\AccessToken;
use Random\RandomException;
-use SebastianBergmann\CodeCoverage\Util\DirectoryCouldNotBeCreatedException;
+use RuntimeException;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
+use App\Event\UserCreatedEvent;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
class UserService
{
public const NOT_FOUND = 'Entity not found';
- public function __construct(private readonly EntityManagerInterface $entityManager,
- private readonly Security $security,
- private readonly AwsService $awsService
+ public function __construct(private readonly EntityManagerInterface $entityManager,
+ private readonly Security $security,
+ private readonly AwsService $awsService,
+ private readonly LoggerService $loggerService,
+ private readonly ActionService $actionService,
+ private readonly EmailService $emailService,
+ private readonly OrganizationsService $organizationsService,
+ private readonly EventDispatcherInterface $eventDispatcher
)
{
@@ -39,16 +45,7 @@ class UserService
*/
public function generateRandomPassword(): string
{
- $length = 50; // Length of the password
- $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+';
- $charactersLength = strlen($characters);
- $randomPassword = '';
-
- for ($i = 0; $i < $length; $i++) {
- $randomPassword .= $characters[random_int(0, $charactersLength - 1)];
- }
-
- return $randomPassword;
+ return bin2hex(random_bytes(32));
}
@@ -88,20 +85,20 @@ class UserService
*/
public function hasAccessTo(User $user, bool $skipSelfCheck = false): bool
{
+ if ($this->security->isGranted('ROLE_SUPER_ADMIN')) {
+ return true;
+ }
if (!$skipSelfCheck && $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())) {
+ if ($this->isAdminOfOrganization($uo->getOrganization()) && $uo->getStatut() === "ACCEPTED" && $uo->isActive()) {
return true;
}
}
}
- if ($this->security->isGranted('ROLE_SUPER_ADMIN')) {
- return true;
- }
return false;
}
@@ -112,7 +109,7 @@ class UserService
* entity role 'ROLE_ADMIN' in the UsersOrganizationsApp entity
* (if he is admin for any application of the organization).
*
- * @param UsersOrganizations $usersOrganizations
+ * @param Organizations $organizations
* @return bool
* @throws Exception
*/
@@ -144,6 +141,7 @@ class UserService
{
$user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $userIdentifier]);
if (!$user) {
+ $this->loggerService->logEntityNotFound('User', ['user_identifier' => $userIdentifier], null);
throw new EntityNotFoundException(self::NOT_FOUND);
}
return $user;
@@ -175,18 +173,20 @@ class UserService
return ['none' => $group];
}
-//TODO: reset function
public function handleProfilePicture(User $user, $picture): void
{
// Get file extension
$extension = $picture->guessExtension();
- // Create custom filename: userNameUserSurname_ddmmyyhhmmss
+ // Create custom filename: userNameUserSurname_dmyHis
$customFilename = $user->getName() . $user->getSurname() . '_' . date('dmyHis') . '.' . $extension;
-// $customFilename = $user->getName() . $user->getSurname() . "." .$extension;
try {
$this->awsService->PutDocObj($_ENV['S3_PORTAL_BUCKET'], $picture, $customFilename, $extension, 'profile/');
-
+ $this->loggerService->logAWSAction(
+ 'Profile picture uploaded to S3', [
+ 'user_id' => $user->getId(),
+ 'filename' => $customFilename,
+ ]);
$user->setPictureUrl('profile/' . $customFilename);
} catch (FileException $e) {
// Handle upload error
@@ -242,12 +242,18 @@ class UserService
if ($roleFormatted === 'ROLE_SUPER_ADMIN' && !in_array('ROLE_ADMIN', $user->getRoles(), true)) {
$user->setRoles(array_merge($user->getRoles(), ['ROLE_ADMIN']));
}
+ $this->loggerService->logRoleAssignment(
+ 'Role assigned to user',
+ [
+ 'user_id' => $user->getId(),
+ 'role' => $roleFormatted,
+ ]
+ );
} else {
// Remove the role if present and not used elsewhere
if (in_array($roleFormatted, $user->getRoles(), true)) {
$uos = $this->entityManager->getRepository(UsersOrganizations::class)
->findBy(['users' => $user, 'isActive' => true]);
-
$hasRole = false;
foreach ($uos as $uo) {
$uoa = $this->entityManager->getRepository(UserOrganizatonApp::class)
@@ -257,7 +263,6 @@ class UserService
'role' => $this->entityManager->getRepository(Roles::class)
->findOneBy(['name' => $role]),
]);
-
if ($uoa) {
$hasRole = true;
break;
@@ -287,17 +292,6 @@ class UserService
return 'ROLE_' . $role;
}
- public function revokeUserTokens(string $userIdentifier)
- {
- $tokens = $this->entityManager->getRepository(AccessToken::class)->findBy([
- 'userIdentifier' => $userIdentifier,
- 'revoked' => false
- ]);
-
- foreach ($tokens as $token) {
- $token->revoke();
- }
- }
public function formatStatutForOrganizations(array $rows): array
{
@@ -330,7 +324,7 @@ class UserService
return $formatted;
}
- public function generatePasswordToken(User $user, int $orgId): string
+ public function generatePasswordToken(User $user, int $orgId = null): string
{
$orgString = "o" . $orgId . "@";
$token = $orgString . bin2hex(random_bytes(32));
@@ -413,15 +407,17 @@ class UserService
return $rolesArray;
}
- public function canEditRolesCheck(User $actingUser, User $user, $org, bool $isAdmin): bool
+ public function canEditRolesCheck(User $actingUser, User $user, bool $isAdmin, UsersOrganizations $uo = null, $org = null): bool
{
$userRoles = $user->getRoles();
$actingUserRoles = $actingUser->getRoles();
// if acting user is admin, he can´t edit super admin roles
-
- if (in_array('ROLE_SUPER_ADMIN', $userRoles, true) && !in_array('ROLE_SUPER_ADMIN', $actingUserRoles, true)) {
+ if (!in_array('ROLE_SUPER_ADMIN', $actingUserRoles, true) && in_array('ROLE_SUPER_ADMIN', $userRoles, true)) {
return false;
}
+ if ($uo && $this->isAdminOfOrganization($uo->getOrganization())) {
+ return true;
+ }
return $isAdmin && !empty($org);
}
@@ -434,7 +430,7 @@ class UserService
* @param Organizations $organization
* @return void
*/
- public function handleExistingUser(User $user, Organizations $organization): void
+ public function handleExistingUser(User $user, Organizations $organization): int
{
if (!$user->isActive()) {
$user->setIsActive(true);
@@ -448,6 +444,8 @@ class UserService
$uo->setModifiedAt(new \DateTimeImmutable('now'));
$this->entityManager->persist($uo);
$this->entityManager->flush();
+
+ return $uo->getId();
}
/**
@@ -460,7 +458,7 @@ class UserService
* @param User $user
* @return void
*/
- public function formatNewUserData(User $user, $picture, bool $setPassword = false): void
+ public function formatUserData(User $user, $picture, bool $setPassword = false): void
{
// capitalize name and surname
$user->setName(ucfirst(strtolower($user->getName())));
@@ -470,13 +468,163 @@ class UserService
$user->setName(trim($user->getName()));
$user->setSurname(trim($user->getSurname()));
$user->setEmail(trim($user->getEmail()));
- if($setPassword) {
+ if ($setPassword) {
//FOR SETTING A DEFAULT RANDOM PASSWORD OF 50 CHARACTERS until user set his own password
- $user->setPassword($this->generateRandomPassword());
+ try {
+ $user->setPassword(bin2hex(random_bytes(50)));
+ } catch (RandomException $e) {
+ $this->loggerService->logError('Error generating random password: ' . $e->getMessage(), [
+ 'target_user_id' => $user->getId(),
+ ]);
+ throw new RuntimeException('Error generating random password: ' . $e->getMessage());
+ }
}
- if($picture) {
+ if ($picture) {
$this->handleProfilePicture($user, $picture);
}
}
+
+ /**
+ * Handle existing user being added to an organization
+ */
+ public function addExistingUserToOrganization(
+ User $existingUser,
+ Organizations $org,
+ User $actingUser,
+ ): int
+ {
+ try {
+ $uoId = $this->handleExistingUser($existingUser, $org);
+
+ $this->loggerService->logExistingUserAddedToOrg(
+ $existingUser->getId(),
+ $org->getId(),
+ $actingUser->getId(),
+ $uoId,
+ );
+ $this->actionService->createAction(
+ "Add existing user to organization",
+ $actingUser,
+ $org,
+ "Added user {$existingUser->getUserIdentifier()} to {$org->getName()}"
+ );
+ $this->sendExistingUserNotifications($existingUser, $org, $actingUser);
+
+ return $uoId;
+ } catch (\Exception $e) {
+ $this->loggerService->logError('Error linking existing user to organization: ' . $e->getMessage(), [
+ 'target_user_id' => $existingUser->getId(),
+ 'organization_id' => $org->getId(),
+ 'acting_user_id' => $actingUser->getId(),
+ ]);
+ throw $e;
+ }
+ }
+
+ /**
+ * Create a brand-new user
+ */
+ public function createNewUser(User $user, User $actingUser, $picture): void
+ {
+ try {
+ $this->formatUserData($user, $picture, true);
+
+ // Generate token here if it's part of the user persistence flow
+ $token = $this->generatePasswordToken($user);
+
+ $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(),
+ ]);
+ throw $e;
+ }
+ }
+
+ /**
+ * Link newly created user to an organization
+ */
+ public function linkUserToOrganization(
+ User $user,
+ Organizations $org,
+ User $actingUser,
+ ): UsersOrganizations
+ {
+ try {
+ $uo = new UsersOrganizations();
+ $uo->setUsers($user);
+ $uo->setOrganization($org);
+ $uo->setStatut("INVITED");
+ $uo->setIsActive(false);
+ $uo->setModifiedAt(new \DateTimeImmutable('now'));
+ $this->entityManager->persist($uo);
+ $this->entityManager->flush();
+
+ $this->loggerService->logUserOrganizationLinkCreated(
+ $user->getId(),
+ $org->getId(),
+ $actingUser->getId(),
+ $uo->getId(),
+
+ );
+
+ $this->actionService->createAction(
+ "Link user to organization",
+ $actingUser,
+ $org,
+ "Added {$user->getUserIdentifier()} to {$org->getName()}"
+ );
+
+ $this->sendNewUserNotifications($user, $org, $actingUser);
+
+ return $uo;
+ } catch (\Exception $e) {
+ $this->loggerService->logError('Error linking user to organization: ' . $e->getMessage(), [
+ 'target_user_id' => $user->getId(),
+ 'organization_id' => $org->getId(),
+ 'acting_user_id' => $actingUser->getId(),
+ ]);
+ throw $e;
+ }
+ }
+
+ // Private helpers for email notifications
+ private function sendExistingUserNotifications(User $user, Organizations $org, User $actingUser): void
+ {
+ try {
+ $token = $this->generatePasswordToken($user, $org->getId());
+ $this->emailService->sendExistingUserNotificationEmail($user, $org, $token);
+ $this->loggerService->logExistingUserNotificationSent($user->getId(), $org->getId());
+ $this->organizationsService->notifyOrganizationAdmins(['user' => $user, 'acting_user_id' => $actingUser->getId(),
+ 'organization' => $org], 'USER_INVITED');
+ } catch (\Exception $e) {
+ $this->loggerService->logError("Error sending existing user notification: " . $e->getMessage(), [
+ 'target_user_id' => $user->getId(),
+ 'organization_id' => $org->getId(),
+ ]);
+ }
+ }
+
+ private function sendNewUserNotifications(User $user, Organizations $org, User $actingUser): void
+ {
+ try {
+ $token = $this->generatePasswordToken($user, $org->getId());
+ $this->emailService->sendPasswordSetupEmail($user, $token);
+ $this->organizationsService->notifyOrganizationAdmins(['user' => $user, 'acting_user_id' => $actingUser->getId(),
+ 'organization' => $org], 'USER_INVITED');
+ } catch (\Exception $e) {
+ $this->loggerService->logError("Error sending password setup email: " . $e->getMessage(), [
+ 'target_user_id' => $user->getId(),
+ 'organization_id' => $org->getId(),
+ ]);
+ }
+ }
+
}
diff --git a/symfony.lock b/symfony.lock
index ef4308d..0644b1a 100644
--- a/symfony.lock
+++ b/symfony.lock
@@ -11,6 +11,18 @@
"config/packages/aws.yaml"
]
},
+ "dama/doctrine-test-bundle": {
+ "version": "8.3",
+ "recipe": {
+ "repo": "github.com/symfony/recipes-contrib",
+ "branch": "main",
+ "version": "8.3",
+ "ref": "dfc51177476fb39d014ed89944cde53dc3326d23"
+ },
+ "files": [
+ "config/packages/dama_doctrine_test_bundle.yaml"
+ ]
+ },
"doctrine/deprecations": {
"version": "1.1",
"recipe": {
diff --git a/templates/application/appSmall.html.twig b/templates/application/appSmall.html.twig
index 14faa93..77fd5e4 100644
--- a/templates/application/appSmall.html.twig
+++ b/templates/application/appSmall.html.twig
@@ -14,7 +14,7 @@
{% if application.hasAccess %}
{% if is_granted("ROLE_SUPER_ADMIN") %}