Merge branch 'develop' into dockerize-portal
This commit is contained in:
commit
3113313ad3
|
|
@ -1,6 +1,7 @@
|
||||||
|
|
||||||
###> symfony/framework-bundle ###
|
###> symfony/framework-bundle ###
|
||||||
/.env.local
|
/.env.local
|
||||||
|
/.env.test
|
||||||
/.env.local.php
|
/.env.local.php
|
||||||
/.env.*.local
|
/.env.*.local
|
||||||
/config/secrets/prod/prod.decrypt.private.php
|
/config/secrets/prod/prod.decrypt.private.php
|
||||||
|
|
@ -15,11 +16,6 @@
|
||||||
.phpunit.result.cache
|
.phpunit.result.cache
|
||||||
###< phpunit/phpunit ###
|
###< phpunit/phpunit ###
|
||||||
|
|
||||||
###> symfony/phpunit-bridge ###
|
|
||||||
.phpunit.result.cache
|
|
||||||
/phpunit.xml
|
|
||||||
###< symfony/phpunit-bridge ###
|
|
||||||
|
|
||||||
###> symfony/asset-mapper ###
|
###> symfony/asset-mapper ###
|
||||||
/public/assets/
|
/public/assets/
|
||||||
/assets/vendor/
|
/assets/vendor/
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/mtdowling/jmespath.php" />
|
<excludeFolder url="file://$MODULE_DIR$/vendor/mtdowling/jmespath.php" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-client" />
|
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-client" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/ralouphie/getallheaders" />
|
<excludeFolder url="file://$MODULE_DIR$/vendor/ralouphie/getallheaders" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/rate-limiter" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
|
|
||||||
|
|
@ -179,6 +179,8 @@
|
||||||
<path value="$PROJECT_DIR$/vendor/mtdowling/jmespath.php" />
|
<path value="$PROJECT_DIR$/vendor/mtdowling/jmespath.php" />
|
||||||
<path value="$PROJECT_DIR$/vendor/psr/http-client" />
|
<path value="$PROJECT_DIR$/vendor/psr/http-client" />
|
||||||
<path value="$PROJECT_DIR$/vendor/twig/extra-bundle" />
|
<path value="$PROJECT_DIR$/vendor/twig/extra-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/staabm/side-effects-detector" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/rate-limiter" />
|
||||||
</include_path>
|
</include_path>
|
||||||
</component>
|
</component>
|
||||||
<component name="PhpProjectSharedConfiguration" php_language_level="8.2" />
|
<component name="PhpProjectSharedConfiguration" php_language_level="8.2" />
|
||||||
|
|
|
||||||
|
|
@ -36,3 +36,6 @@
|
||||||
``` html
|
``` html
|
||||||
<div class="card p-3">
|
<div class="card p-3">
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
php bin/console messenger:consume async -vv
|
||||||
|
|
@ -4,20 +4,36 @@ import {TabulatorFull as Tabulator} from 'tabulator-tables';
|
||||||
import {eyeIconLink, TABULATOR_FR_LANG} from "../js/global.js";
|
import {eyeIconLink, TABULATOR_FR_LANG} from "../js/global.js";
|
||||||
|
|
||||||
export default class extends Controller {
|
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() {
|
connect() {
|
||||||
|
if(this.activitiesValue){
|
||||||
|
this.loadActivities();
|
||||||
|
setInterval(() => {
|
||||||
|
this.loadActivities();
|
||||||
|
}, 60000); // Refresh every 60 seconds
|
||||||
|
}
|
||||||
|
if (this.tableValue && this.sadminValue) {
|
||||||
this.table();
|
this.table();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
table(){
|
table(){
|
||||||
const table = new Tabulator("#tabulator-org", {
|
const table = new Tabulator("#tabulator-org", {
|
||||||
// Register locales here
|
// Register locales here
|
||||||
langs: TABULATOR_FR_LANG,
|
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)
|
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",
|
ajaxConfig: "GET",
|
||||||
pagination: true,
|
pagination: true,
|
||||||
paginationMode: "remote",
|
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 = `<div class="text-danger">Erreur lors du chargement.</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 `
|
||||||
|
<div class="card shadow-sm mb-3 border-0 bg-white rounded-end"
|
||||||
|
style="border-left: 6px solid ${activity.color} !important;">
|
||||||
|
|
||||||
|
<div class="card-header bg-transparent border-0 pb-0 pt-3">
|
||||||
|
<h6 class="text-muted text-uppercase fw-bold mb-0" style="font-size: 0.85rem;">
|
||||||
|
${activity.date}
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body pt-2 pb-4">
|
||||||
|
<div class="card-text fs-5 lh-sm">
|
||||||
|
<span class="fw-bold text-dark">${activity.userName}</span>
|
||||||
|
<div class="text-secondary mt-1">${activity.actionType}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
this.activityListTarget.innerHTML = html;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -58,6 +58,7 @@ export default class extends Controller {
|
||||||
table() {
|
table() {
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
|
placeholder: "Aucun utilisateur trouvé",
|
||||||
title: "",
|
title: "",
|
||||||
field: "isConnected",
|
field: "isConnected",
|
||||||
width: 40, // small column
|
width: 40, // small column
|
||||||
|
|
@ -365,7 +366,8 @@ export default class extends Controller {
|
||||||
vertAlign: "middle",
|
vertAlign: "middle",
|
||||||
headerSort: false,
|
headerSort: false,
|
||||||
formatter: (cell) => {
|
formatter: (cell) => {
|
||||||
const url = cell.getValue();
|
const url = cell.getValue() + '?organizationId=' + this.orgIdValue;
|
||||||
|
console.log(url);
|
||||||
if (url) {
|
if (url) {
|
||||||
return eyeIconLink(url);
|
return eyeIconLink(url);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@
|
||||||
"symfony/process": "7.2.*",
|
"symfony/process": "7.2.*",
|
||||||
"symfony/property-access": "7.2.*",
|
"symfony/property-access": "7.2.*",
|
||||||
"symfony/property-info": "7.2.*",
|
"symfony/property-info": "7.2.*",
|
||||||
|
"symfony/rate-limiter": "7.2.*",
|
||||||
"symfony/runtime": "7.2.*",
|
"symfony/runtime": "7.2.*",
|
||||||
"runtime/frankenphp-symfony": "^0.2.0",
|
"runtime/frankenphp-symfony": "^0.2.0",
|
||||||
"symfony/security-bundle": "7.2.*",
|
"symfony/security-bundle": "7.2.*",
|
||||||
|
|
@ -107,7 +108,8 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpunit/phpunit": "^9.5",
|
"dama/doctrine-test-bundle": "^8.3",
|
||||||
|
"phpunit/phpunit": "^11.0",
|
||||||
"symfony/browser-kit": "7.2.*",
|
"symfony/browser-kit": "7.2.*",
|
||||||
"symfony/css-selector": "7.2.*",
|
"symfony/css-selector": "7.2.*",
|
||||||
"symfony/debug-bundle": "7.2.*",
|
"symfony/debug-bundle": "7.2.*",
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -19,4 +19,5 @@ return [
|
||||||
Symfony\Bundle\MercureBundle\MercureBundle::class => ['all' => true],
|
Symfony\Bundle\MercureBundle\MercureBundle::class => ['all' => true],
|
||||||
Knp\Bundle\TimeBundle\KnpTimeBundle::class => ['all' => true],
|
Knp\Bundle\TimeBundle\KnpTimeBundle::class => ['all' => true],
|
||||||
Aws\Symfony\AwsBundle::class => ['all' => true],
|
Aws\Symfony\AwsBundle::class => ['all' => true],
|
||||||
|
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
when@test:
|
||||||
|
dama_doctrine_test:
|
||||||
|
enable_static_connection: true
|
||||||
|
enable_static_meta_data_cache: true
|
||||||
|
enable_static_query_cache: true
|
||||||
|
|
@ -9,28 +9,91 @@ monolog:
|
||||||
- security
|
- security
|
||||||
- php
|
- php
|
||||||
- error
|
- error
|
||||||
|
- aws_management
|
||||||
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
|
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
|
||||||
|
|
||||||
when@dev:
|
when@dev:
|
||||||
monolog:
|
monolog:
|
||||||
handlers:
|
handlers:
|
||||||
main:
|
critical_errors:
|
||||||
type: stream
|
type: fingers_crossed
|
||||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
action_level: critical
|
||||||
|
handler: error_nested
|
||||||
|
buffer_size: 50
|
||||||
|
|
||||||
|
error_nested:
|
||||||
|
type: rotating_file
|
||||||
|
path: "%kernel.logs_dir%/error.log"
|
||||||
level: debug
|
level: debug
|
||||||
channels: ["!event"]
|
max_files: 30
|
||||||
# uncomment to get logging in your browser
|
|
||||||
# you may have to allow bigger header sizes in your Web server configuration
|
error:
|
||||||
#firephp:
|
type: rotating_file
|
||||||
# type: firephp
|
path: "%kernel.logs_dir%/error.log"
|
||||||
# level: info
|
level: error # logs error, critical, alert, emergency
|
||||||
#chromephp:
|
max_files: 30
|
||||||
# type: chromephp
|
channels: [ error ]
|
||||||
# level: info
|
php_errors:
|
||||||
console:
|
type: rotating_file
|
||||||
type: console
|
path: "%kernel.logs_dir%/php_error.log"
|
||||||
process_psr_3_messages: false
|
level: warning # warnings, errors, fatals…
|
||||||
channels: ["!event", "!doctrine", "!console"]
|
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:
|
when@test:
|
||||||
monolog:
|
monolog:
|
||||||
|
|
@ -57,7 +120,7 @@ when@prod:
|
||||||
|
|
||||||
error_nested:
|
error_nested:
|
||||||
type: rotating_file
|
type: rotating_file
|
||||||
path: "%kernel.logs_dir%/error.log"
|
path: "%kernel.logs_dir%/critical.log"
|
||||||
level: debug
|
level: debug
|
||||||
max_files: 30
|
max_files: 30
|
||||||
|
|
||||||
|
|
@ -76,12 +139,18 @@ when@prod:
|
||||||
channels: [ php ]
|
channels: [ php ]
|
||||||
# User Management
|
# User Management
|
||||||
user_management:
|
user_management:
|
||||||
type: stream
|
type: rotating_file
|
||||||
path: "%kernel.logs_dir%/user_management.log"
|
path: "%kernel.logs_dir%/user_management.log"
|
||||||
level: info
|
level: info
|
||||||
channels: [user_management]
|
channels: [user_management]
|
||||||
max_files: 30
|
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
|
||||||
authentication:
|
authentication:
|
||||||
type: rotating_file
|
type: rotating_file
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,9 @@ security:
|
||||||
user_checker: App\Security\UserChecker
|
user_checker: App\Security\UserChecker
|
||||||
lazy: true
|
lazy: true
|
||||||
provider: app_user_provider
|
provider: app_user_provider
|
||||||
|
login_throttling:
|
||||||
|
max_attempts: 3
|
||||||
|
interval: '1 minute'
|
||||||
form_login:
|
form_login:
|
||||||
login_path: app_login
|
login_path: app_login
|
||||||
check_path: app_login
|
check_path: app_login
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
framework:
|
framework:
|
||||||
default_locale: en
|
default_locale: fr
|
||||||
translator:
|
translator:
|
||||||
default_path: '%kernel.project_dir%/translations'
|
default_path: '%kernel.project_dir%/translations'
|
||||||
fallbacks:
|
fallbacks:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260105152103 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260106080636 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260106084653 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE organizations 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,20 +1,15 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
|
|
||||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||||
backupGlobals="false"
|
|
||||||
colors="true"
|
|
||||||
bootstrap="tests/bootstrap.php"
|
bootstrap="tests/bootstrap.php"
|
||||||
convertDeprecationsToExceptions="false"
|
colors="true"
|
||||||
>
|
cacheDirectory=".phpunit.cache">
|
||||||
|
|
||||||
<php>
|
<php>
|
||||||
<ini name="display_errors" value="1" />
|
<ini name="display_errors" value="1" />
|
||||||
<ini name="error_reporting" value="-1" />
|
<ini name="error_reporting" value="-1" />
|
||||||
<server name="APP_ENV" value="test" force="true" />
|
<server name="APP_ENV" value="test" force="true" />
|
||||||
<server name="SHELL_VERBOSITY" value="-1" />
|
<server name="SHELL_VERBOSITY" value="-1" />
|
||||||
<server name="SYMFONY_PHPUNIT_REMOVE" value="" />
|
|
||||||
<server name="SYMFONY_PHPUNIT_VERSION" value="9.5" />
|
|
||||||
</php>
|
</php>
|
||||||
|
|
||||||
<testsuites>
|
<testsuites>
|
||||||
|
|
@ -23,16 +18,9 @@
|
||||||
</testsuite>
|
</testsuite>
|
||||||
</testsuites>
|
</testsuites>
|
||||||
|
|
||||||
<coverage processUncoveredFiles="true">
|
<source>
|
||||||
<include>
|
<include>
|
||||||
<directory suffix=".php">src</directory>
|
<directory suffix=".php">src</directory>
|
||||||
</include>
|
</include>
|
||||||
</coverage>
|
</source>
|
||||||
|
|
||||||
<listeners>
|
|
||||||
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener" />
|
|
||||||
</listeners>
|
|
||||||
|
|
||||||
<extensions>
|
|
||||||
</extensions>
|
|
||||||
</phpunit>
|
</phpunit>
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\Actions;
|
||||||
|
use App\Entity\Organizations;
|
||||||
|
use App\Service\ActionService;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|
||||||
|
#[Route(path: '/actions', name: 'actions_')]
|
||||||
|
class ActionController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private EntityManagerInterface $entityManager,
|
||||||
|
private ActionService $actionService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/organization/{id}/activities-ajax', name: 'app_organization_activities_ajax', methods: ['GET'])]
|
||||||
|
public function fetchActivitiesAjax(Organizations $organization): JsonResponse
|
||||||
|
{
|
||||||
|
$actions = $this->entityManager->getRepository(Actions::class)->findBy(
|
||||||
|
['Organization' => $organization],
|
||||||
|
['date' => 'DESC'],
|
||||||
|
15
|
||||||
|
);
|
||||||
|
$formattedActivities = $this->actionService->formatActivities($actions);
|
||||||
|
|
||||||
|
return new JsonResponse($formattedActivities);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,18 +5,20 @@ namespace App\Controller;
|
||||||
use App\Entity\Apps;
|
use App\Entity\Apps;
|
||||||
use App\Entity\Organizations;
|
use App\Entity\Organizations;
|
||||||
use App\Service\ActionService;
|
use App\Service\ActionService;
|
||||||
|
use App\Service\LoggerService;
|
||||||
use App\Service\UserService;
|
use App\Service\UserService;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
|
||||||
|
|
||||||
#[Route(path: '/application', name: 'application_')]
|
#[Route(path: '/application', name: 'application_')]
|
||||||
|
|
||||||
class ApplicationController extends AbstractController
|
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());
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
$application = $this->entityManager->getRepository(Apps::class)->find($id);
|
$application = $this->entityManager->getRepository(Apps::class)->find($id);
|
||||||
if (!$application) {
|
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');
|
return $this->redirectToRoute('application_index');
|
||||||
}
|
}
|
||||||
$applicationData = [
|
$applicationData = [
|
||||||
|
|
@ -50,12 +56,26 @@ class ApplicationController extends AbstractController
|
||||||
|
|
||||||
|
|
||||||
if ($request->isMethod('POST')) {
|
if ($request->isMethod('POST')) {
|
||||||
|
try{
|
||||||
$data = $request->request->all();
|
$data = $request->request->all();
|
||||||
$application->setName($data['name']);
|
$application->setName($data['name']);
|
||||||
$application->setDescription($data['description']);
|
$application->setDescription($data['description']);
|
||||||
$application->setDescriptionSmall($data['descriptionSmall']);
|
$application->setDescriptionSmall($data['descriptionSmall']);
|
||||||
$this->entityManager->persist($application);
|
$this->entityManager->persist($application);
|
||||||
$this->actionService->createAction("Modification de l'application ", $actingUser, null, $application->getId());
|
$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');
|
return $this->redirectToRoute('application_index');
|
||||||
}
|
}
|
||||||
|
|
@ -66,36 +86,82 @@ class ApplicationController extends AbstractController
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route(path: '/authorize/{id}', name: 'authorize', methods: ['POST'])]
|
#[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');
|
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
||||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
try{
|
||||||
$application = $this->entityManager->getRepository(Apps::class)->find($id);
|
$application = $this->entityManager->getRepository(Apps::class)->find($id);
|
||||||
if (!$application) {
|
if (!$application) {
|
||||||
|
$this->loggerService->logEntityNotFound('Application', [
|
||||||
|
'applicationId' => $id,
|
||||||
|
'message' => "Application not found for authorization."
|
||||||
|
], $actingUser->getId());
|
||||||
throw $this->createNotFoundException("L'application n'existe pas.");
|
throw $this->createNotFoundException("L'application n'existe pas.");
|
||||||
}
|
}
|
||||||
$orgId = $request->get('organizationId');
|
$orgId = $request->get('organizationId');
|
||||||
|
|
||||||
$organization = $this->entityManager->getRepository(Organizations::Class)->find($orgId);
|
$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);
|
$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());
|
$this->actionService->createAction("Authorization d'accès", $actingUser, $organization, $application->getName());
|
||||||
return new Response('', Response::HTTP_OK);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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');
|
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
||||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
$application = $this->entityManager->getRepository(Apps::class)->find($id);
|
$application = $this->entityManager->getRepository(Apps::class)->find($id);
|
||||||
if (!$application) {
|
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.");
|
throw $this->createNotFoundException("L'application n'existe pas.");
|
||||||
}
|
}
|
||||||
$orgId = $request->get('organizationId');
|
$orgId = $request->get('organizationId');
|
||||||
$organization = $this->entityManager->getRepository(Organizations::Class)->find($orgId);
|
$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);
|
$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());
|
$this->actionService->createAction("Authorization retirer", $actingUser, $organization, $application->getName());
|
||||||
|
|
||||||
return new Response('', Response::HTTP_OK);
|
return new Response('', Response::HTTP_OK);
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ class NotificationController extends AbstractController
|
||||||
#[Route(path: '/', name: 'index', methods: ['GET'])]
|
#[Route(path: '/', name: 'index', methods: ['GET'])]
|
||||||
public function index(): JsonResponse
|
public function index(): JsonResponse
|
||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
|
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
||||||
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
|
||||||
$notifications = $this->notificationRepository->findRecentByUser($user, 50);
|
$notifications = $this->notificationRepository->findRecentByUser($user, 50);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
use App\Service\AccessTokenService;
|
use App\Service\AccessTokenService;
|
||||||
|
use App\Service\LoggerService;
|
||||||
|
use App\Service\UserService;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Psr\Log\LogLevel;
|
use Psr\Log\LogLevel;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
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'])]
|
#[Route('/oauth2/userinfo', name: 'userinfo', methods: ['GET'])]
|
||||||
public function userinfo(Request $request): JsonResponse
|
public function userinfo(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$user = $this->getUser();
|
$user = $this->getUser();
|
||||||
// dd($user);
|
|
||||||
if (!$user) {
|
if (!$user) {
|
||||||
|
$this->loggerService->logAccessDenied($user->getId());
|
||||||
return new JsonResponse(['error' => 'Unauthorized'], 401);
|
return new JsonResponse(['error' => 'Unauthorized'], 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->loggerService->logUserAction($user->getId(), $user->getId(), 'Accessed userinfo endpoint');
|
||||||
return new JsonResponse([
|
return new JsonResponse([
|
||||||
'id' => $user->getId(),
|
'id' => $user->getId(),
|
||||||
'name' => $user->getName(),
|
'name' => $user->getName(),
|
||||||
|
|
@ -66,7 +71,7 @@ class OAuth2Controller extends AbstractController
|
||||||
if (!$userIdentifier) {
|
if (!$userIdentifier) {
|
||||||
return new JsonResponse(["ERROR" => "User identifier is required"], Response::HTTP_BAD_REQUEST);
|
return new JsonResponse(["ERROR" => "User identifier is required"], Response::HTTP_BAD_REQUEST);
|
||||||
}
|
}
|
||||||
$accessTokenService->revokeTokens($userIdentifier);
|
$accessTokenService->revokeUserTokens($userIdentifier);
|
||||||
$logger->info("Revoke tokens successfully");
|
$logger->info("Revoke tokens successfully");
|
||||||
|
|
||||||
return new JsonResponse(["SUCCESS" => "Tokens revoked successfully"], Response::HTTP_OK);
|
return new JsonResponse(["SUCCESS" => "Tokens revoked successfully"], Response::HTTP_OK);
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,20 @@ use App\Form\OrganizationForm;
|
||||||
use App\Repository\OrganizationsRepository;
|
use App\Repository\OrganizationsRepository;
|
||||||
use App\Service\ActionService;
|
use App\Service\ActionService;
|
||||||
use App\Service\AwsService;
|
use App\Service\AwsService;
|
||||||
|
use App\Service\LoggerService;
|
||||||
use App\Service\OrganizationsService;
|
use App\Service\OrganizationsService;
|
||||||
use App\Service\UserOrganizationService;
|
use App\Service\UserOrganizationService;
|
||||||
use App\Service\UserService;
|
use App\Service\UserService;
|
||||||
|
use Doctrine\DBAL\Exception\NonUniqueFieldNameException;
|
||||||
|
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Doctrine\ORM\NonUniqueResultException;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
use App\Entity\Organizations;
|
use App\Entity\Organizations;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
@ -37,7 +43,7 @@ class OrganizationController extends AbstractController
|
||||||
private readonly ActionService $actionService,
|
private readonly ActionService $actionService,
|
||||||
private readonly UserOrganizationService $userOrganizationService,
|
private readonly UserOrganizationService $userOrganizationService,
|
||||||
private readonly OrganizationsRepository $organizationsRepository,
|
private readonly OrganizationsRepository $organizationsRepository,
|
||||||
private readonly AwsService $awsService)
|
private readonly AwsService $awsService, private readonly LoggerService $loggerService, private readonly LoggerInterface $logger)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,45 +51,30 @@ class OrganizationController extends AbstractController
|
||||||
public function index(): Response
|
public function index(): Response
|
||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||||
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
if($this->userService->hasAccessTo($actingUser, true)){
|
||||||
if ($this->isGranted("ROLE_SUPER_ADMIN")) {
|
$orgCount = $this->organizationsRepository->count(['isDeleted' => false]);
|
||||||
$organizations = $this->organizationsRepository->findBy(['isDeleted' => false]);
|
if(!$this->isGranted("ROLE_SUPER_ADMIN")){
|
||||||
|
$userUO = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['users' => $actingUser, 'isActive' => true]);
|
||||||
|
$uoAdmin = 0;
|
||||||
} else {
|
foreach($userUO as $u){
|
||||||
//get all the UO of the user
|
if($this->userService->isAdminOfOrganization($u->getOrganization())){
|
||||||
$uos = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['users' => $user]);
|
$uoAdmin++;
|
||||||
$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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (count($organizations) === 1 && $organizations[0]->isActive() === true) {
|
if($uoAdmin === 1){
|
||||||
return $this->redirectToRoute('organization_show', ['id' => $organizations[0]->getId()]);
|
return $this->redirectToRoute('organization_show', ['id' => $userUO[0]->getOrganization()->getId()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
// 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', [
|
return $this->render('organization/index.html.twig', [
|
||||||
'organizationsData' => $organizationsData,
|
'hasOrganizations' => $orgCount > 0
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
$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
|
public function new(Request $request): Response
|
||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
||||||
|
|
@ -100,10 +91,14 @@ class OrganizationController extends AbstractController
|
||||||
try {
|
try {
|
||||||
$this->entityManager->persist($organization);
|
$this->entityManager->persist($organization);
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
|
$this->loggerService->logOrganizationInformation($organization->getId(), $actingUser->getId(), "Organization Created");
|
||||||
|
$this->loggerService->logSuperAdmin($actingUser->getId(), $actingUser->getId(), "Organization Created", $organization->getId());
|
||||||
$this->actionService->createAction("Create Organization", $actingUser, $organization, $organization->getName());
|
$this->actionService->createAction("Create Organization", $actingUser, $organization, $organization->getName());
|
||||||
|
$this->addFlash('success', 'Organisation crée avec succès.');
|
||||||
return $this->redirectToRoute('organization_index');
|
return $this->redirectToRoute('organization_index');
|
||||||
} catch (Exception $e) {
|
} 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', [
|
return $this->render('organization/new.html.twig', [
|
||||||
|
|
@ -124,21 +119,34 @@ class OrganizationController extends AbstractController
|
||||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
$organization = $this->organizationsRepository->find($id);
|
$organization = $this->organizationsRepository->find($id);
|
||||||
if (!$organization) {
|
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');
|
return $this->redirectToRoute('organization_index');
|
||||||
}
|
}
|
||||||
if (!$this->isGranted("ROLE_SUPER_ADMIN")) {
|
if (!$this->isGranted("ROLE_SUPER_ADMIN")) {
|
||||||
//check if the user is admin of the organization
|
//check if the user is admin of the organization
|
||||||
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
$uo = $this->entityManager->getRepository(UsersOrganizations::class)->findOneBy(['users' => $actingUser, 'organization' => $organization]);
|
||||||
$uo = $this->entityManager->getRepository(UsersOrganizations::class)->findOneBy(['users' => $user, 'organization' => $organization]);
|
|
||||||
if (!$uo) {
|
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');
|
return $this->redirectToRoute('organization_index');
|
||||||
}
|
}
|
||||||
$roleAdmin = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN']);
|
$roleAdmin = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN']);
|
||||||
$uoaAdmin = $this->entityManager->getRepository(UserOrganizatonApp::class)->findOneBy(['userOrganization' => $uo, 'role' => $roleAdmin]);
|
$uoaAdmin = $this->entityManager->getRepository(UserOrganizatonApp::class)->findOneBy(['userOrganization' => $uo, 'role' => $roleAdmin]);
|
||||||
if (!$uoaAdmin) {
|
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');
|
return $this->redirectToRoute('organization_index');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -152,10 +160,16 @@ class OrganizationController extends AbstractController
|
||||||
try {
|
try {
|
||||||
$this->entityManager->persist($organization);
|
$this->entityManager->persist($organization);
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
|
$this->loggerService->logOrganizationInformation($organization->getId(), $actingUser->getId(), "Organization Edited");
|
||||||
|
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->actionService->createAction("Edit Organization", $actingUser, $organization, $organization->getName());
|
||||||
|
$this->addFlash('success', 'Organisation modifiée avec succès.');
|
||||||
return $this->redirectToRoute('organization_index');
|
return $this->redirectToRoute('organization_index');
|
||||||
}catch (Exception $e) {
|
}catch (Exception $e) {
|
||||||
$this->addFlash('error', 'Error editing organization: ' . $e->getMessage());
|
$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', [
|
return $this->render('organization/edit.html.twig', [
|
||||||
|
|
@ -171,43 +185,31 @@ class OrganizationController extends AbstractController
|
||||||
$organization = $this->organizationsRepository->find($id);
|
$organization = $this->organizationsRepository->find($id);
|
||||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
if (!$organization) {
|
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');
|
return $this->redirectToRoute('organization_index');
|
||||||
}
|
}
|
||||||
//check if the user is admin of the organization
|
//check if the user is admin of the organization
|
||||||
if (!$this->isGranted("ROLE_SUPER_ADMIN") && !$this->userService->isAdminOfOrganization($organization)) {
|
if (!$this->userService->isAdminOfOrganization($organization) && !$this->isGranted("ROLE_SUPER_ADMIN")) {
|
||||||
$this->createNotFoundException(self::NOT_FOUND);
|
$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
|
$allApps = $this->entityManager->getRepository(Apps::class)->findAll(); // appsAll
|
||||||
$orgApps = $organization->getApps()->toArray(); // apps
|
$orgApps = $organization->getApps()->toArray(); // apps
|
||||||
|
|
||||||
$apps = $this->organizationsService->appsAccess($allApps, $orgApps);
|
$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);
|
$activities = $this->actionService->formatActivities($actions);
|
||||||
|
|
||||||
$this->actionService->createAction("View Organization", $actingUser, $organization, $organization->getName());
|
$this->actionService->createAction("View Organization", $actingUser, $organization, $organization->getName());
|
||||||
return $this->render('organization/show.html.twig', [
|
return $this->render('organization/show.html.twig', [
|
||||||
'organization' => $organization,
|
'organization' => $organization,
|
||||||
'newUsers' => $newUsers,
|
|
||||||
'adminUsers' => $adminUsers,
|
|
||||||
'users' => $users,
|
|
||||||
'applications' => $apps,
|
'applications' => $apps,
|
||||||
'activities' => $activities,
|
'activities' => $activities,
|
||||||
]);
|
]);
|
||||||
|
|
@ -220,8 +222,14 @@ class OrganizationController extends AbstractController
|
||||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
$organization = $this->organizationsRepository->find($id);
|
$organization = $this->organizationsRepository->find($id);
|
||||||
if (!$organization) {
|
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);
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
$organization->setIsActive(false);
|
$organization->setIsActive(false);
|
||||||
$organization->setIsDeleted(true);
|
$organization->setIsDeleted(true);
|
||||||
// Deactivate all associated UsersOrganizations
|
// Deactivate all associated UsersOrganizations
|
||||||
|
|
@ -229,6 +237,17 @@ class OrganizationController extends AbstractController
|
||||||
|
|
||||||
$this->entityManager->persist($organization);
|
$this->entityManager->persist($organization);
|
||||||
$this->actionService->createAction("Delete Organization", $actingUser, $organization, $organization->getName());
|
$this->actionService->createAction("Delete Organization", $actingUser, $organization, $organization->getName());
|
||||||
|
$this->entityManager->flush();
|
||||||
|
$this->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.');
|
||||||
|
}
|
||||||
|
|
||||||
return $this->redirectToRoute('organization_index');
|
return $this->redirectToRoute('organization_index');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -239,12 +258,20 @@ class OrganizationController extends AbstractController
|
||||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
$organization = $this->organizationsRepository->find($id);
|
$organization = $this->organizationsRepository->find($id);
|
||||||
if (!$organization) {
|
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);
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
$organization->setIsActive(false);
|
$organization->setIsActive(false);
|
||||||
// $this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, null, $organization);
|
// $this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, null, $organization);
|
||||||
$this->entityManager->persist($organization);
|
$this->entityManager->persist($organization);
|
||||||
$this->actionService->createAction("Deactivate Organization", $actingUser, $organization, $organization->getName());
|
$this->actionService->createAction("Deactivate Organization", $actingUser, $organization, $organization->getName());
|
||||||
|
$this->loggerService->logSuperAdmin($actingUser->getId(), $actingUser->getId(),'Organization deactivated', $organization->getId());
|
||||||
|
$this->addFlash('success', 'Organisation désactivé avec succès.');
|
||||||
return $this->redirectToRoute('organization_index');
|
return $this->redirectToRoute('organization_index');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -255,16 +282,24 @@ class OrganizationController extends AbstractController
|
||||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
$organization = $this->organizationsRepository->find($id);
|
$organization = $this->organizationsRepository->find($id);
|
||||||
if (!$organization) {
|
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);
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
}
|
}
|
||||||
$organization->setIsActive(true);
|
$organization->setIsActive(true);
|
||||||
$this->entityManager->persist($organization);
|
$this->entityManager->persist($organization);
|
||||||
|
$this->loggerService->logOrganizationInformation($organization->getId(), $actingUser->getId(),'Organization Activated');
|
||||||
|
$this->loggerService->logSuperAdmin($actingUser->getId(), $actingUser->getId(),'Organization Activated', $organization->getId());
|
||||||
$this->actionService->createAction("Activate Organization", $actingUser, $organization, $organization->getName());
|
$this->actionService->createAction("Activate Organization", $actingUser, $organization, $organization->getName());
|
||||||
|
$this->addFlash('success', 'Organisation activée avec succès.');
|
||||||
return $this->redirectToRoute('organization_index');
|
return $this->redirectToRoute('organization_index');
|
||||||
}
|
}
|
||||||
|
|
||||||
// API endpoint to fetch organization data for Tabulator
|
// 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
|
public function data(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||||
|
|
@ -276,8 +311,6 @@ class OrganizationController extends AbstractController
|
||||||
$filters = $request->query->all('filter');
|
$filters = $request->query->all('filter');
|
||||||
|
|
||||||
|
|
||||||
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
|
||||||
|
|
||||||
$qb = $this->organizationsRepository->createQueryBuilder('o')
|
$qb = $this->organizationsRepository->createQueryBuilder('o')
|
||||||
->where('o.isDeleted = :del')->setParameter('del', false);
|
->where('o.isDeleted = :del')->setParameter('del', false);
|
||||||
|
|
||||||
|
|
@ -289,6 +322,17 @@ class OrganizationController extends AbstractController
|
||||||
$qb->andWhere('o.email LIKE :email')
|
$qb->andWhere('o.email LIKE :email')
|
||||||
->setParameter('email', '%' . $filters['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
|
// Count total
|
||||||
$countQb = clone $qb;
|
$countQb = clone $qb;
|
||||||
|
|
@ -311,7 +355,6 @@ class OrganizationController extends AbstractController
|
||||||
];
|
];
|
||||||
}, $rows);
|
}, $rows);
|
||||||
|
|
||||||
// Tabulator expects: data, last_page (total pages), or total row count depending on config
|
|
||||||
$lastPage = (int)ceil($total / $size);
|
$lastPage = (int)ceil($total / $size);
|
||||||
|
|
||||||
return $this->json([
|
return $this->json([
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ namespace App\Controller;
|
||||||
use App\Repository\UserRepository;
|
use App\Repository\UserRepository;
|
||||||
use App\Repository\UsersOrganizationsRepository;
|
use App\Repository\UsersOrganizationsRepository;
|
||||||
use App\Service\AccessTokenService;
|
use App\Service\AccessTokenService;
|
||||||
|
use App\Service\LoggerService;
|
||||||
use App\Service\OrganizationsService;
|
use App\Service\OrganizationsService;
|
||||||
use App\Service\UserService;
|
use App\Service\UserService;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
|
@ -30,7 +31,7 @@ class SecurityController extends AbstractController
|
||||||
private readonly UsersOrganizationsRepository $uoRepository,
|
private readonly UsersOrganizationsRepository $uoRepository,
|
||||||
private readonly LoggerInterface $logger,
|
private readonly LoggerInterface $logger,
|
||||||
private readonly EntityManagerInterface $entityManager,
|
private readonly EntityManagerInterface $entityManager,
|
||||||
private readonly OrganizationsService $organizationsService)
|
private readonly OrganizationsService $organizationsService, private readonly LoggerService $loggerService, private readonly Security $security)
|
||||||
{
|
{
|
||||||
$this->cguUserService = $cguUserService;
|
$this->cguUserService = $cguUserService;
|
||||||
}
|
}
|
||||||
|
|
@ -51,10 +52,12 @@ class SecurityController extends AbstractController
|
||||||
public function ssoLogout(RequestStack $stack, LoggerInterface $logger, AccessTokenService $accessTokenService, Security $security): Response
|
public function ssoLogout(RequestStack $stack, LoggerInterface $logger, AccessTokenService $accessTokenService, Security $security): Response
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
$user = $this->userService->getUserByIdentifier($this->security->getUser()->getUserIdentifier());
|
||||||
|
$id = $user->getId();
|
||||||
if ($stack->getSession()->invalidate()) {
|
if ($stack->getSession()->invalidate()) {
|
||||||
$accessTokenService->revokeTokens($security->getUser()->getUserIdentifier());
|
$accessTokenService->revokeUserTokens($security->getUser()->getUserIdentifier());
|
||||||
$security->logout(false);
|
$security->logout(false);
|
||||||
$logger->info("Logout successfully");
|
$this->loggerService->logUserConnection('User logged out', ['user_id' => $id]);
|
||||||
return $this->redirect('/');
|
return $this->redirect('/');
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
|
|
@ -69,6 +72,7 @@ class SecurityController extends AbstractController
|
||||||
if ($request->isMethod('POST')) {
|
if ($request->isMethod('POST')) {
|
||||||
if (!$request->request->has('decline')) {
|
if (!$request->request->has('decline')) {
|
||||||
$this->cguUserService->acceptLatestCgu($this->getUser());
|
$this->cguUserService->acceptLatestCgu($this->getUser());
|
||||||
|
$this->loggerService->logCGUAcceptance($this->getUser()->getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->redirectToRoute('oauth2_authorize', $request->query->all());
|
return $this->redirectToRoute('oauth2_authorize', $request->query->all());
|
||||||
|
|
@ -83,12 +87,24 @@ class SecurityController extends AbstractController
|
||||||
$error = $request->get('error');
|
$error = $request->get('error');
|
||||||
$user = $this->userRepository->find($id);
|
$user = $this->userRepository->find($id);
|
||||||
if (!$user) {
|
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);
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
}
|
}
|
||||||
$token = $request->get('token');
|
$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.';
|
$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', [
|
return $this->render('security/password_setup.html.twig', [
|
||||||
'id' => $id,
|
'id' => $id,
|
||||||
|
|
@ -98,17 +114,19 @@ class SecurityController extends AbstractController
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/password_reset/{id}', name: 'password_reset', methods: ['POST'])]
|
#[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);
|
$user = $this->userRepository->find($id);
|
||||||
if (!$user) {
|
if (!$user) {
|
||||||
|
$this->loggerService->logEntityNotFound('User', ['user_id' => $id,
|
||||||
|
'message' => 'user not found for password reset'], $id);
|
||||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
}
|
}
|
||||||
$newPassword = $_POST['_password'] ?? null;
|
$newPassword = $_POST['_password'] ?? null;
|
||||||
$confirmPassword = $_POST['_passwordConfirm'] ?? null;
|
$confirmPassword = $_POST['_passwordConfirm'] ?? null;
|
||||||
if ($newPassword !== $confirmPassword) {
|
if ($newPassword !== $confirmPassword) {
|
||||||
$error = 'Les mots de passe ne correspondent pas. Veuillez réessayer.';
|
$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', [
|
return $this->redirectToRoute('password_setup', [
|
||||||
'id' => $id,
|
'id' => $id,
|
||||||
'token' => $_POST['token'] ?? '',
|
'token' => $_POST['token'] ?? '',
|
||||||
|
|
@ -116,10 +134,11 @@ class SecurityController extends AbstractController
|
||||||
}
|
}
|
||||||
if (!$this->userService->isPasswordStrong($newPassword)) {
|
if (!$this->userService->isPasswordStrong($newPassword)) {
|
||||||
$error = 'Le mot de passe ne respecte pas les critères de sécurité. Veuillez en choisir un autre.';
|
$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.");
|
$this->loggerService->logUserAction($id, $id, ' provided a weak password during password reset.');
|
||||||
return $this->redirectToRoute('password_setup', ['id' => $id, 'token' => $_POST['token'] ?? '', 'error' => $error]);
|
return $this->redirectToRoute('password_setup', ['id' => $id, 'token' => $_POST['token'] ?? '', 'error' => $error]);
|
||||||
}
|
}
|
||||||
$this->userService->updateUserPassword($user, $newPassword);
|
$this->userService->updateUserPassword($user, $newPassword);
|
||||||
|
$this->loggerService->logUserAction($id, $id, 'Password reset user successfully.');
|
||||||
$orgId = $this->userService->getOrgFromToken($_POST['token']);
|
$orgId = $this->userService->getOrgFromToken($_POST['token']);
|
||||||
$uo = $this->uoRepository->findOneBy(['users' => $user, 'organization' => $orgId]);
|
$uo = $this->uoRepository->findOneBy(['users' => $user, 'organization' => $orgId]);
|
||||||
if ($uo) {
|
if ($uo) {
|
||||||
|
|
@ -127,7 +146,11 @@ class SecurityController extends AbstractController
|
||||||
$uo->setIsActive(true);
|
$uo->setIsActive(true);
|
||||||
$this->entityManager->persist($uo);
|
$this->entityManager->persist($uo);
|
||||||
$this->entityManager->flush();
|
$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");
|
$this->organizationsService->notifyOrganizationAdmins($data, "USER_ACCEPTED");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,26 +13,25 @@ use App\Repository\OrganizationsRepository;
|
||||||
use App\Repository\RolesRepository;
|
use App\Repository\RolesRepository;
|
||||||
use App\Repository\UserRepository;
|
use App\Repository\UserRepository;
|
||||||
use App\Repository\UsersOrganizationsRepository;
|
use App\Repository\UsersOrganizationsRepository;
|
||||||
|
use App\Service\AccessTokenService;
|
||||||
use App\Service\ActionService;
|
use App\Service\ActionService;
|
||||||
use App\Service\AwsService;
|
use App\Service\AwsService;
|
||||||
use App\Service\EmailService;
|
use App\Service\EmailService;
|
||||||
|
use App\Service\LoggerService;
|
||||||
use App\Service\OrganizationsService;
|
use App\Service\OrganizationsService;
|
||||||
use App\Service\UserOrganizationAppService;
|
use App\Service\UserOrganizationAppService;
|
||||||
use App\Service\UserOrganizationService;
|
use App\Service\UserOrganizationService;
|
||||||
use App\Service\UserService;
|
use App\Service\UserService;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use mysql_xdevapi\Exception;
|
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
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\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||||
|
|
||||||
#[Route(path: '/user', name: 'user_')]
|
#[Route(path: '/user', name: 'user_')]
|
||||||
class UserController extends AbstractController
|
class UserController extends AbstractController
|
||||||
|
|
@ -51,16 +50,14 @@ class UserController extends AbstractController
|
||||||
private readonly OrganizationsRepository $organizationRepository,
|
private readonly OrganizationsRepository $organizationRepository,
|
||||||
private readonly LoggerInterface $userManagementLogger,
|
private readonly LoggerInterface $userManagementLogger,
|
||||||
private readonly LoggerInterface $organizationManagementLogger,
|
private readonly LoggerInterface $organizationManagementLogger,
|
||||||
private readonly LoggerInterface $accessControlLogger,
|
|
||||||
private readonly LoggerInterface $EmailNotificationLogger,
|
|
||||||
private readonly LoggerInterface $adminActionsLogger,
|
|
||||||
private readonly LoggerInterface $errorLogger,
|
private readonly LoggerInterface $errorLogger,
|
||||||
private readonly LoggerInterface $SecurityLogger,
|
private readonly LoggerInterface $securityLogger,
|
||||||
|
private readonly LoggerService $loggerService,
|
||||||
private readonly EmailService $emailService,
|
private readonly EmailService $emailService,
|
||||||
private readonly AwsService $awsService,
|
private readonly AwsService $awsService,
|
||||||
private readonly OrganizationsService $organizationsService,
|
private readonly OrganizationsService $organizationsService,
|
||||||
private readonly AppsRepository $appsRepository,
|
private readonly AppsRepository $appsRepository,
|
||||||
private readonly RolesRepository $rolesRepository,
|
private readonly RolesRepository $rolesRepository, private readonly AccessTokenService $accessTokenService,
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
@ -76,13 +73,20 @@ class UserController extends AbstractController
|
||||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
|
||||||
// Vérification des droits d'accès supplémentaires
|
// 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
|
// Chargement de l'utilisateur cible à afficher
|
||||||
$user = $this->userRepository->find($id);
|
$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 {
|
try {
|
||||||
// Paramètre optionnel de contexte organisationnel
|
// Paramètre optionnel de contexte organisationnel
|
||||||
$orgId = $request->query->get('organizationId');
|
$orgId = $request->query->get('organizationId');
|
||||||
|
|
@ -105,6 +109,11 @@ class UserController extends AbstractController
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!$uoList) {
|
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);
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,8 +127,15 @@ class UserController extends AbstractController
|
||||||
'users' => $user,
|
'users' => $user,
|
||||||
'isActive' => true,
|
'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
|
// Charger les liens UserOrganizationApp (UOA) actifs pour les UO trouvées
|
||||||
// Load user-organization-app roles (can be empty)
|
// Load user-organization-app roles (can be empty)
|
||||||
$uoa = $this->entityManager
|
$uoa = $this->entityManager
|
||||||
|
|
@ -128,7 +144,6 @@ class UserController extends AbstractController
|
||||||
'userOrganization' => $uoList,
|
'userOrganization' => $uoList,
|
||||||
'isActive' => true,
|
'isActive' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Group UOA by app and ensure every app has a group
|
// Group UOA by app and ensure every app has a group
|
||||||
$data['uoas'] = $this->userOrganizationAppService
|
$data['uoas'] = $this->userOrganizationAppService
|
||||||
->groupUserOrganizationAppsByApplication(
|
->groupUserOrganizationAppsByApplication(
|
||||||
|
|
@ -150,12 +165,13 @@ class UserController extends AbstractController
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
// Calcul du flag de modification : utilisateur admin ET exactement 1 UO
|
// 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) {
|
} catch (\Exception $e) {
|
||||||
// En cas d'erreur, désactiver l'édition et logger l'exception
|
|
||||||
$canEdit = false;
|
|
||||||
$this->errorLogger->error($e->getMessage());
|
$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', [
|
return $this->render('user/show.html.twig', [
|
||||||
'user' => $user,
|
'user' => $user,
|
||||||
|
|
@ -171,64 +187,61 @@ class UserController extends AbstractController
|
||||||
public function edit(int $id, Request $request): Response
|
public function edit(int $id, Request $request): Response
|
||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted('ROLE_USER');
|
$this->denyAccessUnlessGranted('ROLE_USER');
|
||||||
try{
|
|
||||||
|
|
||||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
if ($this->userService->hasAccessTo($actingUser)) {
|
|
||||||
$user = $this->userRepository->find($id);
|
$user = $this->userRepository->find($id);
|
||||||
if (!$user) {
|
if (!$user) {
|
||||||
$this->userManagementLogger->notice('User not found for edit', [
|
$this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getId());
|
||||||
'target_user_id' => $user->getId(),
|
$this->addFlash('error', "L'utilisateur demandé n'existe pas.");
|
||||||
'acting_user_id' => $actingUser->getId(),
|
|
||||||
'ip' => $request->getClientIp(),
|
|
||||||
'timestamp' => (new \DateTimeImmutable('now'))->format(DATE_ATOM),
|
|
||||||
]);
|
|
||||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
if ($this->userService->hasAccessTo($user)) {
|
||||||
|
|
||||||
$form = $this->createForm(UserForm::class, $user);
|
$form = $this->createForm(UserForm::class, $user);
|
||||||
$form->handleRequest($request);
|
$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()) {
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
// Handle user edit
|
// Handle user edit
|
||||||
$picture = $form->get('pictureUrl')->getData();
|
$picture = $form->get('pictureUrl')->getData();
|
||||||
$this->userService->formatNewUserData($user, $picture);
|
$this->userService->formatUserData($user, $picture);
|
||||||
$user->setModifiedAt(new \DateTimeImmutable('now'));
|
$user->setModifiedAt(new \DateTimeImmutable('now'));
|
||||||
|
|
||||||
$this->entityManager->persist($user);
|
$this->entityManager->persist($user);
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
|
|
||||||
//log and action
|
//log and action
|
||||||
$this->userManagementLogger->notice('User information edited', [
|
$this->loggerService->logUserAction($user->getId(), $actingUser->getId(), 'User information edited');
|
||||||
'target_user_id' => $user->getId(),
|
$orgId = $request->get('organizationId');
|
||||||
'acting_user_id' => $actingUser->getId(),
|
if ($orgId) {
|
||||||
'organization_id' => $request->get('organizationId'),
|
$org = $this->organizationRepository->find($orgId);
|
||||||
'ip' => $request->getClientIp(),
|
|
||||||
'timestamp' => (new \DateTimeImmutable('now'))->format(DATE_ATOM),
|
|
||||||
]);
|
|
||||||
if ($request->get('organizationId')) {
|
|
||||||
$org = $this->organizationRepository->find($request->get('organizationId'));
|
|
||||||
if ($org) {
|
if ($org) {
|
||||||
$this->actionService->createAction("Edit user information", $actingUser, $org, $user->getUserIdentifier());
|
$this->actionService->createAction("Edit user information", $actingUser, $org, $user->getUserIdentifier());
|
||||||
$this->organizationManagementLogger->info('User edited within organization context', [
|
$this->loggerService->logUserAction($user->getId(), $actingUser->getId(), 'User information edited');
|
||||||
'target_user_id' => $user->getId(),
|
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
|
||||||
'organization_id' => $org->getId(),
|
$this->loggerService->logSuperAdmin(
|
||||||
'acting_user' => $actingUser->getUserIdentifier(),
|
$user->getId(),
|
||||||
'ip' => $request->getClientIp(),
|
$actingUser->getId(),
|
||||||
]);
|
"Super Admin accessed user edit page",
|
||||||
return $this->redirectToRoute('user_show', ['id' => $user->getId(), 'organizationId' => $request->get('organizationId')]);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
$this->addFlash('success', 'Information modifié avec success.');
|
||||||
|
return $this->redirectToRoute('user_show', ['id' => $user->getId(), 'organizationId' => $orgId]);
|
||||||
|
}
|
||||||
|
$this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getId());
|
||||||
|
$this->addFlash('error', "L'organisation n'existe pas.");
|
||||||
|
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());
|
$this->actionService->createAction("Edit user information", $actingUser, null, $user->getUserIdentifier());
|
||||||
return $this->redirectToRoute('user_show', ['id' => $user->getId()]);
|
return $this->redirectToRoute('user_show', ['id' => $user->getId()]);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return $this->render('user/edit.html.twig', [
|
return $this->render('user/edit.html.twig', [
|
||||||
'user' => $user,
|
'user' => $user,
|
||||||
|
|
@ -236,15 +249,16 @@ class UserController extends AbstractController
|
||||||
'organizationId' => $request->get('organizationId')
|
'organizationId' => $request->get('organizationId')
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
$this->loggerService->logAccessDenied($actingUser->getId());
|
||||||
|
$this->addFlash('error', "Accès non autorisé.");
|
||||||
|
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
|
$this->addFlash('error', 'Une erreur est survenue lors de la modification des informations utilisateur.');
|
||||||
$this->errorLogger->critical($e->getMessage());
|
$this->errorLogger->critical($e->getMessage());
|
||||||
}
|
}
|
||||||
$this->SecurityLogger->warning('Access denied on user edit', [
|
// Default deny access. shouldn't reach here normally.
|
||||||
'target_user_id' => $id,
|
|
||||||
'acting_user' => $actingUser?->getId(),
|
|
||||||
'ip' => $request->getClientIp(),
|
|
||||||
]);
|
|
||||||
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/new', name: 'new', methods: ['GET', 'POST'])]
|
#[Route('/new', name: 'new', methods: ['GET', 'POST'])]
|
||||||
|
|
@ -253,70 +267,128 @@ class UserController extends AbstractController
|
||||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||||
try {
|
try {
|
||||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
if ($this->userService->hasAccessTo($actingUser)) {
|
if (!$this->userService->hasAccessTo($actingUser)) {
|
||||||
|
$this->loggerService->logAccessDenied($actingUser->getId());
|
||||||
|
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||||
|
}
|
||||||
|
|
||||||
$user = new User();
|
$user = new User();
|
||||||
$form = $this->createForm(UserForm::class, $user);
|
$form = $this->createForm(UserForm::class, $user);
|
||||||
$form->handleRequest($request);
|
$form->handleRequest($request);
|
||||||
|
|
||||||
$orgId = $request->get('organizationId');
|
$orgId = $request->get('organizationId');
|
||||||
if ($orgId) {
|
if ($orgId) {
|
||||||
$org = $this->organizationRepository->find($orgId) ?? throw new NotFoundHttpException(sprintf('%s not found', $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()) {
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
$existingUser = $this->userRepository->findOneBy(['email' => $user->getEmail()]);
|
$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());
|
// Case : User exists + has organization context
|
||||||
$this->logger->notice("User added to organization " . $org->getName());
|
if ($existingUser && $org) {
|
||||||
$this->emailService->sendExistingUserNotificationEmail($existingUser, $org);
|
$this->userService->addExistingUserToOrganization(
|
||||||
$this->logger->notice("Existing user notification email sent to " . $existingUser->getUserIdentifier());
|
$existingUser,
|
||||||
$data = ['user' => $existingUser, 'organization' => $org];
|
$org,
|
||||||
$this->organizationsService->notifyOrganizationAdmins($data, 'USER_INVITED');
|
$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]);
|
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,
|
||||||
|
// ]);
|
||||||
|
// }
|
||||||
|
|
||||||
// Handle file upload
|
|
||||||
$picture = $form->get('pictureUrl')->getData();
|
$picture = $form->get('pictureUrl')->getData();
|
||||||
$this->userService->formatNewUserData($user, $picture, true);
|
$this->userService->createNewUser($user, $actingUser, $picture);
|
||||||
|
|
||||||
if ($orgId) {
|
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
|
||||||
$uo = new UsersOrganizations();
|
$this->loggerService->logSuperAdmin(
|
||||||
$uo->setUsers($user);
|
$user->getId(),
|
||||||
$uo->setOrganization($org);
|
$actingUser->getId(),
|
||||||
$uo->setStatut("INVITED");
|
"Super Admin created new user",
|
||||||
$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]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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->redirectToRoute('user_index');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return $this->render('user/new.html.twig', [
|
return $this->render('user/new.html.twig', [
|
||||||
'user' => $user,
|
'user' => $user,
|
||||||
'form' => $form->createView(),
|
'form' => $form->createView(),
|
||||||
'organizationId' => $orgId
|
'organizationId' => $orgId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->logger->error($e->getMessage());
|
$this->errorLogger->critical($e->getMessage());
|
||||||
|
|
||||||
if ($orgId) {
|
if ($orgId) {
|
||||||
|
$this->addFlash('error', 'Une erreur est survenue lors de la création de l\'utilisateur pour l\'organisation .');
|
||||||
return $this->redirectToRoute('organization_show', ['id' => $orgId]);
|
return $this->redirectToRoute('organization_show', ['id' => $orgId]);
|
||||||
}
|
}
|
||||||
|
$this->addFlash('error', 'Une erreur est survenue lors de la création de l\'utilisateur.');
|
||||||
return $this->redirectToRoute('user_index');
|
return $this->redirectToRoute('user_index');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -326,44 +398,98 @@ class UserController extends AbstractController
|
||||||
public function activeStatus(int $id, Request $request): JsonResponse
|
public function activeStatus(int $id, Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||||
|
|
||||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
$status = $request->get('status');
|
||||||
try {
|
try {
|
||||||
if ($this->userService->hasAccessTo($actingUser, true)) {
|
// Access control
|
||||||
|
if (!$this->userService->hasAccessTo($actingUser, true)) {
|
||||||
|
$this->loggerService->logAccessDenied($actingUser->getId());
|
||||||
|
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load target user
|
||||||
$user = $this->userRepository->find($id);
|
$user = $this->userRepository->find($id);
|
||||||
if (!$user) {
|
if (!$user) {
|
||||||
|
$this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getId());
|
||||||
|
|
||||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
}
|
}
|
||||||
$status = $request->get('status');
|
|
||||||
|
// Deactivate
|
||||||
if ($status === 'deactivate') {
|
if ($status === 'deactivate') {
|
||||||
$user->setIsActive(false);
|
$user->setIsActive(false);
|
||||||
|
|
||||||
$this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, $user);
|
$this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, $user);
|
||||||
|
|
||||||
if ($this->userService->isUserConnected($user->getUserIdentifier())) {
|
if ($this->userService->isUserConnected($user->getUserIdentifier())) {
|
||||||
$this->userService->revokeUserTokens($user->getUserIdentifier());
|
$this->accessTokenService->revokeUserTokens($user->getUserIdentifier());
|
||||||
}
|
}
|
||||||
|
|
||||||
$user->setModifiedAt(new \DateTimeImmutable('now'));
|
$user->setModifiedAt(new \DateTimeImmutable('now'));
|
||||||
$this->entityManager->persist($user);
|
$this->entityManager->persist($user);
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
$this->logger->notice("User deactivated " . $user->getUserIdentifier());
|
$this->loggerService->logUserAction($user->getId(), $actingUser->getId(), 'User deactivated');
|
||||||
$this->actionService->createAction("Deactivate user", $actingUser, null, $user->getUserIdentifier());
|
|
||||||
|
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);
|
return new JsonResponse(['status' => 'deactivated'], Response::HTTP_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Activate
|
||||||
if ($status === 'activate') {
|
if ($status === 'activate') {
|
||||||
$user->setIsActive(true);
|
$user->setIsActive(true);
|
||||||
$user->setModifiedAt(new \DateTimeImmutable('now'));
|
$user->setModifiedAt(new \DateTimeImmutable('now'));
|
||||||
$this->logger->notice("User activated " . $user->getUserIdentifier());
|
$this->entityManager->persist($user);
|
||||||
$this->actionService->createAction("Activate user", $actingUser, null, $user->getUserIdentifier());
|
$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);
|
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;
|
||||||
}
|
}
|
||||||
}catch (\Exception $e){
|
|
||||||
$this->logger->error($e->getMessage());
|
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'])]
|
#[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');
|
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
try {
|
try {
|
||||||
|
|
@ -371,15 +497,19 @@ class UserController extends AbstractController
|
||||||
$orgId = $request->get('organizationId');
|
$orgId = $request->get('organizationId');
|
||||||
$org = $this->organizationRepository->find($orgId);
|
$org = $this->organizationRepository->find($orgId);
|
||||||
if (!$org) {
|
if (!$org) {
|
||||||
|
$this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getId());
|
||||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
}
|
}
|
||||||
$user = $this->userRepository->find($id);
|
$user = $this->userRepository->find($id);
|
||||||
if (!$user) {
|
if (!$user) {
|
||||||
|
$this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getId());
|
||||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
}
|
}
|
||||||
$uo = $this->uoRepository->findOneBy(['users' => $user,
|
$uo = $this->uoRepository->findOneBy(['users' => $user,
|
||||||
'organization' => $org]);
|
'organization' => $org]);
|
||||||
if (!$uo) {
|
if (!$uo) {
|
||||||
|
$this->loggerService->logEntityNotFound('UsersOrganization', ['user_id' => $user->getId(),
|
||||||
|
'organization_id' => $org->getId()], $actingUser->getId());
|
||||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
}
|
}
|
||||||
$status = $request->get('status');
|
$status = $request->get('status');
|
||||||
|
|
@ -391,7 +521,7 @@ class UserController extends AbstractController
|
||||||
$data = ['user' => $user,
|
$data = ['user' => $user,
|
||||||
'organization' => $org];
|
'organization' => $org];
|
||||||
$this->organizationsService->notifyOrganizationAdmins($data, "USER_DEACTIVATED");
|
$this->organizationsService->notifyOrganizationAdmins($data, "USER_DEACTIVATED");
|
||||||
$this->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());
|
$this->actionService->createAction("Deactivate user in organization", $actingUser, $org, $org->getName() . " for user " . $user->getUserIdentifier());
|
||||||
return new JsonResponse(['status' => 'deactivated'], Response::HTTP_OK);
|
return new JsonResponse(['status' => 'deactivated'], Response::HTTP_OK);
|
||||||
}
|
}
|
||||||
|
|
@ -399,47 +529,107 @@ class UserController extends AbstractController
|
||||||
$uo->setIsActive(true);
|
$uo->setIsActive(true);
|
||||||
$this->entityManager->persist($uo);
|
$this->entityManager->persist($uo);
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
|
$this->loggerService->logOrganizationInformation($orgId, $actingUser->getId(), "UO link activated with uo id : {$uo->getId()}");
|
||||||
$this->actionService->createAction("Activate user in organization", $actingUser, $org, $org->getName() . " for user " . $user->getUserIdentifier());
|
$this->actionService->createAction("Activate user in organization", $actingUser, $org, $org->getName() . " for user " . $user->getUserIdentifier());
|
||||||
$data = ['user' => $user,
|
$data = ['user' => $user,
|
||||||
'organization' => $org];
|
'organization' => $org];
|
||||||
$this->organizationsService->notifyOrganizationAdmins($data, "USER_ACTIVATED");
|
$this->organizationsService->notifyOrganizationAdmins($data, "USER_ACTIVATED");
|
||||||
return new JsonResponse(['status' => 'activated'], Response::HTTP_OK);
|
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) {
|
} catch (\Exception $exception) {
|
||||||
$this->logger->error($exception->getMessage());
|
$this->loggerService->logCritical($exception->getMessage());
|
||||||
}
|
}
|
||||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
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'])]
|
#[Route('/delete/{id}', name: 'delete', methods: ['GET', 'POST'])]
|
||||||
public function delete(int $id, Request $request): Response
|
public function delete(int $id, Request $request): Response
|
||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted("ROLE_SUPER_ADMIN");
|
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
||||||
|
|
||||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
|
||||||
|
try {
|
||||||
$user = $this->userRepository->find($id);
|
$user = $this->userRepository->find($id);
|
||||||
if (!$user) {
|
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);
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Soft delete the user
|
||||||
|
|
||||||
$user->setIsActive(false);
|
$user->setIsActive(false);
|
||||||
$user->setModifiedAt(new \DateTimeImmutable('now'));
|
|
||||||
$this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, $user);
|
|
||||||
$user->setIsDeleted(true);
|
$user->setIsDeleted(true);
|
||||||
if ($this->userService->isUserConnected($user)) {
|
$user->setModifiedAt(new \DateTimeImmutable('now'));
|
||||||
$this->userService->revokeUserTokens($user->getUserIdentifier());
|
// 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->persist($user);
|
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
$this->actionService->createAction("Delete user", $actingUser, null, $user->getUserIdentifier());
|
|
||||||
$data = ['user' => $user,
|
|
||||||
'organization' => null];
|
|
||||||
$this->organizationsService->notifyOrganizationAdmins($data, "USER_DELETED");
|
|
||||||
|
|
||||||
return new Response('', Response::HTTP_NO_CONTENT); //204
|
// 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'])]
|
#[Route(path: '/application/roles/{id}', name: 'application_role', methods: ['GET', 'POST'])]
|
||||||
public function applicationRole(int $id, Request $request): Response
|
public function applicationRole(int $id, Request $request): Response
|
||||||
{
|
{
|
||||||
|
|
@ -447,19 +637,29 @@ class UserController extends AbstractController
|
||||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
|
||||||
if ($this->userService->hasAccessTo($actingUser, true)) {
|
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'));
|
$application = $this->entityManager->getRepository(Apps::class)->find($request->get('appId'));
|
||||||
if (!$application) {
|
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);
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
$selectedRolesIds = $request->get('roles', []);
|
$selectedRolesIds = $request->get('roles', []);
|
||||||
$roleUser = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'USER']);
|
$roleUser = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'USER']);
|
||||||
if (!$roleUser) {
|
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)) {
|
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)) {
|
if (!in_array((string)$roleUser->getId(), $selectedRolesIds, true)) {
|
||||||
$this->userOrganizationAppService->deactivateAllUserOrganizationsAppLinks($uo, $application);
|
$this->userOrganizationAppService->deactivateAllUserOrganizationsAppLinks($uo, $application);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -476,6 +676,7 @@ class UserController extends AbstractController
|
||||||
}
|
}
|
||||||
|
|
||||||
$user = $uo->getUsers();
|
$user = $uo->getUsers();
|
||||||
|
$this->addFlash('success', 'Rôles mis à jour avec succès.');
|
||||||
return $this->redirectToRoute('user_show', [
|
return $this->redirectToRoute('user_show', [
|
||||||
'user' => $user,
|
'user' => $user,
|
||||||
'id' => $user->getId(),
|
'id' => $user->getId(),
|
||||||
|
|
@ -542,7 +743,6 @@ class UserController extends AbstractController
|
||||||
'statut' => $user->isActive(),
|
'statut' => $user->isActive(),
|
||||||
];
|
];
|
||||||
}, $rows);
|
}, $rows);
|
||||||
|
|
||||||
$lastPage = (int)ceil($total / $size);
|
$lastPage = (int)ceil($total / $size);
|
||||||
|
|
||||||
return $this->json([
|
return $this->json([
|
||||||
|
|
@ -555,6 +755,7 @@ class UserController extends AbstractController
|
||||||
#[Route(path: '/', name: 'index', methods: ['GET'])]
|
#[Route(path: '/', name: 'index', methods: ['GET'])]
|
||||||
public function index(): Response
|
public function index(): Response
|
||||||
{
|
{
|
||||||
|
$this->isGranted('ROLE_SUPER_ADMIN');
|
||||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
if ($this->userService->hasAccessTo($actingUser, true) && $this->isGranted("ROLE_ADMIN")) {
|
if ($this->userService->hasAccessTo($actingUser, true) && $this->isGranted("ROLE_ADMIN")) {
|
||||||
$totalUsers = $this->userRepository->count(['isDeleted' => false, 'isActive' => true]);
|
$totalUsers = $this->userRepository->count(['isDeleted' => false, 'isActive' => true]);
|
||||||
|
|
@ -562,6 +763,9 @@ class UserController extends AbstractController
|
||||||
'users' => $totalUsers
|
'users' => $totalUsers
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//shouldn't be reached normally
|
||||||
|
$this->loggerService->logAccessDenied($actingUser->getId());
|
||||||
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -715,27 +919,38 @@ class UserController extends AbstractController
|
||||||
$orgId = $request->get('organizationId');
|
$orgId = $request->get('organizationId');
|
||||||
$org = $this->organizationRepository->find($orgId);
|
$org = $this->organizationRepository->find($orgId);
|
||||||
if (!$org) {
|
if (!$org) {
|
||||||
|
$this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getId());
|
||||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
}
|
}
|
||||||
$user = $this->userRepository->find($userId);
|
$user = $this->userRepository->find($userId);
|
||||||
if (!$user) {
|
if (!$user) {
|
||||||
|
$this->loggerService->logEntityNotFound('User', ['id' => $user->getId()], $actingUser->getId());
|
||||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
}
|
}
|
||||||
$uo = $this->uoRepository->findOneBy(['users' => $user,
|
$uo = $this->uoRepository->findOneBy(['users' => $user,
|
||||||
'organization' => $org,
|
'organization' => $org,
|
||||||
'statut' => "INVITED"]);
|
'statut' => "INVITED"]);
|
||||||
if (!$uo) {
|
if (!$uo) {
|
||||||
|
$this->loggerService->logEntityNotFound('UsersOrganization', [
|
||||||
|
'user_id' => $user->getId(),
|
||||||
|
'organization_id' => $orgId], $actingUser->getId());
|
||||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
}
|
}
|
||||||
$uo->setModifiedAt(new \DateTimeImmutable());
|
$uo->setModifiedAt(new \DateTimeImmutable());
|
||||||
try {
|
try {
|
||||||
$data = ['user' => $uo->getUsers(), 'organization' => $uo->getOrganization()];
|
$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->logger->info("Invitation email resent to user " . $user->getUserIdentifier() . " for organization " . $org->getName());
|
||||||
$this->organizationsService->notifyOrganizationAdmins($data, 'USER_INVITED');
|
$this->organizationsService->notifyOrganizationAdmins($data, 'USER_INVITED');
|
||||||
return $this->json(['message' => 'Invitation envoyée avec success.'], Response::HTTP_OK);
|
return $this->json(['message' => 'Invitation envoyée avec success.'], Response::HTTP_OK);
|
||||||
} catch (\Exception $e) {
|
} 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);
|
return $this->json(['message' => 'Erreur lors de l\'envoie du mail.'], Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -749,19 +964,35 @@ class UserController extends AbstractController
|
||||||
$userId = $request->get('id');
|
$userId = $request->get('id');
|
||||||
|
|
||||||
if (!$token || !$userId) {
|
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.');
|
throw $this->createNotFoundException('Invalid invitation link.');
|
||||||
}
|
}
|
||||||
$user = $this->userRepository->find($userId);
|
$user = $this->userRepository->find($userId);
|
||||||
if (!$user) {
|
if (!$user) {
|
||||||
|
$this->loggerService->logEntityNotFound('User not found in accept invitation', [
|
||||||
|
'user_id' => $userId
|
||||||
|
],null);
|
||||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
}
|
}
|
||||||
if (!$this->userService->isPasswordTokenValid($user, $token)) {
|
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.');
|
throw $this->createNotFoundException('Invalid or expired invitation token.');
|
||||||
}
|
}
|
||||||
$orgId = $this->userService->getOrgFromToken($token);
|
$orgId = $this->userService->getOrgFromToken($token);
|
||||||
|
if ($orgId) {
|
||||||
$uo = $this->uoRepository->findOneBy(['users' => $user, 'organization' => $orgId]);
|
$uo = $this->uoRepository->findOneBy(['users' => $user, 'organization' => $orgId]);
|
||||||
if (!$uo || $uo->getStatut() !== 'INVITED') {
|
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);
|
$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.');
|
throw $this->createNotFoundException('No pending invitation found for this user and organization.');
|
||||||
}
|
}
|
||||||
$uo->setModifiedAt(new \DateTimeImmutable());
|
$uo->setModifiedAt(new \DateTimeImmutable());
|
||||||
|
|
@ -769,9 +1000,10 @@ class UserController extends AbstractController
|
||||||
$uo->setIsActive(true);
|
$uo->setIsActive(true);
|
||||||
$this->entityManager->persist($uo);
|
$this->entityManager->persist($uo);
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
$this->logger->info("User " . $user->getUserIdentifier() . " accepted invitation for organization ID " . $orgId);
|
$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()}");
|
||||||
return $this->render('user/show.html.twig', ['user' => $user, 'orgId' => $orgId]);
|
}
|
||||||
|
return $this->render('security/login.html.twig');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ class Apps
|
||||||
{
|
{
|
||||||
$this->userOrganizatonApps = new ArrayCollection();
|
$this->userOrganizatonApps = new ArrayCollection();
|
||||||
$this->organization = new ArrayCollection();
|
$this->organization = new ArrayCollection();
|
||||||
|
$this->setIsActive(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,13 @@ namespace App\Entity;
|
||||||
|
|
||||||
use App\Repository\OrganizationsRepository;
|
use App\Repository\OrganizationsRepository;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
#[ORM\Entity(repositoryClass: OrganizationsRepository::class)]
|
#[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
|
class Organizations
|
||||||
{
|
{
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
|
|
@ -24,7 +27,7 @@ class Organizations
|
||||||
#[ORM\Column(length: 255)]
|
#[ORM\Column(length: 255)]
|
||||||
private ?string $address = null;
|
private ?string $address = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 255)]
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
private ?string $logo_url = null;
|
private ?string $logo_url = null;
|
||||||
|
|
||||||
#[ORM\Column(options: ['default' => 'CURRENT_TIMESTAMP'])]
|
#[ORM\Column(options: ['default' => 'CURRENT_TIMESTAMP'])]
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
|
||||||
|
|
||||||
#[ORM\Entity(repositoryClass: UserRepository::class)]
|
#[ORM\Entity(repositoryClass: UserRepository::class)]
|
||||||
#[ORM\Table(name: '`user`')]
|
#[ORM\Table(name: '`user`')]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Event;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
use Symfony\Contracts\EventDispatcher\Event;
|
||||||
|
|
||||||
|
class UserCreatedEvent extends Event
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly User $newUser,
|
||||||
|
private readonly User $actingUser
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getNewUser(): User
|
||||||
|
{
|
||||||
|
return $this->newUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getActingUser(): User
|
||||||
|
{
|
||||||
|
return $this->actingUser;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\EventSubscriber;
|
||||||
|
|
||||||
|
use App\Event\UserCreatedEvent;
|
||||||
|
use App\Service\ActionService;
|
||||||
|
use App\Service\EmailService;
|
||||||
|
use App\Service\LoggerService;
|
||||||
|
use App\Service\UserService; // Only if you need helper methods, otherwise avoid to prevent circular ref
|
||||||
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
|
|
||||||
|
class UserSubscriber implements EventSubscriberInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly EmailService $emailService,
|
||||||
|
private readonly LoggerService $loggerService,
|
||||||
|
private readonly ActionService $actionService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getSubscribedEvents(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
UserCreatedEvent::class => '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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,8 +17,8 @@ class OrganizationForm extends AbstractType
|
||||||
$builder
|
$builder
|
||||||
->add('email', EmailType::class, ['required' => true, 'label' => 'Email*'])
|
->add('email', EmailType::class, ['required' => true, 'label' => 'Email*'])
|
||||||
->add('name', TextType::class, ['required' => true, 'label' => 'Nom de l\'organisation*'])
|
->add('name', TextType::class, ['required' => true, 'label' => 'Nom de l\'organisation*'])
|
||||||
->add('address', TextType::class, ['required' => false, 'label' => 'Adresse'])
|
->add('address', TextType::class, ['required' => true, 'label' => 'Adresse'])
|
||||||
->add('number', TextType::class, ['required' => false, 'label' => 'Numéro de téléphone'])
|
->add('number', TextType::class, ['required' => true, 'label' => 'Numéro de téléphone'])
|
||||||
->add('logoUrl', FileType::class, [
|
->add('logoUrl', FileType::class, [
|
||||||
'required' => false,
|
'required' => false,
|
||||||
'label' => 'Logo',
|
'label' => 'Logo',
|
||||||
|
|
|
||||||
|
|
@ -11,17 +11,38 @@ class AccessTokenService
|
||||||
|
|
||||||
private EntityManagerInterface $entityManager;
|
private EntityManagerInterface $entityManager;
|
||||||
|
|
||||||
public function __construct(EntityManagerInterface $entityManager)
|
public function __construct(EntityManagerInterface $entityManager,
|
||||||
|
private readonly LoggerService $loggerService)
|
||||||
{
|
{
|
||||||
$this->entityManager = $entityManager;
|
$this->entityManager = $entityManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function revokeTokens(String $userIdentifier): void {
|
public function revokeUserTokens(string $userIdentifier): void
|
||||||
$accessTokens = $this->entityManager->getRepository(AccessToken::class)->findBy(['userIdentifier' => $userIdentifier, 'revoked' => false]);
|
{
|
||||||
foreach($accessTokens as $accessToken) {
|
$tokens = $this->entityManager->getRepository(AccessToken::class)->findBy([
|
||||||
$accessToken->revoke();
|
'userIdentifier' => $userIdentifier,
|
||||||
$this->entityManager->persist($accessToken);
|
'revoked' => false
|
||||||
$this->entityManager->flush();
|
]);
|
||||||
|
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(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,11 +40,11 @@ readonly class ActionService
|
||||||
{
|
{
|
||||||
return array_map(function (Actions $activity) {
|
return array_map(function (Actions $activity) {
|
||||||
return [
|
return [
|
||||||
'date' => $activity->getDate(),
|
'date' => $activity->getDate()->format('d/m/Y H:i'),
|
||||||
'actionType' => $activity->getActionType(),
|
'actionType' => $activity->getActionType(),
|
||||||
'users' => $activity->getUsers(),
|
'userName' => $activity->getUsers()->getName(),
|
||||||
'organization' => $activity->getOrganization(),
|
// 'organization' => $activity->getOrganization(),
|
||||||
'description' => $activity->getDescription(),
|
// 'description' => $activity->getDescription(),
|
||||||
'color' => $this->getActivityColor($activity->getDate())
|
'color' => $this->getActivityColor($activity->getDate())
|
||||||
];
|
];
|
||||||
}, $activities);
|
}, $activities);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace App\Service;
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\Service\LoggerService;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
use App\Entity\Cgu;
|
use App\Entity\Cgu;
|
||||||
|
|
@ -9,7 +10,7 @@ use App\Entity\CguUser;
|
||||||
|
|
||||||
class CguUserService
|
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]);
|
$cguUser = $this->entityManager->getRepository(CguUser::class)->findOneBy(['users' => $user, 'cgu' => $latestCgu]);
|
||||||
if (!$cguUser) {
|
if (!$cguUser) {
|
||||||
|
try{
|
||||||
// Create a new CguUser relation if it doesn't exist
|
// Create a new CguUser relation if it doesn't exist
|
||||||
$cguUser = new CguUser();
|
$cguUser = new CguUser();
|
||||||
$cguUser->setUsers($user);
|
$cguUser->setUsers($user);
|
||||||
$cguUser->setCgu($latestCgu);
|
$cguUser->setCgu($latestCgu);
|
||||||
$this->entityManager->persist($cguUser);
|
$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);
|
$cguUser->setIsAccepted(true);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ namespace App\Service;
|
||||||
|
|
||||||
use App\Entity\Organizations;
|
use App\Entity\Organizations;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
|
use App\Service\LoggerService;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||||
|
|
@ -14,15 +15,12 @@ class EmailService
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly MailerInterface $mailer,
|
private readonly MailerInterface $mailer,
|
||||||
private readonly UserService $userService,
|
|
||||||
private readonly LoggerInterface $logger,
|
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
|
// Generate absolute URL for the password setup route
|
||||||
$link = $this->urlGenerator->generate(
|
$link = $this->urlGenerator->generate(
|
||||||
'password_setup',
|
'password_setup',
|
||||||
|
|
@ -46,15 +44,16 @@ class EmailService
|
||||||
]);
|
]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
$orgId = $this->getOrgFromToken($token);
|
||||||
$this->mailer->send($email);
|
$this->mailer->send($email);
|
||||||
|
$this->loggerService->logEmailSent($user->getId(), $orgId, 'Password setup email sent.');
|
||||||
} catch (\Symfony\Component\Mailer\Exception\TransportExceptionInterface $e) {
|
} catch (\Symfony\Component\Mailer\Exception\TransportExceptionInterface $e) {
|
||||||
$this->logger->error('Failed to send password setup email: ' . $e->getMessage());
|
$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',[
|
$link = $this->urlGenerator->generate('user_accept',[
|
||||||
'id' => $existingUser->getId(),
|
'id' => $existingUser->getId(),
|
||||||
'token' => $token
|
'token' => $token
|
||||||
|
|
@ -73,10 +72,26 @@ class EmailService
|
||||||
]);
|
]);
|
||||||
|
|
||||||
try{
|
try{
|
||||||
|
$orgId = $org->getId();
|
||||||
|
$this->loggerService->logEmailSent($existingUser->getId(), $orgId, 'Existing user notification email sent.');
|
||||||
$this->mailer->send($email);
|
$this->mailer->send($email);
|
||||||
} catch (TransportExceptionInterface $e) {
|
} catch (TransportExceptionInterface $e) {
|
||||||
$this->logger->error('Failed to send existing user notification email: ' . $e->getMessage());
|
$this->logger->error('Failed to send existing user notification email: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,263 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
|
||||||
|
readonly class LoggerService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private LoggerInterface $userManagementLogger,
|
||||||
|
private LoggerInterface $organizationManagementLogger,
|
||||||
|
private LoggerInterface $accessControlLogger,
|
||||||
|
private LoggerInterface $emailNotificationLogger,
|
||||||
|
private LoggerInterface $adminActionsLogger,
|
||||||
|
private LoggerInterface $securityLogger,
|
||||||
|
private LoggerInterface $errorLogger,
|
||||||
|
private LoggerInterface $awsLogger,
|
||||||
|
private RequestStack $requestStack,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// User Management Logs
|
||||||
|
public function logUserCreated(int $userId, int $actingUserId): void
|
||||||
|
{
|
||||||
|
$this->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(),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -65,7 +65,7 @@ class NotificationService
|
||||||
$this->send(
|
$this->send(
|
||||||
recipient: $recipient,
|
recipient: $recipient,
|
||||||
type: self::TYPE_USER_DEACTIVATED,
|
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()),
|
message: sprintf('%s %s a été désactivé de %s', $removedUser->getName(), $removedUser->getSurname(), $organization->getName()),
|
||||||
data: [
|
data: [
|
||||||
'userId' => $removedUser->getId(),
|
'userId' => $removedUser->getId(),
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,11 @@ use App\Entity\Apps;
|
||||||
use App\Entity\Organizations;
|
use App\Entity\Organizations;
|
||||||
use App\Entity\Roles;
|
use App\Entity\Roles;
|
||||||
use App\Entity\UserOrganizatonApp;
|
use App\Entity\UserOrganizatonApp;
|
||||||
|
use App\Entity\UsersOrganizations;
|
||||||
use App\Repository\UsersOrganizationsRepository;
|
use App\Repository\UsersOrganizationsRepository;
|
||||||
|
use App\Service\LoggerService;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Component\HttpFoundation\File\Exception\FileException;
|
use Symfony\Component\HttpFoundation\File\Exception\FileException;
|
||||||
|
|
||||||
class OrganizationsService
|
class OrganizationsService
|
||||||
|
|
@ -18,7 +21,8 @@ class OrganizationsService
|
||||||
string $logoDirectory, private readonly AwsService $awsService,
|
string $logoDirectory, private readonly AwsService $awsService,
|
||||||
private readonly EntityManagerInterface $entityManager,
|
private readonly EntityManagerInterface $entityManager,
|
||||||
private readonly UsersOrganizationsRepository $uoRepository,
|
private readonly UsersOrganizationsRepository $uoRepository,
|
||||||
private readonly NotificationService $notificationService
|
private readonly NotificationService $notificationService,
|
||||||
|
private readonly LoggerInterface $emailNotificationLogger, private readonly LoggerService $loggerService,
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
$this->logoDirectory = $logoDirectory;
|
$this->logoDirectory = $logoDirectory;
|
||||||
|
|
@ -32,8 +36,18 @@ class OrganizationsService
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->awsService->PutDocObj($_ENV['S3_PORTAL_BUCKET'], $logoFile, $customFilename, $extension, 'logo/');
|
$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);
|
$organization->setLogoUrl('logo/' . $customFilename);
|
||||||
} catch (FileException $e) {
|
} 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());
|
throw new FileException('Failed to upload logo to S3: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -85,6 +99,10 @@ class OrganizationsService
|
||||||
$newUser,
|
$newUser,
|
||||||
$data['organization']
|
$data['organization']
|
||||||
);
|
);
|
||||||
|
$this->loggerService->logAdminNotified([
|
||||||
|
'admin_user_id' =>$adminUO->getUsers()->getId(),
|
||||||
|
'target_user_id' => $newUser->getId(),
|
||||||
|
'organization_id' => $data['organization']->getId(),'case' =>$type]);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'USER_INVITED':
|
case 'USER_INVITED':
|
||||||
|
|
@ -95,7 +113,12 @@ class OrganizationsService
|
||||||
$invitedUser,
|
$invitedUser,
|
||||||
$data['organization']
|
$data['organization']
|
||||||
);
|
);
|
||||||
|
$this->loggerService->logAdminNotified([
|
||||||
|
'admin_user_id' =>$adminUO->getUsers()->getId(),
|
||||||
|
'target_user_id' => $invitedUser->getId(),
|
||||||
|
'organization_id' => $data['organization']->getId(),'case' =>$type]);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'USER_DEACTIVATED':
|
case 'USER_DEACTIVATED':
|
||||||
if ($uoa && $adminUO->getUsers()->getId() !== $data['user']->getId() ) {
|
if ($uoa && $adminUO->getUsers()->getId() !== $data['user']->getId() ) {
|
||||||
|
|
@ -105,7 +128,12 @@ class OrganizationsService
|
||||||
$removedUser,
|
$removedUser,
|
||||||
$data['organization']
|
$data['organization']
|
||||||
);
|
);
|
||||||
|
$this->loggerService->logAdminNotified([
|
||||||
|
'admin_user_id' =>$adminUO->getUsers()->getId(),
|
||||||
|
'target_user_id' => $removedUser->getId(),
|
||||||
|
'organization_id' => $data['organization']->getId(),'case' =>$type]);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'USER_DELETED':
|
case 'USER_DELETED':
|
||||||
if ($uoa && $adminUO->getUsers()->getId() !== $data['user']->getId() ) {
|
if ($uoa && $adminUO->getUsers()->getId() !== $data['user']->getId() ) {
|
||||||
|
|
@ -115,6 +143,10 @@ class OrganizationsService
|
||||||
$removedUser,
|
$removedUser,
|
||||||
$data['organization']
|
$data['organization']
|
||||||
);
|
);
|
||||||
|
$this->loggerService->logAdminNotified([
|
||||||
|
'admin_user_id' =>$adminUO->getUsers()->getId(),
|
||||||
|
'target_user_id' => $removedUser->getId(),
|
||||||
|
'organization_id' => $data['organization']->getId(),'case' =>$type]);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'USER_ACTIVATED':
|
case 'USER_ACTIVATED':
|
||||||
|
|
@ -125,6 +157,10 @@ class OrganizationsService
|
||||||
$activatedUser,
|
$activatedUser,
|
||||||
$data['organization']
|
$data['organization']
|
||||||
);
|
);
|
||||||
|
$this->loggerService->logAdminNotified([
|
||||||
|
'admin_user_id' =>$adminUO->getUsers()->getId(),
|
||||||
|
'target_user_id' => $activatedUser->getId(),
|
||||||
|
'organization_id' => $data['organization']->getId(),'case' =>$type]);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,15 @@ use App\Entity\User;
|
||||||
use App\Entity\UserOrganizatonApp;
|
use App\Entity\UserOrganizatonApp;
|
||||||
use App\Entity\UsersOrganizations;
|
use App\Entity\UsersOrganizations;
|
||||||
use App\Service\ActionService;
|
use App\Service\ActionService;
|
||||||
|
use App\Service\LoggerService;
|
||||||
use App\Service\UserService;
|
use App\Service\UserService;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Bundle\SecurityBundle\Security;
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
|
|
||||||
class UserOrganizationAppService
|
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]);
|
$uoas = $this->entityManager->getRepository(UserOrganizatonApp::class)->findBy(['userOrganization' => $userOrganization, 'isActive' => true]);
|
||||||
}
|
}
|
||||||
foreach ($uoas as $uoa) {
|
foreach ($uoas as $uoa) {
|
||||||
|
try{
|
||||||
$uoa->setIsActive(false);
|
$uoa->setIsActive(false);
|
||||||
$this->actionService->createAction("Deactivate UOA link", $userOrganization->getUsers(),
|
$this->actionService->createAction("Deactivate UOA link", $userOrganization->getUsers(),
|
||||||
$userOrganization->getOrganization(), "App: " . $uoa->getApplication()->getName() . ", Role: " . $uoa->getRole()->getName());
|
$userOrganization->getOrganization(), "App: " . $uoa->getApplication()->getName() . ", Role: " . $uoa->getRole()->getName());
|
||||||
$this->entityManager->persist($uoa);
|
$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()) {
|
if (!$uoa->isActive()) {
|
||||||
$uoa->setIsActive(true);
|
$uoa->setIsActive(true);
|
||||||
$this->entityManager->persist($uoa);
|
$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(
|
$this->actionService->createAction(
|
||||||
"Re-activate user role for application",
|
"Re-activate user role for application",
|
||||||
$actingUser,
|
$actingUser,
|
||||||
|
|
@ -148,7 +165,11 @@ class UserOrganizationAppService
|
||||||
if ($uoa->isActive()) {
|
if ($uoa->isActive()) {
|
||||||
$uoa->setIsActive(false);
|
$uoa->setIsActive(false);
|
||||||
$this->entityManager->persist($uoa);
|
$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(
|
$this->actionService->createAction(
|
||||||
"Deactivate user role for application",
|
"Deactivate user role for application",
|
||||||
$actingUser,
|
$actingUser,
|
||||||
|
|
@ -185,6 +206,11 @@ class UserOrganizationAppService
|
||||||
$this->ensureAdminRoleForSuperAdmin($newUoa);
|
$this->ensureAdminRoleForSuperAdmin($newUoa);
|
||||||
}
|
}
|
||||||
$this->entityManager->persist($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",
|
$this->actionService->createAction("New user role for application",
|
||||||
$actingUser,
|
$actingUser,
|
||||||
$uo->getOrganization(),
|
$uo->getOrganization(),
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use App\Entity\Organizations;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Entity\UsersOrganizations;
|
use App\Entity\UsersOrganizations;
|
||||||
use App\Service\ActionService;
|
use App\Service\ActionService;
|
||||||
|
use App\Service\LoggerService;
|
||||||
use \App\Service\UserOrganizationAppService;
|
use \App\Service\UserOrganizationAppService;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
|
|
@ -19,7 +20,7 @@ readonly class UserOrganizationService
|
||||||
{
|
{
|
||||||
|
|
||||||
public function __construct(
|
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]);
|
$uos = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['organization' => $organizations, 'isActive' => true]);
|
||||||
}
|
}
|
||||||
//deactivate all UO links
|
//deactivate all UO links
|
||||||
|
if (!empty($uos)) {
|
||||||
foreach ($uos as $uo) {
|
foreach ($uos as $uo) {
|
||||||
$this->userOrganizationAppService->deactivateAllUserOrganizationsAppLinks($uo);
|
$this->userOrganizationAppService->deactivateAllUserOrganizationsAppLinks($uo);
|
||||||
|
$this->loggerService->logOrganizationInformation($uo->getOrganization()->getId(), $actingUser->getId(),
|
||||||
|
'Uo link deactivated');
|
||||||
$uo->setIsActive(false);
|
$uo->setIsActive(false);
|
||||||
$this->entityManager->persist($uo);
|
$this->entityManager->persist($uo);
|
||||||
$this->actionService->createAction("Deactivate UO link", $actingUser, $uo->getOrganization(), $uo->getOrganization()->getName() );
|
$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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ use App\Entity\Roles;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Entity\UserOrganizatonApp;
|
use App\Entity\UserOrganizatonApp;
|
||||||
use App\Entity\UsersOrganizations;
|
use App\Entity\UsersOrganizations;
|
||||||
use App\Service\AwsService;
|
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use DateTimeZone;
|
use DateTimeZone;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
|
@ -16,9 +15,11 @@ use Doctrine\ORM\EntityNotFoundException;
|
||||||
use Exception;
|
use Exception;
|
||||||
use League\Bundle\OAuth2ServerBundle\Model\AccessToken;
|
use League\Bundle\OAuth2ServerBundle\Model\AccessToken;
|
||||||
use Random\RandomException;
|
use Random\RandomException;
|
||||||
use SebastianBergmann\CodeCoverage\Util\DirectoryCouldNotBeCreatedException;
|
use RuntimeException;
|
||||||
use Symfony\Bundle\SecurityBundle\Security;
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
use Symfony\Component\HttpFoundation\File\Exception\FileException;
|
use Symfony\Component\HttpFoundation\File\Exception\FileException;
|
||||||
|
use App\Event\UserCreatedEvent;
|
||||||
|
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||||
|
|
||||||
class UserService
|
class UserService
|
||||||
{
|
{
|
||||||
|
|
@ -27,7 +28,12 @@ class UserService
|
||||||
|
|
||||||
public function __construct(private readonly EntityManagerInterface $entityManager,
|
public function __construct(private readonly EntityManagerInterface $entityManager,
|
||||||
private readonly Security $security,
|
private readonly Security $security,
|
||||||
private readonly AwsService $awsService
|
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
|
public function generateRandomPassword(): string
|
||||||
{
|
{
|
||||||
$length = 50; // Length of the password
|
return bin2hex(random_bytes(32));
|
||||||
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+';
|
|
||||||
$charactersLength = strlen($characters);
|
|
||||||
$randomPassword = '';
|
|
||||||
|
|
||||||
for ($i = 0; $i < $length; $i++) {
|
|
||||||
$randomPassword .= $characters[random_int(0, $charactersLength - 1)];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $randomPassword;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -88,20 +85,20 @@ class UserService
|
||||||
*/
|
*/
|
||||||
public function hasAccessTo(User $user, bool $skipSelfCheck = false): bool
|
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()) {
|
if (!$skipSelfCheck && $user->getUserIdentifier() === $this->security->getUser()->getUserIdentifier()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
$userOrganizations = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['users' => $user]);
|
$userOrganizations = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['users' => $user]);
|
||||||
if ($userOrganizations) {
|
if ($userOrganizations) {
|
||||||
foreach ($userOrganizations as $uo) {
|
foreach ($userOrganizations as $uo) {
|
||||||
if ($this->isAdminOfOrganization($uo->getOrganization())) {
|
if ($this->isAdminOfOrganization($uo->getOrganization()) && $uo->getStatut() === "ACCEPTED" && $uo->isActive()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($this->security->isGranted('ROLE_SUPER_ADMIN')) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -112,7 +109,7 @@ class UserService
|
||||||
* entity role 'ROLE_ADMIN' in the UsersOrganizationsApp entity
|
* entity role 'ROLE_ADMIN' in the UsersOrganizationsApp entity
|
||||||
* (if he is admin for any application of the organization).
|
* (if he is admin for any application of the organization).
|
||||||
*
|
*
|
||||||
* @param UsersOrganizations $usersOrganizations
|
* @param Organizations $organizations
|
||||||
* @return bool
|
* @return bool
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
|
|
@ -144,6 +141,7 @@ class UserService
|
||||||
{
|
{
|
||||||
$user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $userIdentifier]);
|
$user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $userIdentifier]);
|
||||||
if (!$user) {
|
if (!$user) {
|
||||||
|
$this->loggerService->logEntityNotFound('User', ['user_identifier' => $userIdentifier], null);
|
||||||
throw new EntityNotFoundException(self::NOT_FOUND);
|
throw new EntityNotFoundException(self::NOT_FOUND);
|
||||||
}
|
}
|
||||||
return $user;
|
return $user;
|
||||||
|
|
@ -175,18 +173,20 @@ class UserService
|
||||||
return ['none' => $group];
|
return ['none' => $group];
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: reset function
|
|
||||||
public function handleProfilePicture(User $user, $picture): void
|
public function handleProfilePicture(User $user, $picture): void
|
||||||
{
|
{
|
||||||
// Get file extension
|
// Get file extension
|
||||||
$extension = $picture->guessExtension();
|
$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() . '_' . date('dmyHis') . '.' . $extension;
|
||||||
// $customFilename = $user->getName() . $user->getSurname() . "." .$extension;
|
|
||||||
try {
|
try {
|
||||||
$this->awsService->PutDocObj($_ENV['S3_PORTAL_BUCKET'], $picture, $customFilename, $extension, 'profile/');
|
$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);
|
$user->setPictureUrl('profile/' . $customFilename);
|
||||||
} catch (FileException $e) {
|
} catch (FileException $e) {
|
||||||
// Handle upload error
|
// Handle upload error
|
||||||
|
|
@ -242,12 +242,18 @@ class UserService
|
||||||
if ($roleFormatted === 'ROLE_SUPER_ADMIN' && !in_array('ROLE_ADMIN', $user->getRoles(), true)) {
|
if ($roleFormatted === 'ROLE_SUPER_ADMIN' && !in_array('ROLE_ADMIN', $user->getRoles(), true)) {
|
||||||
$user->setRoles(array_merge($user->getRoles(), ['ROLE_ADMIN']));
|
$user->setRoles(array_merge($user->getRoles(), ['ROLE_ADMIN']));
|
||||||
}
|
}
|
||||||
|
$this->loggerService->logRoleAssignment(
|
||||||
|
'Role assigned to user',
|
||||||
|
[
|
||||||
|
'user_id' => $user->getId(),
|
||||||
|
'role' => $roleFormatted,
|
||||||
|
]
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// Remove the role if present and not used elsewhere
|
// Remove the role if present and not used elsewhere
|
||||||
if (in_array($roleFormatted, $user->getRoles(), true)) {
|
if (in_array($roleFormatted, $user->getRoles(), true)) {
|
||||||
$uos = $this->entityManager->getRepository(UsersOrganizations::class)
|
$uos = $this->entityManager->getRepository(UsersOrganizations::class)
|
||||||
->findBy(['users' => $user, 'isActive' => true]);
|
->findBy(['users' => $user, 'isActive' => true]);
|
||||||
|
|
||||||
$hasRole = false;
|
$hasRole = false;
|
||||||
foreach ($uos as $uo) {
|
foreach ($uos as $uo) {
|
||||||
$uoa = $this->entityManager->getRepository(UserOrganizatonApp::class)
|
$uoa = $this->entityManager->getRepository(UserOrganizatonApp::class)
|
||||||
|
|
@ -257,7 +263,6 @@ class UserService
|
||||||
'role' => $this->entityManager->getRepository(Roles::class)
|
'role' => $this->entityManager->getRepository(Roles::class)
|
||||||
->findOneBy(['name' => $role]),
|
->findOneBy(['name' => $role]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($uoa) {
|
if ($uoa) {
|
||||||
$hasRole = true;
|
$hasRole = true;
|
||||||
break;
|
break;
|
||||||
|
|
@ -287,17 +292,6 @@ class UserService
|
||||||
return 'ROLE_' . $role;
|
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
|
public function formatStatutForOrganizations(array $rows): array
|
||||||
{
|
{
|
||||||
|
|
@ -330,7 +324,7 @@ class UserService
|
||||||
return $formatted;
|
return $formatted;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function generatePasswordToken(User $user, int $orgId): string
|
public function generatePasswordToken(User $user, int $orgId = null): string
|
||||||
{
|
{
|
||||||
$orgString = "o" . $orgId . "@";
|
$orgString = "o" . $orgId . "@";
|
||||||
$token = $orgString . bin2hex(random_bytes(32));
|
$token = $orgString . bin2hex(random_bytes(32));
|
||||||
|
|
@ -413,15 +407,17 @@ class UserService
|
||||||
return $rolesArray;
|
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();
|
$userRoles = $user->getRoles();
|
||||||
$actingUserRoles = $actingUser->getRoles();
|
$actingUserRoles = $actingUser->getRoles();
|
||||||
// if acting user is admin, he can´t edit super admin roles
|
// if acting user is admin, he can´t edit super admin roles
|
||||||
|
if (!in_array('ROLE_SUPER_ADMIN', $actingUserRoles, true) && in_array('ROLE_SUPER_ADMIN', $userRoles, true)) {
|
||||||
if (in_array('ROLE_SUPER_ADMIN', $userRoles, true) && !in_array('ROLE_SUPER_ADMIN', $actingUserRoles, true)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if ($uo && $this->isAdminOfOrganization($uo->getOrganization())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return $isAdmin && !empty($org);
|
return $isAdmin && !empty($org);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -434,7 +430,7 @@ class UserService
|
||||||
* @param Organizations $organization
|
* @param Organizations $organization
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function handleExistingUser(User $user, Organizations $organization): void
|
public function handleExistingUser(User $user, Organizations $organization): int
|
||||||
{
|
{
|
||||||
if (!$user->isActive()) {
|
if (!$user->isActive()) {
|
||||||
$user->setIsActive(true);
|
$user->setIsActive(true);
|
||||||
|
|
@ -448,6 +444,8 @@ class UserService
|
||||||
$uo->setModifiedAt(new \DateTimeImmutable('now'));
|
$uo->setModifiedAt(new \DateTimeImmutable('now'));
|
||||||
$this->entityManager->persist($uo);
|
$this->entityManager->persist($uo);
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return $uo->getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -460,7 +458,7 @@ class UserService
|
||||||
* @param User $user
|
* @param User $user
|
||||||
* @return void
|
* @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
|
// capitalize name and surname
|
||||||
$user->setName(ucfirst(strtolower($user->getName())));
|
$user->setName(ucfirst(strtolower($user->getName())));
|
||||||
|
|
@ -472,11 +470,161 @@ class UserService
|
||||||
$user->setEmail(trim($user->getEmail()));
|
$user->setEmail(trim($user->getEmail()));
|
||||||
if ($setPassword) {
|
if ($setPassword) {
|
||||||
//FOR SETTING A DEFAULT RANDOM PASSWORD OF 50 CHARACTERS until user set his own password
|
//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);
|
$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(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
12
symfony.lock
12
symfony.lock
|
|
@ -11,6 +11,18 @@
|
||||||
"config/packages/aws.yaml"
|
"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": {
|
"doctrine/deprecations": {
|
||||||
"version": "1.1",
|
"version": "1.1",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
{% if application.hasAccess %}
|
{% if application.hasAccess %}
|
||||||
{% if is_granted("ROLE_SUPER_ADMIN") %}
|
{% if is_granted("ROLE_SUPER_ADMIN") %}
|
||||||
<form method="POST"
|
<form method="POST"
|
||||||
action="{{ path('application_remove', {'id': application.entity.id}) }}"
|
action="{{ path('application_revoke', {'id': application.entity.id}) }}"
|
||||||
data-controller="application"
|
data-controller="application"
|
||||||
data-application-application-value="{{ application.entity.name }}"
|
data-application-application-value="{{ application.entity.name }}"
|
||||||
data-application-organization-value="{{ organization.name|capitalize }}"
|
data-application-organization-value="{{ organization.name|capitalize }}"
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,22 @@
|
||||||
<div class="w-100 h-100 p-5 m-auto">
|
<div class="w-100 h-100 p-5 m-auto">
|
||||||
<div class="row m-5">
|
<div class="row m-5">
|
||||||
<div class="container mt-5">
|
<div class="container mt-5">
|
||||||
|
{% for type, messages in app.flashes %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{{ type }}">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
<h1 class="mb-4">Bienvenue sur la suite Easy</h1>
|
<h1 class="mb-4">Bienvenue sur la suite Easy</h1>
|
||||||
<p class="lead">Ici, vous pouvez trouver toutes nos applications à un seul endroit !</p>
|
<p class="lead">Ici, vous pouvez trouver toutes nos applications à un seul endroit !</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if applications is empty %}
|
||||||
|
<div class="alert alert-info w-100 text-center" role="alert">
|
||||||
|
Aucune application disponible pour le moment. Veuillez revenir plus tard.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% for application in applications %}
|
{% for application in applications %}
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
{% include 'application/InformationCard.html.twig' with {
|
{% include 'application/InformationCard.html.twig' with {
|
||||||
|
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
{% block body %}
|
|
||||||
|
|
||||||
|
|
||||||
<div class="card border-0">
|
|
||||||
<div class="card-header d-flex justify-content-between align-items-center border-0">
|
|
||||||
<h3>{{ title }}</h3>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
{% if activities|length == 0 %}
|
|
||||||
<p>Aucune activité récente.</p>
|
|
||||||
{% else %}
|
|
||||||
{% set sortedActivities = activities|sort((a, b) => a.date <=> b.date)|reverse %}
|
|
||||||
<ul class="list-group gap-2">
|
|
||||||
{% for activity in sortedActivities%}
|
|
||||||
{% include 'user/organization/userActivity.html.twig' with {
|
|
||||||
activityTime: activity.date,
|
|
||||||
action: activity.actionType,
|
|
||||||
userName: activity.users.name,
|
|
||||||
color: activity.color
|
|
||||||
} %}
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
@ -5,9 +5,6 @@
|
||||||
<div class="card no-header-bg p-3 m-3">
|
<div class="card no-header-bg p-3 m-3">
|
||||||
<div class="card-header border-0">
|
<div class="card-header border-0">
|
||||||
<h2>Modifier l'organisation</h2>
|
<h2>Modifier l'organisation</h2>
|
||||||
{% if is_granted("ROLE_SUPER_ADMIN") %}
|
|
||||||
{# <a href="{{ path('organization_delete', {'id': organization.id}) }}" class="btn btn-danger">Supprimer</a>#}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,13 @@
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="w-100 h-100 p-5 m-auto">
|
<div class="w-100 h-100 p-5 m-auto">
|
||||||
|
{% for type, messages in app.flashes %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{{ type }}">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
<div class="card no-header-bg p-3 m-3 border-0">
|
<div class="card no-header-bg p-3 m-3 border-0">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center border-0">
|
<div class="card-header d-flex justify-content-between align-items-center border-0">
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
|
|
@ -11,30 +18,31 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if is_granted("ROLE_SUPER_ADMIN") %}
|
{% if is_granted("ROLE_SUPER_ADMIN") %}
|
||||||
<a href="{{ path('organization_new') }}" class="btn btn-primary">Ajouter une organisation</a>
|
<a href="{{ path('organization_create') }}" class="btn btn-primary">Ajouter une organisation</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if organizationsData|length == 0 %}
|
{% if is_granted('ROLE_SUPER_ADMIN') and not hasOrganizations %}
|
||||||
|
|
||||||
{# style présent juste pour créer de l'espace #}
|
|
||||||
<div class="div text-center my-5 py-5">
|
<div class="div text-center my-5 py-5">
|
||||||
<h1 class="my-5 ty-5"> Aucune organisation trouvée. </h1>
|
<h1 class="my-5 ty-5"> Aucune organisation trouvée. </h1>
|
||||||
<a href="{{ path('organization_new') }}" class="btn btn-primary">Créer une organisation</a>
|
<a href="{{ path('organization_create') }}" class="btn btn-primary">Créer une organisation</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
<div id="tabulator-org" data-controller="organization"
|
<div id="tabulator-org"
|
||||||
data-organization-data-value="{{ organizationsData|json_encode(constant("JSON_UNESCAPED_UNICODE"))|e("html_attr") }}"
|
data-controller="organization"
|
||||||
data-organization-aws-value="{{ aws_url }}"></div>
|
data-organization-table-value="true"
|
||||||
|
data-organization-user-value={{ app.user.getId() }}
|
||||||
|
data-organization-sadmin-value="{{ is_granted('ROLE_SUPER_ADMIN') ? true : false }}"
|
||||||
|
data-organization-aws-value="{{ aws_url }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,20 @@
|
||||||
<div class="w-100 h-100 p-5 m-auto">
|
<div class="w-100 h-100 p-5 m-auto">
|
||||||
<div class="card no-header-bg p-3 m-3">
|
<div class="card no-header-bg p-3 m-3">
|
||||||
<div class="card-header border-0">
|
<div class="card-header border-0">
|
||||||
|
{% for type, messages in app.flashes %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{{ type }}">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
<div class="card-title d-flex justify-content-between align-items-center">
|
<div class="card-title d-flex justify-content-between align-items-center">
|
||||||
<h1>Ajouter une organisation</h1>
|
<h1>Ajouter une organisation</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="post" action="{{ path('organization_new') }}" enctype="multipart/form-data">
|
<form method="post" action="{{ path('organization_create') }}" enctype="multipart/form-data">
|
||||||
{{ form_start(form) }}
|
{{ form_start(form) }}
|
||||||
{{ form_widget(form) }}
|
{{ form_widget(form) }}
|
||||||
<button type="submit" class="btn btn-primary">Enregistrer</button>
|
<button type="submit" class="btn btn-primary">Enregistrer</button>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,13 @@
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="w-100 h-100 p-5 m-auto">
|
<div class="w-100 h-100 p-5 m-auto">
|
||||||
|
{% for type, messages in app.flashes %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{{ type }}">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
<div class="col d-flex justify-content-between align-items-center">
|
<div class="col d-flex justify-content-between align-items-center">
|
||||||
<div class="d-flex ">
|
<div class="d-flex ">
|
||||||
{% if organization.logoUrl %}
|
{% if organization.logoUrl %}
|
||||||
|
|
@ -49,7 +56,8 @@
|
||||||
<h2>
|
<h2>
|
||||||
Nouveaux utilisateurs
|
Nouveaux utilisateurs
|
||||||
</h2>
|
</h2>
|
||||||
<a href="{{ path('user_new', {'organizationId': organization.id}) }}" class="btn btn-primary">Ajouter un utilisateur</a>
|
<a href="{{ path('user_new', {'organizationId': organization.id}) }}"
|
||||||
|
class="btn btn-primary">Ajouter un utilisateur</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div id="tabulator-userListSmall" data-controller="user"
|
<div id="tabulator-userListSmall" data-controller="user"
|
||||||
|
|
@ -106,14 +114,34 @@
|
||||||
</div>
|
</div>
|
||||||
{# Activities col #}
|
{# Activities col #}
|
||||||
<div class="col-3 m-auto">
|
<div class="col-3 m-auto">
|
||||||
{% include 'organization/activity.html.twig' with {
|
<div class="card border-0"
|
||||||
title: 'Activités récentes',
|
data-controller="organization"
|
||||||
empty_message: 'Aucune activité récente.'
|
data-organization-activities-value = "true"
|
||||||
} %}
|
data-organization-id-value="{{ organization.id }}">
|
||||||
|
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center border-0">
|
||||||
|
<h3>Activité récente</h3>
|
||||||
|
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" data-action="organization#loadActivities">
|
||||||
|
<i class="fas fa-sync"></i> Rafraîchir
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body bg-light">
|
||||||
|
<div class="d-flex flex-column" data-organization-target="activityList">
|
||||||
|
<div class="text-center text-muted p-5">
|
||||||
|
<span class="spinner-border" aria-hidden="true"></span>
|
||||||
</div>
|
</div>
|
||||||
{# Ne pas enlever le 2ème /div#}
|
</div>
|
||||||
|
|
||||||
|
{# Empty state #}
|
||||||
|
<div class="d-none" data-organization-target="emptyMessage">
|
||||||
|
<div class="alert alert-light text-center shadow-sm">Aucune activité récente.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,9 @@
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<h2>Modifier l'utilisateur</h2>
|
<h2>Modifier l'utilisateur</h2>
|
||||||
</div>
|
</div>
|
||||||
|
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
||||||
<a href="{{ path('user_delete', {'id': user.id}) }}" class="btn btn-danger m-3">Supprimer</a>
|
<a href="{{ path('user_delete', {'id': user.id}) }}" class="btn btn-danger m-3">Supprimer</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,18 @@
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
||||||
<div class="w-100 h-100 p-5 m-auto">
|
<div class="w-100 h-100 p-5 m-auto">
|
||||||
|
{% for type, messages in app.flashes %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{{ type }}">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
<div class="card p-3 m-3 border-0">
|
<div class="card p-3 m-3 border-0">
|
||||||
<div class="card-header border-0">
|
<div class="card-header border-0">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3 ">
|
<div class="d-flex justify-content-between align-items-center mb-3 ">
|
||||||
<h1>Gestion Utilisateurs</h1>
|
<h1>Gestion Utilisateurs</h1>
|
||||||
<a href="{{ path('user_new') }}" class="btn btn-primary">Ajouter un utilisateur</a>
|
{# <a href="{{ path('user_new') }}" class="btn btn-primary">Ajouter un utilisateur</a>#}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
{% block body %}
|
|
||||||
|
|
||||||
<div class="card border">
|
|
||||||
<div class="card-header d-flex align-items-center border-0">
|
|
||||||
<div class="row align-items-center">
|
|
||||||
|
|
||||||
<h4 class="mb-0">
|
|
||||||
<span style="display:inline-block; width:16px; height:16px; border-radius:50%; background:{{ color }}; margin-right:10px;"></span>
|
|
||||||
{{ activityTime|ago }}</h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<p>{{ userName }} - {{ action }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
|
|
@ -4,6 +4,13 @@
|
||||||
|
|
||||||
<div class="w-100 h-100 p-5 m-auto">
|
<div class="w-100 h-100 p-5 m-auto">
|
||||||
<div class="card p-3 m-3 border-0 no-header-bg">
|
<div class="card p-3 m-3 border-0 no-header-bg">
|
||||||
|
{% for type, messages in app.flashes %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{{ type }}">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
{% if is_granted("ROLE_ADMIN") %}
|
{% if is_granted("ROLE_ADMIN") %}
|
||||||
<div class="card-header border-0 d-flex justify-content-between align-items-center ">
|
<div class="card-header border-0 d-flex justify-content-between align-items-center ">
|
||||||
|
|
@ -14,13 +21,6 @@
|
||||||
{% if is_granted("ROLE_SUPER_ADMIN") %}
|
{% if is_granted("ROLE_SUPER_ADMIN") %}
|
||||||
<a href="{{ path('user_delete', {'id': user.id}) }}"
|
<a href="{{ path('user_delete', {'id': user.id}) }}"
|
||||||
class="btn btn-secondary">Supprimer</a>
|
class="btn btn-secondary">Supprimer</a>
|
||||||
{% if user.active %}
|
|
||||||
<a href="{{ path('user_deactivate', {'id': user.id}) }}"
|
|
||||||
class="btn btn-secondary">Désactiver l'utilisateur</a>
|
|
||||||
{% else %}
|
|
||||||
<a href="{{ path('user_activate', {'id': user.id}) }}" class="btn btn-primary ">Activer
|
|
||||||
l'utilisateur</a>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -13,28 +13,11 @@
|
||||||
|
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
{% if canEdit %}
|
{% if canEdit %}
|
||||||
{% if organizationId is not null %}
|
|
||||||
{% if uoActive %}
|
|
||||||
<form method="post" action="{{ path('user_deactivate_organization', {'id': user.id}) }}"
|
|
||||||
onsubmit="return confirm('Vous allez retirer l\'utilisateur de cette organisation, êtes vous sûre?');">
|
|
||||||
<input type="hidden" name="organizationId" value="{{ organizationId }}">
|
|
||||||
<button class="btn btn-secondary" type="submit">Désactiver l'utilisateur de
|
|
||||||
l'organisation
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
{% else %}
|
|
||||||
<form method="post" action="{{ path('user_activate_organization', {'id': user.id}) }}"
|
|
||||||
onsubmit="return confirm('Vous allez activer cette utilisateur dans votre organisation, êtes vous sûre?');">
|
|
||||||
<input type="hidden" name="organizationId" value="{{ organizationId }}">
|
|
||||||
<button class="btn btn-primary" type="submit">Activer l'utilisateur de l'organisation
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
|
|
||||||
<a href="{{ path('user_edit', {'id': user.id, 'organizationId': organizationId}) }}"
|
<a href="{{ path('user_edit', {'id': user.id, 'organizationId': organizationId}) }}"
|
||||||
class="btn btn-primary">Modifier</a>
|
class="btn btn-primary">Modifier</a>
|
||||||
|
{% elseif user.id == app.user.id or is_granted("ROLE_SUPER_ADMIN") %}
|
||||||
|
<a href="{{ path('user_edit', {'id': user.id}) }}"
|
||||||
|
class="btn btn-primary">Modifier</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Controller;
|
||||||
|
|
||||||
|
use App\Entity\Actions;
|
||||||
|
use App\Entity\Organizations;
|
||||||
|
use App\Entity\User;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
|
||||||
|
class ActionControllerTest extends WebTestCase
|
||||||
|
{
|
||||||
|
private KernelBrowser $client;
|
||||||
|
private EntityManagerInterface $entityManager;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->client = static::createClient();
|
||||||
|
|
||||||
|
// Retrieve the EntityManager from the test container
|
||||||
|
$this->entityManager = static::getContainer()->get('doctrine')->getManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to create a valid User entity with all required fields and log them in.
|
||||||
|
*/
|
||||||
|
private function authenticateUser(): User
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
$user->setEmail('test_' . uniqid() . '@example.com'); // Ensure uniqueness
|
||||||
|
$user->setPassword('secure_password');
|
||||||
|
$user->setName('Test');
|
||||||
|
$user->setSurname('User');
|
||||||
|
$user->setRoles(['ROLE_USER']);
|
||||||
|
|
||||||
|
// Defaults (isActive, isDeleted, dates) are handled by the User constructor
|
||||||
|
|
||||||
|
$this->entityManager->persist($user);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$this->client->loginUser($user);
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function fetch_activities_ajax_returns_json_response(): void
|
||||||
|
{
|
||||||
|
// 1. Arrange: Authenticate
|
||||||
|
$user = $this->authenticateUser();
|
||||||
|
|
||||||
|
// 2. Arrange: Create a valid Organization
|
||||||
|
$organization = new Organizations();
|
||||||
|
$organization->setName('Test Corp');
|
||||||
|
$organization->setEmail('contact@testcorp.com');
|
||||||
|
$organization->setNumber(101); // Required int
|
||||||
|
$organization->setAddress('123 Main St'); // Required string
|
||||||
|
$organization->setLogoUrl('logo.png'); // Required string
|
||||||
|
// Defaults (isActive, isDeleted, collections) handled by Constructor
|
||||||
|
|
||||||
|
$this->entityManager->persist($organization);
|
||||||
|
|
||||||
|
// 3. Arrange: Create an Action linked to the Organization
|
||||||
|
$action = new Actions();
|
||||||
|
$action->setOrganization($organization); // Link to the org
|
||||||
|
$action->setUsers($user); // Link to the user
|
||||||
|
$action->setActionType('UPDATE'); // Required string
|
||||||
|
$action->setDescription('Updated profile details');
|
||||||
|
// Date is set automatically in __construct
|
||||||
|
|
||||||
|
$this->entityManager->persist($action);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
// 4. Act: Request the URL using the Organization ID
|
||||||
|
$url = sprintf('/actions/organization/%d/activities-ajax', $organization->getId());
|
||||||
|
$this->client->request('GET', $url);
|
||||||
|
|
||||||
|
// 5. Assert: Verify Success
|
||||||
|
$this->assertResponseIsSuccessful(); // Status 200
|
||||||
|
$this->assertResponseHeaderSame('content-type', 'application/json');
|
||||||
|
|
||||||
|
// 6. Assert: Verify JSON Content
|
||||||
|
$responseContent = $this->client->getResponse()->getContent();
|
||||||
|
$this->assertJson($responseContent);
|
||||||
|
|
||||||
|
$data = json_decode($responseContent, true);
|
||||||
|
|
||||||
|
// Since we created 1 action, we expect the array to be non-empty
|
||||||
|
$this->assertIsArray($data);
|
||||||
|
$this->assertNotEmpty($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function fetch_activities_returns_404_for_invalid_organization(): void
|
||||||
|
{
|
||||||
|
$this->authenticateUser();
|
||||||
|
|
||||||
|
// Act: Request with an ID that definitely doesn't exist (e.g., extremely high int)
|
||||||
|
$this->client->request('GET', '/actions/organization/99999999/activities-ajax');
|
||||||
|
|
||||||
|
// Assert: 404 Not Found (Standard Symfony ParamConverter behavior)
|
||||||
|
$this->assertResponseStatusCodeSame(404);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,301 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Controller;
|
||||||
|
|
||||||
|
use App\Entity\Apps;
|
||||||
|
use App\Entity\Organizations;
|
||||||
|
use App\Service\ActionService;
|
||||||
|
use App\Service\LoggerService;
|
||||||
|
use App\Tests\Functional\AbstractFunctionalTest;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
|
||||||
|
class ApplicationControllerTest extends AbstractFunctionalTest
|
||||||
|
{
|
||||||
|
|
||||||
|
//region Index Tests
|
||||||
|
#[Test]
|
||||||
|
public function index_redirects_unauthenticated_user(): void
|
||||||
|
{
|
||||||
|
$this->client->request('GET', '/application/');
|
||||||
|
self::assertResponseRedirects('/login'); // Assuming your login route is /login
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function index_lists_applications_for_authenticated_user(): void
|
||||||
|
{
|
||||||
|
// 1. Arrange: Create User and Data
|
||||||
|
$user = $this->createUser('user@test.com');
|
||||||
|
$this->createApp('App One');
|
||||||
|
$this->createApp('App Two');
|
||||||
|
|
||||||
|
// 2. Act: Login and Request
|
||||||
|
$this->client->loginUser($user);
|
||||||
|
$this->client->request('GET', '/application/');
|
||||||
|
|
||||||
|
// 3. Assert
|
||||||
|
self::assertResponseIsSuccessful();
|
||||||
|
self::assertSelectorTextContains('body', 'App One');
|
||||||
|
self::assertSelectorTextContains('body', 'App Two');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function index_no_application_found(): void
|
||||||
|
{
|
||||||
|
$user = $this->createUser('user@test.com');
|
||||||
|
$this->client->loginUser($user);
|
||||||
|
$this->client->request('GET', '/application/');
|
||||||
|
|
||||||
|
self::assertResponseIsSuccessful();
|
||||||
|
self::assertSelectorTextContains('body', 'Aucune application disponible');
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region Edit Tests
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function edit_page_denies_access_to_regular_users(): void
|
||||||
|
{
|
||||||
|
$user = $this->createUser('regular@test.com');
|
||||||
|
$app = $this->createApp('Target App');
|
||||||
|
|
||||||
|
$this->client->loginUser($user);
|
||||||
|
$this->client->request('GET', '/application/edit/' . $app->getId());
|
||||||
|
|
||||||
|
self::assertResponseStatusCodeSame(403);
|
||||||
|
}
|
||||||
|
#[Test]
|
||||||
|
public function edit_page_denies_access_to_admin_users(): void
|
||||||
|
{
|
||||||
|
$user = $this->createUser('admin@test.com', ['ROLE_ADMIN']);
|
||||||
|
$app = $this->createApp('Target App');
|
||||||
|
|
||||||
|
$this->client->loginUser($user);
|
||||||
|
$this->client->request('GET', '/application/edit/' . $app->getId());
|
||||||
|
|
||||||
|
self::assertResponseStatusCodeSame(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function edit_page_loads_for_super_admin(): void
|
||||||
|
{
|
||||||
|
$admin = $this->createUser('admin@test.com', ['ROLE_SUPER_ADMIN']);
|
||||||
|
$app = $this->createApp('Editable App');
|
||||||
|
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
$crawler = $this->client->request('GET', '/application/edit/' . $app->getId());
|
||||||
|
|
||||||
|
self::assertResponseIsSuccessful();
|
||||||
|
$this->assertCount(1, $crawler->filter('input[name="name"]'));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function edit_submits_changes_successfully(): void
|
||||||
|
{
|
||||||
|
$admin = $this->createUser('admin@test.com', ['ROLE_SUPER_ADMIN']);
|
||||||
|
$app = $this->createApp('Old Name');
|
||||||
|
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
|
||||||
|
// Simulate POST request directly (mimicking form submission)
|
||||||
|
$this->client->request('POST', '/application/edit/' . $app->getId(), [
|
||||||
|
'name' => 'New Name',
|
||||||
|
'description' => 'Updated Description',
|
||||||
|
'descriptionSmall' => 'Updated Small',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Assert Redirection
|
||||||
|
self::assertResponseRedirects('/application/');
|
||||||
|
$this->client->followRedirect();
|
||||||
|
|
||||||
|
// Assert Database Update
|
||||||
|
$this->entityManager->clear(); // Clear identity map to force fresh fetch
|
||||||
|
$updatedApp = $this->entityManager->getRepository(Apps::class)->find($app->getId());
|
||||||
|
$this->assertEquals('New Name', $updatedApp->getName());
|
||||||
|
$this->assertEquals('Updated Description', $updatedApp->getDescription());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function edit_handles_non_existent_id_get(): void
|
||||||
|
{
|
||||||
|
$admin = $this->createUser('admin@test.com', ['ROLE_SUPER_ADMIN']);
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
|
||||||
|
$this->client->request('GET', '/application/edit/999999');
|
||||||
|
|
||||||
|
self::assertResponseRedirects('/application/');
|
||||||
|
$this->client->followRedirect();
|
||||||
|
self::assertSelectorExists('.alert-danger');
|
||||||
|
|
||||||
|
self::assertSelectorTextContains('.alert-danger', "n'existe pas");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function edit_handles_non_existent_id_post(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$admin = $this->createUser('superAdmin@test.com', ['ROLE_SUPER_ADMIN']);
|
||||||
|
$app = $this->createApp('App With Issue');
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
|
||||||
|
$this->client->request('POST', '/application/edit/' . 99999, [
|
||||||
|
'name' => 'New Name',
|
||||||
|
'description' => 'Updated Description',
|
||||||
|
'descriptionSmall' => 'Updated Small',
|
||||||
|
]);
|
||||||
|
|
||||||
|
self::assertResponseRedirects('/application/');
|
||||||
|
$this->client->followRedirect();
|
||||||
|
self::assertSelectorExists('.alert-danger');
|
||||||
|
self::assertSelectorTextContains('.alert-danger', "n'existe pas");
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region Authorize Tests
|
||||||
|
#[Test]
|
||||||
|
public function authorize_adds_organization_successfully(): void
|
||||||
|
{
|
||||||
|
$admin = $this->createUser('admin@test.com', ['ROLE_SUPER_ADMIN']);
|
||||||
|
$app = $this->createApp('Auth App');
|
||||||
|
$org = $this->createOrganization('Test Org');
|
||||||
|
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
|
||||||
|
$this->client->request('POST', '/application/authorize/' . $app->getId(), [
|
||||||
|
'organizationId' => $org->getId()
|
||||||
|
]);
|
||||||
|
|
||||||
|
self::assertResponseStatusCodeSame(200);
|
||||||
|
|
||||||
|
// Clear Doctrine memory to force fetching fresh data from DB
|
||||||
|
$this->entityManager->clear();
|
||||||
|
|
||||||
|
$updatedApp = $this->entityManager->getRepository(Apps::class)->find($app->getId());
|
||||||
|
|
||||||
|
$exists = $updatedApp->getOrganization()->exists(function($key, $element) use ($org) {
|
||||||
|
return $element->getId() === $org->getId();
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->assertTrue($exists, 'The application is not linked to the organization.');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function authorize_fails_on_invalid_organization(): void
|
||||||
|
{
|
||||||
|
$admin = $this->createUser('sAdmin@test.com', ['ROLE_SUPER_ADMIN']);
|
||||||
|
$app = $this->createApp('App For Org Test');
|
||||||
|
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
$this->client->request('POST', '/application/authorize/' . $app->getId(), [
|
||||||
|
'organizationId' => 99999
|
||||||
|
]);
|
||||||
|
|
||||||
|
self::assertResponseStatusCodeSame(404);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function authorize_fails_on_invalid_application(): void
|
||||||
|
{
|
||||||
|
$admin = $this->createUser('sAdmin@test.com', ['ROLE_SUPER_ADMIN']);
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
$this->client->request('POST', '/application/authorize/99999', [
|
||||||
|
'organizationId' => 1
|
||||||
|
]);
|
||||||
|
self::assertResponseStatusCodeSame(404);
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region revoke Tests
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function revoke_denies_access_to_admins(): void
|
||||||
|
{
|
||||||
|
$user = $this->createUser('Admin@test.com', ['ROLE_ADMIN']);
|
||||||
|
$app = $this->createApp('App To Revoke');
|
||||||
|
$org = $this->createOrganization('Org To Revoke');
|
||||||
|
$this->client->loginUser($user);
|
||||||
|
$this->client->request('POST', '/application/revoke/'. $app->getId(), [
|
||||||
|
'organizationId' => $org->getId()
|
||||||
|
]);
|
||||||
|
self::assertResponseStatusCodeSame(403);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function revoke_denies_access_to_user(): void
|
||||||
|
{
|
||||||
|
$user = $this->createUser('user@test.com');
|
||||||
|
$app = $this->createApp('App To Revoke');
|
||||||
|
$org = $this->createOrganization('Org To Revoke');
|
||||||
|
$this->client->loginUser($user);
|
||||||
|
$this->client->request('POST', '/application/revoke/'. $app->getId(), [
|
||||||
|
'organizationId' => $org->getId()
|
||||||
|
]);
|
||||||
|
self::assertResponseStatusCodeSame(403);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function revoke_removes_organization_successfully(): void
|
||||||
|
{
|
||||||
|
$admin = $this->createUser('sAdmin@test.com', ['ROLE_SUPER_ADMIN']);
|
||||||
|
$app = $this->createApp('App To Revoke Org');
|
||||||
|
$org = $this->createOrganization('Org To Be Revoked');
|
||||||
|
// First, authorize the organization
|
||||||
|
$app->addOrganization($org);
|
||||||
|
$this->entityManager->persist($app);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
$this->client->request('POST', '/application/revoke/'. $app->getId(), [
|
||||||
|
'organizationId' => $org->getId()
|
||||||
|
]);
|
||||||
|
self::assertResponseStatusCodeSame(200);
|
||||||
|
|
||||||
|
// Clear Doctrine memory to force fetching fresh data from DB
|
||||||
|
$this->entityManager->clear();
|
||||||
|
$updatedApp = $this->entityManager->getRepository(Apps::class)->find($app->getId());
|
||||||
|
$exists = $updatedApp->getOrganization()->exists(function($key, $element) use ($org) {
|
||||||
|
return $element === $org;
|
||||||
|
});
|
||||||
|
self::assertFalse($exists, 'The organization was removed from the application.');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function revoke_fails_on_invalid_organization(): void
|
||||||
|
{
|
||||||
|
$admin = $this->createUser('sAdmin@test.com', ['ROLE_SUPER_ADMIN']);
|
||||||
|
$app = $this->createApp('App To Revoke Org');
|
||||||
|
$org = $this->createOrganization('Org To Be Revoked');
|
||||||
|
// First, authorize the organization
|
||||||
|
$app->addOrganization($org);
|
||||||
|
$this->entityManager->persist($app);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
$this->client->request('POST', '/application/revoke/' . $app->
|
||||||
|
getId(), [
|
||||||
|
'organizationId' => 99999
|
||||||
|
]);
|
||||||
|
self::assertResponseStatusCodeSame(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function revoke_fails_on_invalid_application(): void
|
||||||
|
{
|
||||||
|
$admin = $this->createUser('sAdmin@test.com', ['ROLE_SUPER_ADMIN']);
|
||||||
|
$org = $this->createOrganization('Org To Be Revoked');
|
||||||
|
// First, authorize the organization
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
$this->client->request('POST', '/application/revoke/' . 9999, [
|
||||||
|
'organizationId' => 99999
|
||||||
|
]);
|
||||||
|
self::assertResponseStatusCodeSame(404, "L'application n'existe pas.");
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Controller;
|
||||||
|
|
||||||
|
use App\Tests\Functional\AbstractFunctionalTest;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
|
||||||
|
class IndexControllerTest extends AbstractFunctionalTest
|
||||||
|
{
|
||||||
|
|
||||||
|
//Region dashboard tests
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region index tests
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_index_successful_super_admin(): void
|
||||||
|
{
|
||||||
|
$admin = $this->createUser('sAdmin@test.com', ['ROLE_SUPER_ADMIN']);
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
$this->client->request('GET', '/');
|
||||||
|
|
||||||
|
self::assertResponseRedirects('/organization/');
|
||||||
|
$this->client->followRedirect();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_index_successful_admin_single_org(): void
|
||||||
|
{
|
||||||
|
$admin = $this->createUser('admin@test.com', ['ROLE_ADMIN']);
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
$org = $this->createOrganization('Test Org');
|
||||||
|
$app = $this->createApp('Test App');
|
||||||
|
$app -> addOrganization($org);
|
||||||
|
$uo = $this->createUOLink($admin, $org);
|
||||||
|
$role = $this->createRole('ADMIN');
|
||||||
|
$uoa = $this->createUOALink($uo , $app, $role);
|
||||||
|
|
||||||
|
$this->client->request('GET', '/');
|
||||||
|
self::assertResponseRedirects('/organization/');
|
||||||
|
|
||||||
|
|
||||||
|
$this->client->followRedirect();
|
||||||
|
|
||||||
|
|
||||||
|
self::assertResponseRedirects('/organization/view/' . $org->getId());
|
||||||
|
|
||||||
|
|
||||||
|
$this->client->followRedirect();
|
||||||
|
self::assertResponseIsSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_index_successful_admin_mutiple_org(): void
|
||||||
|
{
|
||||||
|
$admin = $this->createUser('admin@test.com', ['ROLE_ADMIN']);
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
$org = $this->createOrganization('Test Org');
|
||||||
|
$org2 = $this->createOrganization('Test Org2');
|
||||||
|
$app = $this->createApp('Test App');
|
||||||
|
$app -> addOrganization($org);
|
||||||
|
$uo = $this->createUOLink($admin, $org);
|
||||||
|
$uo2 = $this->createUOLink($admin, $org2);
|
||||||
|
$role = $this->createRole('ADMIN');
|
||||||
|
$uoa = $this->createUOALink($uo , $app, $role);
|
||||||
|
$uoa2 = $this->createUOALink($uo2 , $app, $role);
|
||||||
|
|
||||||
|
$this->client->request('GET', '/');
|
||||||
|
self::assertResponseRedirects('/organization/');
|
||||||
|
$this->client->followRedirect();
|
||||||
|
self::assertResponseIsSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_index_successful_user(): void
|
||||||
|
{
|
||||||
|
$user = $this->createUser('user@test.com', ['ROLE_USER']);
|
||||||
|
$this->client->loginUser($user);
|
||||||
|
$org = $this->createOrganization('Test Org');
|
||||||
|
$app = $this->createApp('Test App');
|
||||||
|
$app -> addOrganization($org);
|
||||||
|
$uo = $this->createUOLink($user, $org);
|
||||||
|
$role = $this->createRole('USER');
|
||||||
|
$uoa = $this->createUOALink($uo , $app, $role);
|
||||||
|
$this->client->request('GET', '/');
|
||||||
|
self::assertResponseRedirects('/application/');
|
||||||
|
$this->client->followRedirect();
|
||||||
|
self::assertResponseIsSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_index_unauthenticated(): void
|
||||||
|
{
|
||||||
|
$this->client->request('GET', '/');
|
||||||
|
self::assertResponseRedirects('/login');
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Controller;
|
||||||
|
|
||||||
|
use App\Tests\Functional\AbstractFunctionalTest;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationControllerTest extends AbstractFunctionalTest{
|
||||||
|
|
||||||
|
//region index tests
|
||||||
|
#[Test]
|
||||||
|
public function test_index_super_admin_success(): void
|
||||||
|
{
|
||||||
|
$admin = $this->createUser('admin@test.com', ['ROLE_SUPER_ADMIN']);
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
$this->createNotification($admin, 'Test Notification 1');
|
||||||
|
$this->createNotification($admin, 'Test Notification 2', true);
|
||||||
|
$this->client->request('GET', '/notifications/');
|
||||||
|
self::assertResponseIsSuccessful();
|
||||||
|
$responseData = json_decode($this->client->getResponse()->getContent(), true);
|
||||||
|
$this->assertArrayHasKey('notifications', $responseData);
|
||||||
|
$this->assertArrayHasKey('unreadCount', $responseData);
|
||||||
|
$this->assertCount(2, $responseData['notifications']);
|
||||||
|
$this->assertEquals(1, $responseData['unreadCount']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_index_non_super_admin_forbidden(): void
|
||||||
|
{
|
||||||
|
$admin = $this->createUser('admin@test.com', ['ROLE_ADMIN']);
|
||||||
|
$user = $this->createUser('user@test.com', ['ROLE_USER']);
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
$this->client->request('GET', '/notifications/');
|
||||||
|
self::assertResponseStatusCodeSame(403);
|
||||||
|
$this->client->loginUser($user);
|
||||||
|
$this->client->request('GET', '/notifications/');
|
||||||
|
self::assertResponseStatusCodeSame(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region unread tests
|
||||||
|
#[Test]
|
||||||
|
public function test_unread_authenticated_user_success(): void
|
||||||
|
{
|
||||||
|
$user = $this->createUser('s', ['ROLE_SUPER_ADMIN']);
|
||||||
|
$this->client->loginUser($user);
|
||||||
|
$this->createNotification($user, 'Unread Notification 1');
|
||||||
|
$this->createNotification($user, 'Read Notification 1', true);
|
||||||
|
$this->client->request('GET', '/notifications/unread');
|
||||||
|
self::assertResponseIsSuccessful();
|
||||||
|
$responseData = json_decode($this->client->getResponse()->getContent(), true);
|
||||||
|
$this->assertArrayHasKey('notifications', $responseData);
|
||||||
|
$this->assertArrayHasKey('unreadCount', $responseData);
|
||||||
|
$this->assertCount(1, $responseData['notifications']);
|
||||||
|
$this->assertEquals(1, $responseData['unreadCount']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_unread_unauthenticated_user_forbidden(): void
|
||||||
|
{
|
||||||
|
$this->client->request('GET', '/notifications/unread');
|
||||||
|
self::assertResponseStatusCodeSame(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region markAsRead tests
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_markAsRead_authenticated_user_success(): void
|
||||||
|
{
|
||||||
|
$user = $this->createUser('user');
|
||||||
|
$this->client->loginUser($user);
|
||||||
|
$notification = $this->createNotification($user, 'Notification to Mark Read');
|
||||||
|
$this->client->request('POST', '/notifications/' . $notification->getId() . '/read');
|
||||||
|
self::assertResponseIsSuccessful();
|
||||||
|
$responseData = json_decode($this->client->getResponse()->getContent(), true);
|
||||||
|
$this->assertArrayHasKey('success', $responseData);
|
||||||
|
$this->assertTrue($responseData['success']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_markAsRead_notification_not_found(): void
|
||||||
|
{
|
||||||
|
$user = $this->createUser('user');
|
||||||
|
$this->client->loginUser($user);
|
||||||
|
$notification = $this->createNotification($user, 'Notification to Mark Read');
|
||||||
|
$this->client->request('POST', '/notifications/9999/read'); // Non-existent ID
|
||||||
|
self::assertResponseStatusCodeSame(404);
|
||||||
|
$responseData = json_decode($this->client->getResponse()->getContent(), true);
|
||||||
|
$this->assertArrayHasKey('error', $responseData);
|
||||||
|
$this->assertEquals('Notification not found', $responseData['error']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_markAsRead_unauthenticated_user_forbidden(): void
|
||||||
|
{
|
||||||
|
$this->client->request('POST', '/notifications/1/read');
|
||||||
|
self::assertResponseRedirects('/login');
|
||||||
|
$this->client->followRedirect();
|
||||||
|
self::assertResponseStatusCodeSame(200); // Login page
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,359 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Controller;
|
||||||
|
|
||||||
|
use App\Entity\Apps;
|
||||||
|
use App\Entity\Organizations;
|
||||||
|
use App\Entity\Roles;
|
||||||
|
use App\Entity\UserOrganizatonApp;
|
||||||
|
use App\Entity\UsersOrganizations;
|
||||||
|
use App\Service\AwsService;
|
||||||
|
use App\Tests\Functional\AbstractFunctionalTest;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||||
|
|
||||||
|
class OrganizationControllerTest extends AbstractFunctionalTest
|
||||||
|
{
|
||||||
|
|
||||||
|
//region INDEX tests
|
||||||
|
#[Test]
|
||||||
|
public function test_index_super_admin_success(): void
|
||||||
|
{
|
||||||
|
// 1. Arrange
|
||||||
|
$admin = $this->createUser('sAdmin@test.com', ['ROLE_SUPER_ADMIN']);
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
|
||||||
|
// Create at least one org so 'hasOrganizations' becomes true
|
||||||
|
$this->createOrganization('Organization 1');
|
||||||
|
$this->createOrganization('Organization 2');
|
||||||
|
|
||||||
|
$this->client->request('GET', '/organization/');
|
||||||
|
|
||||||
|
self::assertResponseIsSuccessful();
|
||||||
|
self::assertSelectorTextNotContains('body', 'Aucune organisation trouvée');
|
||||||
|
|
||||||
|
self::assertSelectorExists('#tabulator-org');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_index_regular_user_forbidden(): void
|
||||||
|
{
|
||||||
|
// 1. Arrange
|
||||||
|
$user = $this->createUser('user@mail.com');
|
||||||
|
$this->client->loginUser($user);
|
||||||
|
|
||||||
|
// 2. Act
|
||||||
|
$this->client->request('GET', '/organization/');
|
||||||
|
// 3. Assert
|
||||||
|
self::assertResponseStatusCodeSame(403);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_index_no_organizations(): void
|
||||||
|
{
|
||||||
|
// 1. Arrange
|
||||||
|
$admin = $this->createUser('user@mail.com', ['ROLE_SUPER_ADMIN']);
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
// 2. Act
|
||||||
|
$this->client->request('GET', '/organization/');
|
||||||
|
// 3. Assert
|
||||||
|
self::assertResponseIsSuccessful();
|
||||||
|
self::assertSelectorTextContains('body', 'Aucune organisation trouvée');
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region CREATE tests
|
||||||
|
#[Test]
|
||||||
|
public function test_create_super_admin_success(): void
|
||||||
|
{
|
||||||
|
// 1. Arrange: Disable reboot to keep our AWS mock alive
|
||||||
|
$this->client->disableReboot();
|
||||||
|
|
||||||
|
$admin = $this->createUser('admin@user.com', ['ROLE_SUPER_ADMIN']);
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
|
||||||
|
// 2. MOCK AWS Service (Crucial!)
|
||||||
|
// Your code calls $awsService->PutDocObj, so we must intercept that.
|
||||||
|
// 2. MOCK AWS Service
|
||||||
|
$awsMock = $this->createMock(AwsService::class);
|
||||||
|
$awsMock->expects($this->any())
|
||||||
|
->method('PutDocObj')
|
||||||
|
->willReturn(1); // <--- FIXED: Return an integer, not a boolean
|
||||||
|
|
||||||
|
// Inject the mock into the test container
|
||||||
|
static::getContainer()->set(AwsService::class, $awsMock);
|
||||||
|
|
||||||
|
// 3. Create a Dummy Image File
|
||||||
|
$tempFile = tempnam(sys_get_temp_dir(), 'test_logo');
|
||||||
|
file_put_contents($tempFile, 'fake image content'); // Create a dummy file
|
||||||
|
|
||||||
|
$logo = new UploadedFile(
|
||||||
|
$tempFile,
|
||||||
|
'logo.png',
|
||||||
|
'image/png',
|
||||||
|
null,
|
||||||
|
true // 'test' mode = true
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Act: Request the page
|
||||||
|
$this->client->request('GET', '/organization/create');
|
||||||
|
|
||||||
|
// 5. Submit Form with the FILE object and correct field name 'logoUrl'
|
||||||
|
$this->client->submitForm('Enregistrer', [
|
||||||
|
'organization_form[name]' => 'New Organization',
|
||||||
|
'organization_form[email]' => 'unique-' . uniqid('', true) . '@test.com',
|
||||||
|
'organization_form[address]' => '123 Test Street',
|
||||||
|
'organization_form[number]' => '0102030405',
|
||||||
|
'organization_form[logoUrl]' => $logo, // Pass the OBJECT, not a string
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 6. Assert
|
||||||
|
// Check for redirect (302)
|
||||||
|
self::assertResponseRedirects('/organization/');
|
||||||
|
|
||||||
|
$this->client->followRedirect();
|
||||||
|
|
||||||
|
// Ensure we see the success state
|
||||||
|
self::assertSelectorTextNotContains('body', 'Aucune organisation trouvée');
|
||||||
|
self::assertSelectorExists('#tabulator-org');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_create_regular_user_forbidden(): void
|
||||||
|
{
|
||||||
|
// 1. Arrange
|
||||||
|
$user = $this->createUser('user@email.com');
|
||||||
|
$this->client->loginUser($user);
|
||||||
|
// 2. Act
|
||||||
|
$this->client->request('GET', '/organization/create');
|
||||||
|
// 3. Assert
|
||||||
|
self::assertResponseStatusCodeSame(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_create_super_admin_invalid_data(): void
|
||||||
|
{
|
||||||
|
// 1. Arrange
|
||||||
|
$admin = $this->createUser('admin@email.com', ['ROLE_SUPER_ADMIN']);
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
// 2. Act
|
||||||
|
$this->client->request('GET', '/organization/create');
|
||||||
|
$this->client->submitForm('Enregistrer', [
|
||||||
|
'organization_form[name]' => '', // Invalid: name is required
|
||||||
|
'organization_form[email]' => 'not-an-email', // Invalid email format
|
||||||
|
'organization_form[address]' => '123 Test St',
|
||||||
|
'organization_form[number]' => '0102030405',
|
||||||
|
]);
|
||||||
|
// 3. Assert
|
||||||
|
self::assertResponseIsSuccessful(); // Form isn't redirected
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_create_super_admin_duplicate_email(): void
|
||||||
|
{
|
||||||
|
// 1. Arrange
|
||||||
|
$admin = $this->createUser('admin@email.com', ['ROLE_SUPER_ADMIN']);
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
$existingOrg = $this->createOrganization('Existing Org');
|
||||||
|
// 2. Act
|
||||||
|
$this->client->request('GET', '/organization/create');
|
||||||
|
$this->client->submitForm('Enregistrer', [
|
||||||
|
'organization_form[name]' => 'New Org',
|
||||||
|
'organization_form[email]' => $existingOrg->getEmail(), // Duplicate email
|
||||||
|
'organization_form[address]' => '123 Test St',
|
||||||
|
'organization_form[number]' => '0102030405',
|
||||||
|
]);
|
||||||
|
// 3. Assert
|
||||||
|
self::assertResponseIsSuccessful(); // Form isn't redirected
|
||||||
|
self::assertSelectorTextContains('body', 'Une organisation avec cet email existe déjà.');
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region EDIT tests
|
||||||
|
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_edit_super_admin_success(): void
|
||||||
|
{
|
||||||
|
// 1. Arrange: Disable reboot to keep our AWS mock alive
|
||||||
|
$this->client->disableReboot();
|
||||||
|
|
||||||
|
$admin = $this->createUser('admin@user.com', ['ROLE_SUPER_ADMIN']);
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
|
||||||
|
// 2. MOCK AWS Service (Crucial!)
|
||||||
|
// Your code calls $awsService->PutDocObj, so we must intercept that.
|
||||||
|
// 2. MOCK AWS Service
|
||||||
|
$awsMock = $this->createMock(AwsService::class);
|
||||||
|
$awsMock->expects($this->any())
|
||||||
|
->method('PutDocObj')
|
||||||
|
->willReturn(1); // <--- FIXED: Return an integer, not a boolean
|
||||||
|
|
||||||
|
// Inject the mock into the test container
|
||||||
|
static::getContainer()->set(AwsService::class, $awsMock);
|
||||||
|
|
||||||
|
// 3. Create a Dummy Image File
|
||||||
|
$tempFile = tempnam(sys_get_temp_dir(), 'test_logo');
|
||||||
|
file_put_contents($tempFile, 'fake image content'); // Create a dummy file
|
||||||
|
|
||||||
|
$logo = new UploadedFile(
|
||||||
|
$tempFile,
|
||||||
|
'logo.png',
|
||||||
|
'image/png',
|
||||||
|
null,
|
||||||
|
true // 'test' mode = true
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create an organization to edit
|
||||||
|
$organization = $this->createOrganization('Org to Edit');
|
||||||
|
// 4. Act: Request the edit page
|
||||||
|
$this->client->request('GET', '/organization/edit/' . $organization->getId());
|
||||||
|
// 5. Submit Form with the FILE object and correct field name 'logoUrl'
|
||||||
|
$this->client->submitForm('Enregistrer', [
|
||||||
|
'organization_form[name]' => 'Edited Organization',
|
||||||
|
'organization_form[email]' => 'edited-' . uniqid('', true) . '@test.com',
|
||||||
|
'organization_form[address]' => '456 Edited Street',
|
||||||
|
'organization_form[number]' => '0504030201',
|
||||||
|
'organization_form[logoUrl]' => $logo, // Pass the OBJECT, not a
|
||||||
|
]);
|
||||||
|
// 6. Assert
|
||||||
|
// Check for redirect (302)
|
||||||
|
self::assertResponseRedirects('/organization/');
|
||||||
|
$this->client->followRedirect();
|
||||||
|
// Ensure we see the success state
|
||||||
|
self::assertSelectorTextNotContains('body', 'Aucune organisation trouvée');
|
||||||
|
self::assertSelectorExists('#tabulator-org');
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_edit_regular_user_forbidden(): void
|
||||||
|
{
|
||||||
|
// 1. Arrange
|
||||||
|
$user = $this->createUser('user@email.com');
|
||||||
|
$this->client->loginUser($user);
|
||||||
|
// Create an organization to edit
|
||||||
|
$organization = $this->createOrganization('Org to Edit');
|
||||||
|
// 2. Act
|
||||||
|
$this->client->request('GET', '/organization/edit/' . $organization->getId());
|
||||||
|
// 3. Assert
|
||||||
|
self::assertResponseStatusCodeSame(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_edit_super_admin_invalid_data(): void
|
||||||
|
{
|
||||||
|
// 1. Arrange
|
||||||
|
$admin = $this->createUser('admin@mail.com', ['ROLE_SUPER_ADMIN']);
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
// Create an organization to edit
|
||||||
|
$organization = $this->createOrganization('Org to Edit');
|
||||||
|
// 2. Act
|
||||||
|
$this->client->request('GET', '/organization/edit/' . $organization->getId());
|
||||||
|
$this->client->submitForm('Enregistrer', [
|
||||||
|
'organization_form[name]' => '', // Invalid: name is required
|
||||||
|
'organization_form[email]' => 'not-an-email', // Invalid email format
|
||||||
|
'organization_form[address]' => '123 Test St',
|
||||||
|
'organization_form[number]' => '0102030405',
|
||||||
|
]);
|
||||||
|
// 3. Assert
|
||||||
|
self::assertResponseIsSuccessful(); // Form isn't redirected
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_edit_nonexistent_organization_not_found(): void
|
||||||
|
{
|
||||||
|
// 1. Arrange
|
||||||
|
$admin = $this->createUser('admin@mail.com', ['ROLE_SUPER_ADMIN']);
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
// 2. Act
|
||||||
|
$this->client->request('GET', '/organization/edit/99999'); // Assuming
|
||||||
|
// 3. Assert
|
||||||
|
self::assertResponseStatusCodeSame(302);
|
||||||
|
|
||||||
|
self::assertResponseRedirects('/organization/');
|
||||||
|
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
|
//region DELETE tests
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_delete_super_admin_success(): void
|
||||||
|
{
|
||||||
|
// 1. Arrange
|
||||||
|
$admin = $this->createUser('admin@email.com', ['ROLE_SUPER_ADMIN']);
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
$organization = $this->createOrganization('Org to Delete');
|
||||||
|
// 2. Act
|
||||||
|
$this->client->request('POST', '/organization/delete/' . $organization->getId());
|
||||||
|
// 3. Assert
|
||||||
|
self::assertResponseRedirects('/organization/');
|
||||||
|
$this->client->followRedirect();
|
||||||
|
self::assertSelectorTextNotContains('body', 'Org to Delete');
|
||||||
|
self::assertTrue($this->entityManager->getRepository(Organizations::class)->find($organization->getId())->isDeleted());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_delete_regular_user_forbidden(): void
|
||||||
|
{
|
||||||
|
// 1. Arrange
|
||||||
|
$user = $this->createUser('user@mail.com');
|
||||||
|
$this->client->loginUser($user);
|
||||||
|
$organization = $this->createOrganization('Org to Delete');
|
||||||
|
// 2. Act
|
||||||
|
$this->client->request('POST', '/organization/delete/' . $organization->getId());
|
||||||
|
// 3. Assert
|
||||||
|
self::assertResponseStatusCodeSame(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_delete_nonexistent_organization_not_found(): void
|
||||||
|
{
|
||||||
|
// 1. Arrange
|
||||||
|
$admin = $this->createUser('admin@user.com', ['ROLE_SUPER_ADMIN']);
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
// 2. Act
|
||||||
|
$this->client->request('POST', '/organization/delete/99999'); // Assuming
|
||||||
|
// 3. Assert
|
||||||
|
self::assertResponseStatusCodeSame(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_delete_organization_with_dependencies(): void
|
||||||
|
{
|
||||||
|
// 1. Arrange
|
||||||
|
$admin = $this->createUser('user@admin.com', ['ROLE_SUPER_ADMIN']);
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
$organization = $this->createOrganization('Org with Deps');
|
||||||
|
$app = $this->createApp('Dependent App');
|
||||||
|
$role = $this->createRole('ROLE_USER');
|
||||||
|
$uoLink = $this->createUOLink($admin, $organization);
|
||||||
|
$uoaLink = $this->createUOALink($uoLink, $app, $role);
|
||||||
|
// 2. Act
|
||||||
|
$this->client->request('POST', '/organization/delete/' . $organization->getId());
|
||||||
|
// 3. Assert
|
||||||
|
self::assertResponseRedirects('/organization/');
|
||||||
|
$this->client->followRedirect();
|
||||||
|
|
||||||
|
self::assertSelectorTextContains('body', 'Aucune organisation trouvée');
|
||||||
|
//link should be deactivated, not deleted
|
||||||
|
self::assertCount(1, $this->entityManager->getRepository(Apps::class)->findAll());
|
||||||
|
self::assertCount(1, $this->entityManager->getRepository(Roles::class)->findAll());
|
||||||
|
self::assertCount(1, $this->entityManager->getRepository(UsersOrganizations::class)->findAll());
|
||||||
|
self::assertCount(1, $this->entityManager->getRepository(UserOrganizatonApp::class)->findAll());
|
||||||
|
self::assertTrue($this->entityManager->getRepository(Organizations::class)->find($organization->getId())->isDeleted());
|
||||||
|
self::assertFalse($this->entityManager->getRepository(UserOrganizatonApp::class)->find($uoLink->getId())->isActive());
|
||||||
|
self::assertFalse($this->entityManager->getRepository(UserOrganizatonApp::class)->find($uoaLink->getId())->isActive());
|
||||||
|
self::assertSelectorNotExists('#tabulator-org');
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,604 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace App\Tests\Controller;
|
||||||
|
|
||||||
|
use App\Service\AwsService;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Entity\Apps;
|
||||||
|
use App\Entity\Roles;
|
||||||
|
use App\Entity\Organizations;
|
||||||
|
use App\Entity\UsersOrganizations;
|
||||||
|
use App\Entity\UserOrganizatonApp;
|
||||||
|
use App\Tests\Functional\AbstractFunctionalTest;
|
||||||
|
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||||
|
use function Symfony\Component\DependencyInjection\Loader\Configurator\param;
|
||||||
|
|
||||||
|
//This test will generate warning, ignore it
|
||||||
|
class UserControllerTest extends AbstractFunctionalTest
|
||||||
|
{
|
||||||
|
//region Index Tests
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_index_super_admin_success(): void
|
||||||
|
{
|
||||||
|
$admin = $this->createUser('admin@admin.com', ['ROLE_SUPER_ADMIN']);
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
|
||||||
|
$this->client->request('GET', '/user/');
|
||||||
|
self::assertResponseIsSuccessful();
|
||||||
|
self::assertSelectorTextNotContains('body', 'Aucun utilisateur trouvé');
|
||||||
|
self::assertSelectorExists('#tabulator-userList');
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_index_regular_user_forbidden(): void
|
||||||
|
{
|
||||||
|
// 1. Arrange
|
||||||
|
$user = $this->createUser('user@mail.com');
|
||||||
|
$this->client->loginUser($user);
|
||||||
|
|
||||||
|
// 2. Act
|
||||||
|
$this->client->request('GET', '/user/');
|
||||||
|
// 3. Assert
|
||||||
|
self::assertResponseStatusCodeSame(403);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//Can't test for no users as page is designed to always have at least one user (the logged in one)
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region Show Tests
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_view_super_admin(): void
|
||||||
|
{
|
||||||
|
$admin = $this->createUser('admin@admin', ['ROLE_SUPER_ADMIN']);
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
|
||||||
|
$role = $this->createRole('ADMIN');
|
||||||
|
$role2 = $this->createRole('EMPTY');
|
||||||
|
$app = $this->createApp('Test App');
|
||||||
|
$organization = $this->createOrganization('Test Org');
|
||||||
|
$uo = $this->createUOLink($admin, $organization);
|
||||||
|
$uoa = $this->createUOALink($uo, $app, $role);
|
||||||
|
|
||||||
|
$this->client->request('GET', '/user/view/' . $admin->getId());
|
||||||
|
self::assertResponseIsSuccessful();
|
||||||
|
self::assertSelectorTextContains('body', $admin->getEmail());
|
||||||
|
self::assertSelectorTextContains('body', $admin->getName());
|
||||||
|
self::assertSelectorTextContains('body', $app->getName());
|
||||||
|
self::assertSelectorTextContains('body', ucfirst(strtolower($role->getName())));
|
||||||
|
self::assertCheckboxChecked("roles[]", ucfirst(strtolower($role->getName())));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_view_regular_user_forbidden(): void
|
||||||
|
{
|
||||||
|
// 1. Arrange
|
||||||
|
$user = $this->createUser('user@email.com');
|
||||||
|
$user2 = $this->createUser('user2@email.com');
|
||||||
|
$this->client->loginUser($user);
|
||||||
|
// 2. Act
|
||||||
|
$this->client->request('GET', '/user/view/' . $user2->getId());
|
||||||
|
// 3. Assert
|
||||||
|
self::assertResponseStatusCodeSame(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_view_admin(): void
|
||||||
|
{
|
||||||
|
$admin = $this->createUser('admin@admin', ['ROLE_ADMIN']);
|
||||||
|
$user = $this->createUser('user@admin');
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
|
||||||
|
$role = $this->createRole('ADMIN');
|
||||||
|
$role2 = $this->createRole('USER');
|
||||||
|
$app = $this->createApp('Test App');
|
||||||
|
$organization = $this->createOrganization('Test Org');
|
||||||
|
$uo = $this->createUOLink($admin, $organization);
|
||||||
|
$uo2 = $this->createUOLink($user, $organization);
|
||||||
|
$uoa = $this->createUOALink($uo, $app, $role);
|
||||||
|
$uoa2 = $this->createUOALink($uo2, $app, $role2);
|
||||||
|
|
||||||
|
$this->client->request('GET', '/user/view/' . $user->getId() . '?organizationId=' . $organization->getId());
|
||||||
|
self::assertResponseIsSuccessful();
|
||||||
|
self::assertSelectorTextContains('body', $user->getEmail());
|
||||||
|
self::assertSelectorTextContains('body', $user->getName());
|
||||||
|
self::assertSelectorTextContains('body', $app->getName());
|
||||||
|
self::assertSelectorTextContains('body', ucfirst(strtolower($role->getName())));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_view_admin_different_organization_forbidden(): void
|
||||||
|
{
|
||||||
|
$admin = $this->createUser('admin@admin', ['ROLE_ADMIN']);
|
||||||
|
$user = $this->createUser('user@admin');
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
|
||||||
|
$role = $this->createRole('ADMIN');
|
||||||
|
$role2 = $this->createRole('USER');
|
||||||
|
$app = $this->createApp('Test App');
|
||||||
|
$organization = $this->createOrganization('Test Org');
|
||||||
|
$organization2 = $this->createOrganization('Test Org2');
|
||||||
|
$uo = $this->createUOLink($admin, $organization);
|
||||||
|
$uo2 = $this->createUOLink($user, $organization2);
|
||||||
|
$uoa = $this->createUOALink($uo, $app, $role);
|
||||||
|
$uoa2 = $this->createUOALink($uo2, $app, $role2);
|
||||||
|
|
||||||
|
$this->client->request('GET', '/user/view/' . $user->getId() . '?organizationId=' . $organization->getId());
|
||||||
|
self::assertResponseStatusCodeSame(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_view_user_self_success(): void
|
||||||
|
{
|
||||||
|
$user = $this->createUser('user@email.com');
|
||||||
|
$this->client->loginUser($user);
|
||||||
|
$this->client->request('GET', '/user/view/' . $user->getId());
|
||||||
|
self::assertResponseIsSuccessful();
|
||||||
|
self::assertSelectorTextContains('body', $user->getEmail());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_view_user_self_with_organization_success(): void
|
||||||
|
{
|
||||||
|
$user = $this->createUser('user@email.com');
|
||||||
|
$organization = $this->createOrganization('Test Org');
|
||||||
|
$uo = $this->createUOLink($user, $organization);
|
||||||
|
$this->client->loginUser($user);
|
||||||
|
$this->client->request('GET', '/user/view/' . $user->getId());
|
||||||
|
self::assertResponseIsSuccessful();
|
||||||
|
self::assertSelectorTextContains('body', $user->getEmail());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_view_user_not_found(): void
|
||||||
|
{
|
||||||
|
$admin = $this->createUser('admin@admin', ['ROLE_SUPER_ADMIN']);
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
|
||||||
|
$this->client->request('GET', '/user/view/999999');
|
||||||
|
self::assertResponseStatusCodeSame(404);
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region Edit Tests
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_edit_super_admin_success(): void
|
||||||
|
{
|
||||||
|
$admin = $this->createUser('admin@admin', ['ROLE_SUPER_ADMIN']);
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
$this->client->request('GET', '/user/edit/' . $admin->getId());
|
||||||
|
self::assertResponseIsSuccessful();
|
||||||
|
self::assertSelectorTextContains('body', 'Modifier l\'utilisateur');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_edit_regular_user_forbidden(): void
|
||||||
|
{
|
||||||
|
// 1. Arrange
|
||||||
|
$user = $this->createUser('user@mail.com');
|
||||||
|
$this->client->loginUser($user);
|
||||||
|
// 2. Act
|
||||||
|
$this->client->request('GET', '/user/edit/' . $user->getId());
|
||||||
|
// 3. Assert
|
||||||
|
self::assertResponseIsSuccessful();
|
||||||
|
self::assertSelectorTextContains('body', 'Modifier l\'utilisateur');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_edit_other_user_forbidden(): void
|
||||||
|
{
|
||||||
|
// 1. Arrange
|
||||||
|
$user = $this->createUser('user@email.com');
|
||||||
|
$user2 = $this->createUser('user2@email.com');
|
||||||
|
$this->client->loginUser($user);
|
||||||
|
// 2. Act
|
||||||
|
$this->client->request('GET', '/user/edit/' . $user2->getId());
|
||||||
|
// 3. Assert
|
||||||
|
self::assertResponseStatusCodeSame(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_edit_user_not_found(): void
|
||||||
|
{
|
||||||
|
$admin = $this->createUser('admin@admin', ['ROLE_SUPER_ADMIN']);
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
|
||||||
|
$this->client->request('GET', '/user/edit/999999');
|
||||||
|
self::assertResponseStatusCodeSame(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_edit_super_admin_edit_other_user_success(): void
|
||||||
|
{
|
||||||
|
// 1. Arrange: Disable reboot to keep our AWS mock alive
|
||||||
|
$this->client->disableReboot();
|
||||||
|
|
||||||
|
$admin = $this->createUser('admin@user.com', ['ROLE_SUPER_ADMIN']);
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
|
||||||
|
// 2. MOCK AWS Service (Crucial!)
|
||||||
|
// Your code calls $awsService->PutDocObj, so we must intercept that.
|
||||||
|
// 2. MOCK AWS Service
|
||||||
|
$awsMock = $this->createMock(AwsService::class);
|
||||||
|
$awsMock->expects($this->any())
|
||||||
|
->method('PutDocObj')
|
||||||
|
->willReturn(1); // <--- FIXED: Return an integer, not a boolean
|
||||||
|
|
||||||
|
// Inject the mock into the test container
|
||||||
|
static::getContainer()->set(AwsService::class, $awsMock);
|
||||||
|
|
||||||
|
// 3. Create a Dummy Image File
|
||||||
|
$tempFile = tempnam(sys_get_temp_dir(), 'test_logo');
|
||||||
|
file_put_contents($tempFile, 'fake image content'); // Create a dummy file
|
||||||
|
|
||||||
|
$logo = new UploadedFile(
|
||||||
|
$tempFile,
|
||||||
|
'logo.png',
|
||||||
|
'image/png',
|
||||||
|
null,
|
||||||
|
true // 'test' mode = true
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Act: Submit the Edit Form
|
||||||
|
$this->client->request('GET', '/user/edit/' . $admin->getId());
|
||||||
|
$this->client->submitForm('Enregistrer', [
|
||||||
|
'user_form[email]' => 'new@mail.com',
|
||||||
|
'user_form[name]' => 'New Name',
|
||||||
|
'user_form[pictureUrl]' => $logo,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 5. Assert
|
||||||
|
self::assertResponseRedirects('/user/view/' . $admin->getId());
|
||||||
|
$this->client->followRedirect();
|
||||||
|
self::assertSelectorTextContains('body', 'new@mail.com');
|
||||||
|
|
||||||
|
// Clean up the temporary file}
|
||||||
|
unlink($tempFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_edit_admin_user_not_found(): void
|
||||||
|
{
|
||||||
|
$admin = $this->createUser('admin@admin', ['ROLE_SUPER_ADMIN']);
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
$this->client->request('GET', '/user/edit/999999');
|
||||||
|
self::assertResponseStatusCodeSame(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_edit_admin_edit_other_user_success(): void
|
||||||
|
{
|
||||||
|
// 1. Arrange: Disable reboot to keep our AWS mock alive
|
||||||
|
$this->client->disableReboot();
|
||||||
|
|
||||||
|
$admin = $this->createUser('admin@user.com', ['ROLE_ADMIN']);
|
||||||
|
$user = $this->createUser('user@user.com');
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
$org = $this->createOrganization('Test Org');
|
||||||
|
$uoAdmin = $this->createUOLink($admin, $org);
|
||||||
|
$uoUser = $this->createUOLink($user, $org);
|
||||||
|
$app = $this->createApp('Test App');
|
||||||
|
$roleAdmin = $this->createRole('ADMIN');
|
||||||
|
$roleUser = $this->createRole('USER');
|
||||||
|
$this->createUOALink($uoAdmin, $app, $roleAdmin);
|
||||||
|
$this->createUOALink($uoUser, $app, $roleUser);
|
||||||
|
|
||||||
|
// 2. MOCK AWS Service (Crucial!)
|
||||||
|
// Your code calls $awsService->PutDocObj, so we must intercept that.
|
||||||
|
// 2. MOCK AWS Service
|
||||||
|
$awsMock = $this->createMock(AwsService::class);
|
||||||
|
$awsMock->expects($this->any())
|
||||||
|
->method('PutDocObj')
|
||||||
|
->willReturn(1); // <--- FIXED: Return an integer, not a boolean
|
||||||
|
|
||||||
|
// Inject the mock into the test container
|
||||||
|
static::getContainer()->set(AwsService::class, $awsMock);
|
||||||
|
|
||||||
|
// 3. Create a Dummy Image File
|
||||||
|
$tempFile = tempnam(sys_get_temp_dir(), 'test_logo');
|
||||||
|
file_put_contents($tempFile, 'fake image content'); // Create a dummy file
|
||||||
|
|
||||||
|
$logo = new UploadedFile(
|
||||||
|
$tempFile,
|
||||||
|
'logo.png',
|
||||||
|
'image/png',
|
||||||
|
null,
|
||||||
|
true // 'test' mode = true
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Act: Submit the Edit Form
|
||||||
|
$this->client->request('GET', '/user/edit/' . $user->getId() . '?organizationId=' . $org->getId());
|
||||||
|
$this->client->submitForm('Enregistrer', [
|
||||||
|
'user_form[email]' => 'new@mail.com',
|
||||||
|
'user_form[name]' => 'New Name',
|
||||||
|
'user_form[pictureUrl]' => $logo,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 5. Assert
|
||||||
|
self::assertResponseRedirects('/user/view/' . $user->getId() . '?organizationId=' . $org->getId());
|
||||||
|
$this->client->followRedirect();
|
||||||
|
self::assertSelectorTextContains('body', 'new@mail.com');
|
||||||
|
|
||||||
|
// Clean up the temporary file}
|
||||||
|
unlink($tempFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_edit_admin_edit_other_user_different_organization_forbidden(): void
|
||||||
|
{
|
||||||
|
// 1. Arrange: Disable reboot to keep our AWS mock alive
|
||||||
|
$this->client->disableReboot();
|
||||||
|
|
||||||
|
$admin = $this->createUser('admin@user.com', ['ROLE_ADMIN']);
|
||||||
|
$user = $this->createUser('user@user.com');
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
$org = $this->createOrganization('Test Org');
|
||||||
|
$org2 = $this->createOrganization('Test Org2');
|
||||||
|
$uoAdmin = $this->createUOLink($admin, $org);
|
||||||
|
$uoUser = $this->createUOLink($user, $org2);
|
||||||
|
$app = $this->createApp('Test App');
|
||||||
|
$roleAdmin = $this->createRole('ADMIN');
|
||||||
|
$roleUser = $this->createRole('USER');
|
||||||
|
$this->createUOALink($uoAdmin, $app, $roleAdmin);
|
||||||
|
$this->createUOALink($uoUser, $app, $roleUser);
|
||||||
|
|
||||||
|
// 2. MOCK AWS Service (Crucial!)
|
||||||
|
// Your code calls $awsService->PutDocObj, so we must intercept that.
|
||||||
|
// 2. MOCK AWS Service
|
||||||
|
$awsMock = $this->createMock(AwsService::class);
|
||||||
|
$awsMock->expects($this->any())
|
||||||
|
->method('PutDocObj')
|
||||||
|
->willReturn(1); // <--- FIXED: Return an integer, not a boolean
|
||||||
|
|
||||||
|
// Inject the mock into the test container
|
||||||
|
static::getContainer()->set(AwsService::class, $awsMock);
|
||||||
|
|
||||||
|
// 3. Create a Dummy Image File
|
||||||
|
$tempFile = tempnam(sys_get_temp_dir(), 'test_logo');
|
||||||
|
file_put_contents($tempFile, 'fake image content'); // Create a dummy file
|
||||||
|
|
||||||
|
$logo = new UploadedFile(
|
||||||
|
$tempFile,
|
||||||
|
'logo.png',
|
||||||
|
'image/png',
|
||||||
|
null,
|
||||||
|
true // 'test' mode = true
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Act: Submit the Edit Form
|
||||||
|
$this->client->request('GET', '/user/edit/' . $user->getId() . '?organizationId=' . $org2->getId());
|
||||||
|
// 5. Assert
|
||||||
|
self::assertResponseStatusCodeSame(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_edit_user_not_found_admin(): void
|
||||||
|
{
|
||||||
|
$admin = $this->createUser('admin@admin', ['ROLE_ADMIN']);
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
|
||||||
|
$this->client->request('GET', '/user/edit/999999');
|
||||||
|
self::assertResponseStatusCodeSame(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_edit_user_self_success(): void
|
||||||
|
{
|
||||||
|
$user = $this->createUser('user@email.com');
|
||||||
|
$this->client->loginUser($user);
|
||||||
|
$this->client->request('GET', '/user/edit/' . $user->getId());
|
||||||
|
self::assertResponseIsSuccessful();
|
||||||
|
self::assertSelectorTextContains('body', 'Modifier l\'utilisateur');
|
||||||
|
$this->client->submitForm('Enregistrer', [
|
||||||
|
'user_form[email]' => 'new@email.com',
|
||||||
|
'user_form[name]' => 'New Name',
|
||||||
|
]);
|
||||||
|
self::assertResponseRedirects('/user/view/' . $user->getId());
|
||||||
|
$this->client->followRedirect();
|
||||||
|
self::assertSelectorTextContains('body', 'new@email.com');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_edit_user_self_with_organization_success(): void
|
||||||
|
{
|
||||||
|
$user = $this->createUser('user@email.com');
|
||||||
|
$this->client->loginUser($user);
|
||||||
|
$org = $this->createOrganization('Test Org');
|
||||||
|
$this->createUOLink($user, $org);
|
||||||
|
$this->client->request('GET', '/user/edit/' . $user->getId() . '?organizationId=' . $org->getId());
|
||||||
|
self::assertResponseIsSuccessful();
|
||||||
|
self::assertSelectorTextContains('body', 'Modifier l\'utilisateur');
|
||||||
|
$this->client->submitForm('Enregistrer', [
|
||||||
|
'user_form[email]' => 'new@email.com',
|
||||||
|
'user_form[name]' => 'New Name',
|
||||||
|
]);
|
||||||
|
self::assertResponseRedirects('/user/view/' . $user->getId() . '?organizationId=' . $org->getId());
|
||||||
|
$this->client->followRedirect();
|
||||||
|
self::assertSelectorTextContains('body', 'new@email.com');
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region Create Tests
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_create_super_admin_forbidden(): void
|
||||||
|
{
|
||||||
|
$admin = $this->createUser('admin@admin.com', ['ROLE_SUPER_ADMIN']);
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
$this->client->request('GET', '/user/new');
|
||||||
|
$this->client->followRedirect();
|
||||||
|
self::assertResponseStatusCodeSame(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_create_regular_user_forbidden(): void
|
||||||
|
{
|
||||||
|
// 1. Arrange
|
||||||
|
$user = $this->createUser('user@email.com');
|
||||||
|
$this->client->loginUser($user);
|
||||||
|
// 2. Act
|
||||||
|
$this->client->request('GET', '/user/new');
|
||||||
|
// 3. Assert
|
||||||
|
self::assertResponseStatusCodeSame(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_create_admin_forbidden(): void
|
||||||
|
{
|
||||||
|
// 1. Arrange
|
||||||
|
$admin = $this->createUser('admin@email.com', ['ROLE_ADMIN']);
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
// 2. Act
|
||||||
|
$this->client->request('GET', '/user/new');
|
||||||
|
// 3. Assert
|
||||||
|
self::assertResponseRedirects('/user/');
|
||||||
|
$this->client->followRedirect();
|
||||||
|
self::assertResponseStatusCodeSame(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_create_super_admin_valid(): void
|
||||||
|
{
|
||||||
|
$admin = $this->createUser('admin@admin.com', ['ROLE_SUPER_ADMIN']);
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
$org = $this->createOrganization('Test Org');
|
||||||
|
$uo = $this->createUOLink($admin, $org);
|
||||||
|
$app = $this->createApp('Test App');
|
||||||
|
$role = $this->createRole('ADMIN');
|
||||||
|
$this->createUOALink($uo, $app, $role);
|
||||||
|
$this->client->request('GET', '/user/new?organizationId=' . $org->getId());
|
||||||
|
self::assertResponseIsSuccessful();
|
||||||
|
$this->client->submitForm('Enregistrer', [
|
||||||
|
'user_form[email]' => 'email@email.com',
|
||||||
|
'user_form[name]' => 'name',
|
||||||
|
'user_form[surname]' => 'surname'
|
||||||
|
]);
|
||||||
|
self::assertResponseRedirects('/organization/view/' . $org->getId());
|
||||||
|
$this->client->followRedirect();
|
||||||
|
self::assertCount(2, $this->entityManager->getRepository(User::class)->findAll());
|
||||||
|
self::assertCount(2, $this->entityManager->getRepository(UsersOrganizations::class)->findAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_create_admin_valid(): void
|
||||||
|
{
|
||||||
|
$admin = $this->createUser('admin@admin.com', ['ROLE_ADMIN']);
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
$org = $this->createOrganization('Test Org');
|
||||||
|
$uo = $this->createUOLink($admin, $org);
|
||||||
|
$app = $this->createApp('Test App');
|
||||||
|
$role = $this->createRole('ADMIN');
|
||||||
|
$this->createUOALink($uo, $app, $role);
|
||||||
|
$this->client->request('GET', '/user/new?organizationId=' . $org->getId());
|
||||||
|
self::assertResponseIsSuccessful();
|
||||||
|
$this->client->submitForm('Enregistrer', [
|
||||||
|
'user_form[email]' => 'email@email.com',
|
||||||
|
'user_form[name]' => 'name',
|
||||||
|
'user_form[surname]' => 'surname'
|
||||||
|
]);
|
||||||
|
self::assertResponseRedirects('/organization/view/' . $org->getId());
|
||||||
|
$this->client->followRedirect();
|
||||||
|
self::assertCount(2, $this->entityManager->getRepository(User::class)->findAll());
|
||||||
|
self::assertCount(2, $this->entityManager->getRepository(UsersOrganizations::class)->findAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_create_admin_no_organization_forbidden(): void
|
||||||
|
{
|
||||||
|
$admin = $this->createUser('user@email.com', ['ROLE_ADMIN']);
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
$this->client->request('GET', '/user/new');
|
||||||
|
self::assertResponseRedirects('/user/');
|
||||||
|
$this->client->followRedirect();
|
||||||
|
self::assertResponseStatusCodeSame(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region Delete Tests
|
||||||
|
#[Test]
|
||||||
|
public function test_delete_super_admin_success(): void
|
||||||
|
{
|
||||||
|
$admin = $this->createUser('admin@admin.com', ['ROLE_SUPER_ADMIN']);
|
||||||
|
$user = $this->createUser('user@emai.com');
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
$org = $this->createOrganization('Test Org');
|
||||||
|
$app = $this->createApp('Test App');
|
||||||
|
$role = $this->createRole('USER');
|
||||||
|
$uoUser = $this->createUOLink($user, $org);
|
||||||
|
$this->createUOALink($uoUser, $app, $role);
|
||||||
|
$this->client->request('POST', '/user/delete/' . $user->getId());
|
||||||
|
self::assertResponseRedirects('/user/');
|
||||||
|
$this->client->followRedirect();
|
||||||
|
self::assertCount(2, $this->entityManager->getRepository(User::class)->findAll());
|
||||||
|
self::assertCount(1, $this->entityManager->getRepository(UsersOrganizations::class)->findAll());
|
||||||
|
self::assertCount(1, $this->entityManager->getRepository(UserOrganizatonApp::class)->findAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_delete_admin_forbidden(): void
|
||||||
|
{
|
||||||
|
$admin = $this->createUser('admin@email.com', ['ROLE_ADMIN']);
|
||||||
|
$user = $this->createUser('user@email.com');
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
$this->client->request('POST', '/user/delete/' . $user->getId());
|
||||||
|
self::assertResponseStatusCodeSame(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_delete_not_found(): void
|
||||||
|
{
|
||||||
|
$admin = $this->createUser('admin@eamil.com', ['ROLE_SUPER_ADMIN']);
|
||||||
|
$this->client->loginUser($admin);
|
||||||
|
$this->client->request('POST', '/user/delete/999999');
|
||||||
|
self::assertResponseStatusCodeSame(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
// même erreur que pour la sécurité. Problème lié au SSO.
|
||||||
|
//region activate/deactivate tests
|
||||||
|
|
||||||
|
// #[Test]
|
||||||
|
// public function test_deactivate_super_admin_success(): void
|
||||||
|
// {
|
||||||
|
// $admin = $this->createUser('admin@email.com', ['ROLE_SUPER_ADMIN']);
|
||||||
|
// $user = $this->createUser('user@email.com');
|
||||||
|
// $this->client->loginUser($admin);
|
||||||
|
// $org = $this->createOrganization('Test Org');
|
||||||
|
// $app = $this->createApp('Test App');
|
||||||
|
// $role = $this->createRole('USER');
|
||||||
|
// $uoUser = $this->createUOLink($user, $org);
|
||||||
|
// $this->createUOALink($uoUser, $app, $role);
|
||||||
|
// $this->client->request('POST', '/user/activeStatus/' . $user->getId(), ['status' => 'deactivate']);
|
||||||
|
// self::assertResponseRedirects('/user/');
|
||||||
|
// $this->client->followRedirect();
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
// même erreur que pour la sécurité. Problème lié au SSO.
|
||||||
|
//region tabulator tests
|
||||||
|
// #[Test]
|
||||||
|
// public function test_tabulator_super_admin_success(): void{
|
||||||
|
// $admin = $this->createUser('admin@email.com', ['ROLE_SUPER_ADMIN']);
|
||||||
|
// $this->client->loginUser($admin);
|
||||||
|
// $this->client->request('GET', '/user/data');
|
||||||
|
// self::assertResponseIsSuccessful();
|
||||||
|
// self::assertResponseHeaderSame('Content-Type', 'application/json');
|
||||||
|
//
|
||||||
|
// $response = $this->client->getResponse();
|
||||||
|
// $data = json_decode($response->getContent(), true);
|
||||||
|
// self::assertArrayHasKey('data', $data);
|
||||||
|
// }
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Functional;
|
||||||
|
|
||||||
|
use App\Entity\Apps;
|
||||||
|
use App\Entity\Notification;
|
||||||
|
use App\Entity\Organizations;
|
||||||
|
use App\Entity\Roles;
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Entity\UserOrganizatonApp;
|
||||||
|
use App\Entity\UsersOrganizations;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||||
|
|
||||||
|
abstract class AbstractFunctionalTest extends WebTestCase
|
||||||
|
{
|
||||||
|
protected KernelBrowser $client;
|
||||||
|
protected EntityManagerInterface $entityManager;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->client = static::createClient();
|
||||||
|
|
||||||
|
// Access the container to get the EntityManager
|
||||||
|
$this->entityManager = static::getContainer()->get(EntityManagerInterface::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createUser(string $email, array $roles = ['ROLE_USER']): User
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
$user->setEmail($email);
|
||||||
|
$user->setRoles($roles);
|
||||||
|
$user->setName('Test');
|
||||||
|
$user->setSurname('User');
|
||||||
|
$user->setPassword('$2y$13$...'); // Dummy hash, logic typically bypassed by loginUser
|
||||||
|
|
||||||
|
$this->entityManager->persist($user);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createApp(string $name): Apps
|
||||||
|
{
|
||||||
|
// Based on your Entity, these fields are NOT nullable, so we must fill them
|
||||||
|
$app = new Apps();
|
||||||
|
$app->setName($name);
|
||||||
|
$app->setTitle($name . ' Title');
|
||||||
|
$app->setSubDomain(strtolower($name)); // Assuming valid subdomain logic
|
||||||
|
$app->setLogoUrl('https://example.com/logo.png');
|
||||||
|
// $app->setDescription() is nullable, so we can skip or set it
|
||||||
|
|
||||||
|
$this->entityManager->persist($app);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return $app;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createOrganization(string $name): Organizations
|
||||||
|
{
|
||||||
|
// I am assuming the Organizations entity structure here based on context
|
||||||
|
$org = new Organizations();
|
||||||
|
$org->setName($name);
|
||||||
|
$org->setEmail('contact@' . strtolower($name) . '.com');
|
||||||
|
$org->setNumber(100 + rand(1, 900)); // Example number
|
||||||
|
$org->setAddress('123 ' . $name . ' St'); // Example address
|
||||||
|
$org->setLogoUrl('https://example.com/org_logo.png');
|
||||||
|
|
||||||
|
$this->entityManager->persist($org);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return $org;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createUOLink(User $user, Organizations $organization): UsersOrganizations{
|
||||||
|
$uo = new UsersOrganizations();
|
||||||
|
$uo->setUsers($user);
|
||||||
|
$uo->setOrganization($organization);
|
||||||
|
$this->entityManager->persist($uo);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return $uo;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createUOALink(UsersOrganizations $uo, Apps $app, Roles $role): UserOrganizatonApp{
|
||||||
|
$uoa = new UserOrganizatonApp();
|
||||||
|
$uoa->setUserOrganization($uo);
|
||||||
|
$uoa->setApplication($app);
|
||||||
|
$uoa->setRole($role);
|
||||||
|
$this->entityManager->persist($uoa);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return $uoa;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createRole(string $name): Roles{
|
||||||
|
$role = new Roles();
|
||||||
|
$role->setName($name);
|
||||||
|
$this->entityManager->persist($role);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return $role;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createNotification($user, string $title, bool $isRead = false): Notification{
|
||||||
|
$notification = new Notification();
|
||||||
|
$notification->setUser($user);
|
||||||
|
$notification->setTitle($title);
|
||||||
|
$notification->setMessage('This is a test notification message.');
|
||||||
|
$notification->setType('info');
|
||||||
|
$notification->setIsRead($isRead);
|
||||||
|
$this->entityManager->persist($notification);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
return $notification;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function countEntities(string $entityClass): int
|
||||||
|
{
|
||||||
|
return $this->entityManager->getRepository($entityClass)->count([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Service;
|
||||||
|
|
||||||
|
use App\Service\AccessTokenService;
|
||||||
|
use App\Service\LoggerService;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Doctrine\ORM\EntityRepository;
|
||||||
|
use League\Bundle\OAuth2ServerBundle\Model\AccessToken;
|
||||||
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class AccessTokenServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
private AccessTokenService $service;
|
||||||
|
|
||||||
|
// Mocks
|
||||||
|
private MockObject|EntityManagerInterface $entityManager;
|
||||||
|
private MockObject|LoggerService $loggerService;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->entityManager = $this->createMock(EntityManagerInterface::class);
|
||||||
|
$this->loggerService = $this->createMock(LoggerService::class);
|
||||||
|
|
||||||
|
$this->service = new AccessTokenService(
|
||||||
|
$this->entityManager,
|
||||||
|
$this->loggerService
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRevokeUserTokensSuccess(): void
|
||||||
|
{
|
||||||
|
$userIdentifier = 'test@user.com';
|
||||||
|
|
||||||
|
// 1. Create Mock Tokens
|
||||||
|
$token1 = $this->createMock(AccessToken::class);
|
||||||
|
$token1->method('getIdentifier')->willReturn('token_1');
|
||||||
|
|
||||||
|
$token2 = $this->createMock(AccessToken::class);
|
||||||
|
$token2->method('getIdentifier')->willReturn('token_2');
|
||||||
|
|
||||||
|
// 2. Mock Repository to return these tokens
|
||||||
|
$repo = $this->createMock(EntityRepository::class);
|
||||||
|
$repo->expects($this->once())
|
||||||
|
->method('findBy')
|
||||||
|
->with(['userIdentifier' => $userIdentifier, 'revoked' => false])
|
||||||
|
->willReturn([$token1, $token2]);
|
||||||
|
|
||||||
|
$this->entityManager->expects($this->once())
|
||||||
|
->method('getRepository')
|
||||||
|
->with(AccessToken::class)
|
||||||
|
->willReturn($repo);
|
||||||
|
|
||||||
|
// 3. Expect revoke() to be called on EACH token
|
||||||
|
$token1->expects($this->once())->method('revoke');
|
||||||
|
$token2->expects($this->once())->method('revoke');
|
||||||
|
|
||||||
|
// 4. Expect success logs
|
||||||
|
$this->loggerService->expects($this->exactly(2))
|
||||||
|
->method('logTokenRevocation')
|
||||||
|
->with(
|
||||||
|
'Access token revoked for user',
|
||||||
|
$this->callback(function ($context) use ($userIdentifier) {
|
||||||
|
return $context['user_identifier'] === $userIdentifier
|
||||||
|
&& in_array($context['token_id'], ['token_1', 'token_2']);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 5. Run
|
||||||
|
$this->service->revokeUserTokens($userIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRevokeUserTokensHandlesException(): void
|
||||||
|
{
|
||||||
|
$userIdentifier = 'fail@user.com';
|
||||||
|
|
||||||
|
// 1. Create a Token that fails to revoke
|
||||||
|
$tokenBad = $this->createMock(AccessToken::class);
|
||||||
|
$tokenBad->method('getIdentifier')->willReturn('bad_token');
|
||||||
|
|
||||||
|
// Throw exception when revoke is called
|
||||||
|
$tokenBad->expects($this->once())
|
||||||
|
->method('revoke')
|
||||||
|
->willThrowException(new \Exception('DB Connection Lost'));
|
||||||
|
|
||||||
|
// 2. Create a Token that works (to prove loop continues, if applicable)
|
||||||
|
// Your code uses try-catch inside the loop, so it SHOULD continue.
|
||||||
|
$tokenGood = $this->createMock(AccessToken::class);
|
||||||
|
$tokenGood->method('getIdentifier')->willReturn('good_token');
|
||||||
|
$tokenGood->expects($this->once())->method('revoke');
|
||||||
|
|
||||||
|
// 3. Mock Repository
|
||||||
|
$repo = $this->createMock(EntityRepository::class);
|
||||||
|
$repo->method('findBy')->willReturn([$tokenBad, $tokenGood]);
|
||||||
|
$this->entityManager->method('getRepository')->willReturn($repo);
|
||||||
|
|
||||||
|
// 4. Expect Logger calls
|
||||||
|
// Expect 1 Error log
|
||||||
|
$this->loggerService->expects($this->once())
|
||||||
|
->method('logError')
|
||||||
|
->with(
|
||||||
|
'Error revoking access token: DB Connection Lost',
|
||||||
|
['user_identifier' => $userIdentifier, 'token_id' => 'bad_token']
|
||||||
|
);
|
||||||
|
|
||||||
|
// Expect 1 Success log (for the good token)
|
||||||
|
$this->loggerService->expects($this->once())
|
||||||
|
->method('logTokenRevocation')
|
||||||
|
->with(
|
||||||
|
'Access token revoked for user',
|
||||||
|
['user_identifier' => $userIdentifier, 'token_id' => 'good_token']
|
||||||
|
);
|
||||||
|
|
||||||
|
// 5. Run
|
||||||
|
$this->service->revokeUserTokens($userIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRevokeUserTokensDoesNothingIfNoneFound(): void
|
||||||
|
{
|
||||||
|
$userIdentifier = 'ghost@user.com';
|
||||||
|
|
||||||
|
$repo = $this->createMock(EntityRepository::class);
|
||||||
|
$repo->method('findBy')->willReturn([]); // Empty array
|
||||||
|
|
||||||
|
$this->entityManager->method('getRepository')->willReturn($repo);
|
||||||
|
|
||||||
|
// Expect NO logs
|
||||||
|
$this->loggerService->expects($this->never())->method('logTokenRevocation');
|
||||||
|
$this->loggerService->expects($this->never())->method('logError');
|
||||||
|
|
||||||
|
$this->service->revokeUserTokens($userIdentifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,193 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Service;
|
||||||
|
|
||||||
|
use App\Entity\Actions;
|
||||||
|
use App\Entity\Organizations;
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Service\ActionService;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class ActionServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
private ActionService $service;
|
||||||
|
private MockObject|EntityManagerInterface $entityManager;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->entityManager = $this->createMock(EntityManagerInterface::class);
|
||||||
|
$this->service = new ActionService($this->entityManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to set private ID properties on entities without setters
|
||||||
|
*/
|
||||||
|
private function setEntityId(object $entity, int $id): void
|
||||||
|
{
|
||||||
|
$reflection = new \ReflectionClass($entity);
|
||||||
|
if ($reflection->hasProperty('id')) {
|
||||||
|
$property = $reflection->getProperty('id');
|
||||||
|
// $property->setAccessible(true); // Uncomment for PHP < 8.1
|
||||||
|
$property->setValue($entity, $id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// TEST: getActivityColor
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
public function testGetActivityColorRecent(): void
|
||||||
|
{
|
||||||
|
// Less than 15 minutes ago
|
||||||
|
$date = new \DateTimeImmutable('-10 minutes');
|
||||||
|
$color = $this->service->getActivityColor($date);
|
||||||
|
$this->assertEquals('#086572', $color);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetActivityColorMedium(): void
|
||||||
|
{
|
||||||
|
// Between 15 and 60 minutes ago
|
||||||
|
$date = new \DateTimeImmutable('-30 minutes');
|
||||||
|
$color = $this->service->getActivityColor($date);
|
||||||
|
$this->assertEquals('#247208', $color);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetActivityColorOld(): void
|
||||||
|
{
|
||||||
|
// Older than 1 hour
|
||||||
|
$date = new \DateTimeImmutable('-2 hours');
|
||||||
|
$color = $this->service->getActivityColor($date);
|
||||||
|
$this->assertEquals('#cc664c', $color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// TEST: formatActivities
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
public function testFormatActivities(): void
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
$org = new Organizations();
|
||||||
|
|
||||||
|
$action1 = new Actions();
|
||||||
|
$action1->setDate(new \DateTimeImmutable('-5 minutes')); // Recent
|
||||||
|
$action1->setActionType('LOGIN');
|
||||||
|
$action1->setUsers($user);
|
||||||
|
$action1->setOrganization($org);
|
||||||
|
$action1->setDescription('User logged in');
|
||||||
|
|
||||||
|
$action2 = new Actions();
|
||||||
|
$action2->setDate(new \DateTimeImmutable('-2 hours')); // Old
|
||||||
|
$action2->setUsers($user);
|
||||||
|
$action2->setActionType('LOGOUT');
|
||||||
|
|
||||||
|
$activities = [$action1, $action2];
|
||||||
|
|
||||||
|
$result = $this->service->formatActivities($activities);
|
||||||
|
|
||||||
|
$this->assertCount(2, $result);
|
||||||
|
|
||||||
|
// Check first activity (Recent)
|
||||||
|
$this->assertEquals('#086572', $result[0]['color']);
|
||||||
|
$this->assertEquals('LOGIN', $result[0]['actionType']);
|
||||||
|
$this->assertSame($user->getName(), $result[0]['userName']);
|
||||||
|
|
||||||
|
// Check second activity (Old)
|
||||||
|
$this->assertEquals('#cc664c', $result[1]['color']);
|
||||||
|
$this->assertEquals('LOGOUT', $result[1]['actionType']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// TEST: createAction
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
public function testCreateActionBasic(): void
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
$user->setEmail('user@test.com');
|
||||||
|
|
||||||
|
$this->entityManager->expects($this->once())
|
||||||
|
->method('persist')
|
||||||
|
->with($this->callback(function (Actions $action) use ($user) {
|
||||||
|
return $action->getActionType() === 'LOGIN'
|
||||||
|
&& $action->getUsers() === $user
|
||||||
|
&& $action->getOrganization() === null
|
||||||
|
&& $action->getDescription() === null;
|
||||||
|
}));
|
||||||
|
|
||||||
|
$this->entityManager->expects($this->once())->method('flush');
|
||||||
|
|
||||||
|
$this->service->createAction('LOGIN', $user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCreateActionWithOrganizationAndTarget(): void
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
$user->setEmail('admin@test.com');
|
||||||
|
|
||||||
|
$org = new Organizations();
|
||||||
|
$this->setEntityId($org, 99);
|
||||||
|
|
||||||
|
// Expect persist with full details
|
||||||
|
$this->entityManager->expects($this->once())
|
||||||
|
->method('persist')
|
||||||
|
->with($this->callback(function (Actions $action) use ($user, $org) {
|
||||||
|
return $action->getActionType() === 'UPDATE'
|
||||||
|
&& $action->getUsers() === $user
|
||||||
|
&& $action->getOrganization() === $org
|
||||||
|
// Check description generated by descriptionAction
|
||||||
|
&& str_contains($action->getDescription(), 'UPDATE by admin@test.com onto Settings');
|
||||||
|
}));
|
||||||
|
|
||||||
|
$this->entityManager->expects($this->once())->method('flush');
|
||||||
|
|
||||||
|
$this->service->createAction('UPDATE', $user, $org, 'Settings');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// TEST: descriptionAction
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
public function testDescriptionActionSuccess(): void
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
$user->setEmail('jane@doe.com');
|
||||||
|
|
||||||
|
$action = new Actions();
|
||||||
|
$action->setActionType('DELETE');
|
||||||
|
$action->setUsers($user);
|
||||||
|
|
||||||
|
// Pass by reference
|
||||||
|
$this->service->descriptionAction($action, 'Document.pdf');
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
'DELETE by jane@doe.com onto Document.pdf',
|
||||||
|
$action->getDescription()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDescriptionActionThrowsIfNoUser(): void
|
||||||
|
{
|
||||||
|
$action = new Actions();
|
||||||
|
$action->setActionType('DELETE');
|
||||||
|
// No user set
|
||||||
|
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
$this->expectExceptionMessage('Action must have a user set');
|
||||||
|
|
||||||
|
$this->service->descriptionAction($action, 'Target');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDescriptionActionThrowsIfInvalidType(): void
|
||||||
|
{
|
||||||
|
$invalidObject = new \stdClass();
|
||||||
|
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
$this->expectExceptionMessage('Action must be an instance of Actions');
|
||||||
|
|
||||||
|
// Pass an object that is NOT an instance of Actions entity
|
||||||
|
$this->service->descriptionAction($invalidObject, 'Target');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,235 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
if (!function_exists('App\Service\uuid_create')) {
|
||||||
|
function uuid_create($type) {
|
||||||
|
// Return a fixed dummy UUID for testing reliability
|
||||||
|
return '78832168-3015-4673-952c-745143093202';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('App\Service\uuid_is_valid')) {
|
||||||
|
function uuid_is_valid($uuid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
namespace App\Tests\Service;
|
||||||
|
|
||||||
|
use App\Service\AwsService;
|
||||||
|
use Aws\CommandInterface;
|
||||||
|
use Aws\Middleware;
|
||||||
|
use Aws\MockHandler;
|
||||||
|
use Aws\History;
|
||||||
|
use Aws\Result;
|
||||||
|
use Aws\S3\S3Client;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class AwsServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
private AwsService $service;
|
||||||
|
private MockHandler $mockHandler;
|
||||||
|
private History $history;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
// 1. Create a MockHandler to queue up fake responses
|
||||||
|
$this->mockHandler = new MockHandler();
|
||||||
|
|
||||||
|
// 2. Create a History container to capture the requests sent
|
||||||
|
$this->history = new History();
|
||||||
|
|
||||||
|
// 3. Instantiate S3Client passing the MockHandler DIRECTLY to 'handler'
|
||||||
|
$s3Client = new S3Client([
|
||||||
|
'region' => 'eu-west-3',
|
||||||
|
'version' => 'latest',
|
||||||
|
'handler' => $this->mockHandler,
|
||||||
|
'credentials' => [
|
||||||
|
'key' => 'test',
|
||||||
|
'secret' => 'test',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 4. Attach the History middleware
|
||||||
|
$s3Client->getHandlerList()->appendSign(Middleware::history($this->history));
|
||||||
|
|
||||||
|
$this->service = new AwsService(
|
||||||
|
$s3Client,
|
||||||
|
'https://s3.eu-west-3.amazonaws.com'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGenerateUUIDv4(): void
|
||||||
|
{
|
||||||
|
$uuid = $this->service->generateUUIDv4();
|
||||||
|
// Matches the static string we defined at the top of this file
|
||||||
|
$this->assertEquals('78832168-3015-4673-952c-745143093202', $uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetPublicUrl(): void
|
||||||
|
{
|
||||||
|
$result = $this->service->getPublicUrl('my-bucket');
|
||||||
|
$this->assertEquals('https://my-bucket.s3.eu-west-3.amazonaws.com/', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// TEST: createBucket
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
public function testCreateBucketSuccess(): void
|
||||||
|
{
|
||||||
|
// Queue a success response (200 OK)
|
||||||
|
$this->mockHandler->append(new Result(['@metadata' => ['statusCode' => 200]]));
|
||||||
|
|
||||||
|
$result = $this->service->createBucket();
|
||||||
|
|
||||||
|
// Since we mocked uuid_create, we know EXACTLY what the bucket name will be
|
||||||
|
$expectedBucketName = '78832168-3015-4673-952c-745143093202';
|
||||||
|
$this->assertEquals($expectedBucketName, $result);
|
||||||
|
|
||||||
|
$this->assertCount(1, $this->history);
|
||||||
|
/** @var CommandInterface $cmd */
|
||||||
|
$cmd = $this->history->getLastCommand();
|
||||||
|
|
||||||
|
$this->assertEquals('CreateBucket', $cmd->getName());
|
||||||
|
$this->assertEquals('BucketOwnerPreferred', $cmd['ObjectOwnership']);
|
||||||
|
$this->assertEquals($expectedBucketName, $cmd['Bucket']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCreateBucketFailure(): void
|
||||||
|
{
|
||||||
|
$this->mockHandler->append(new Result(['@metadata' => ['statusCode' => 403]]));
|
||||||
|
|
||||||
|
$result = $this->service->createBucket();
|
||||||
|
|
||||||
|
$this->assertIsArray($result);
|
||||||
|
$this->assertEquals(403, $result['statusCode']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// TEST: DeleteBucket
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
public function testDeleteBucket(): void
|
||||||
|
{
|
||||||
|
$this->mockHandler->append(new Result(['@metadata' => ['statusCode' => 200]]));
|
||||||
|
|
||||||
|
$result = $this->service->DeleteBucket('test-bucket');
|
||||||
|
|
||||||
|
$this->assertEquals('test-bucket', $result);
|
||||||
|
|
||||||
|
$cmd = $this->history->getLastCommand();
|
||||||
|
$this->assertEquals('DeleteBucket', $cmd->getName());
|
||||||
|
$this->assertEquals('test-bucket', $cmd['Bucket']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// TEST: getListObject
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
public function testGetListObjectReturnsContents(): void
|
||||||
|
{
|
||||||
|
$this->mockHandler->append(new Result([
|
||||||
|
'Contents' => [
|
||||||
|
['Key' => 'file1.txt'],
|
||||||
|
['Key' => 'file2.jpg'],
|
||||||
|
]
|
||||||
|
]));
|
||||||
|
|
||||||
|
$result = $this->service->getListObject('my-bucket', 'prefix');
|
||||||
|
|
||||||
|
$this->assertCount(2, $result);
|
||||||
|
$this->assertEquals('file1.txt', $result[0]['Key']);
|
||||||
|
|
||||||
|
$cmd = $this->history->getLastCommand();
|
||||||
|
$this->assertEquals('ListObjectsV2', $cmd->getName());
|
||||||
|
$this->assertEquals('my-bucket', $cmd['Bucket']);
|
||||||
|
$this->assertEquals('prefix', $cmd['Prefix']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// TEST: PutDocObj
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
public function testPutDocObj(): void
|
||||||
|
{
|
||||||
|
$tempFile = tempnam(sys_get_temp_dir(), 'test_s3');
|
||||||
|
file_put_contents($tempFile, 'dummy content');
|
||||||
|
|
||||||
|
$this->mockHandler->append(new Result(['@metadata' => ['statusCode' => 200]]));
|
||||||
|
|
||||||
|
// Helper object to bypass strictly typed generic object hint + fopen
|
||||||
|
$fileObj = new class($tempFile) {
|
||||||
|
public function __construct(private $path) {}
|
||||||
|
public function __toString() { return $this->path; }
|
||||||
|
};
|
||||||
|
|
||||||
|
$status = $this->service->PutDocObj(
|
||||||
|
'my-bucket',
|
||||||
|
$fileObj,
|
||||||
|
'image.png',
|
||||||
|
'image/png',
|
||||||
|
'folder/'
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(200, $status);
|
||||||
|
|
||||||
|
$cmd = $this->history->getLastCommand();
|
||||||
|
$this->assertEquals('PutObject', $cmd->getName());
|
||||||
|
$this->assertEquals('folder/image.png', $cmd['Key']);
|
||||||
|
$this->assertNotEmpty($cmd['ChecksumSHA256']);
|
||||||
|
|
||||||
|
@unlink($tempFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// TEST: renameDocObj
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
public function testRenameDocObj(): void
|
||||||
|
{
|
||||||
|
$this->mockHandler->append(
|
||||||
|
new Result(['@metadata' => ['statusCode' => 200]]),
|
||||||
|
new Result(['@metadata' => ['statusCode' => 204]])
|
||||||
|
);
|
||||||
|
|
||||||
|
$status = $this->service->renameDocObj('b', 'old.txt', 'new.txt', 'p/');
|
||||||
|
|
||||||
|
$this->assertEquals(200, $status);
|
||||||
|
|
||||||
|
$this->assertCount(2, $this->history);
|
||||||
|
$requests = iterator_to_array($this->history);
|
||||||
|
|
||||||
|
/** @var CommandInterface $cmdCopy */
|
||||||
|
$cmdCopy = $requests[0]['command'];
|
||||||
|
$this->assertEquals('CopyObject', $cmdCopy->getName());
|
||||||
|
$this->assertEquals('p/new.txt', $cmdCopy['Key']);
|
||||||
|
|
||||||
|
/** @var CommandInterface $cmdDelete */
|
||||||
|
$cmdDelete = $requests[1]['command'];
|
||||||
|
$this->assertEquals('DeleteObject', $cmdDelete->getName());
|
||||||
|
$this->assertEquals('p/old.txt', $cmdDelete['Key']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// TEST: moveDocObj
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
public function testMoveDocObj(): void
|
||||||
|
{
|
||||||
|
$this->mockHandler->append(
|
||||||
|
new Result(['@metadata' => ['statusCode' => 200]]),
|
||||||
|
new Result(['@metadata' => ['statusCode' => 204]])
|
||||||
|
);
|
||||||
|
|
||||||
|
$status = $this->service->moveDocObj('b', 'file.txt', 'old/', 'new/');
|
||||||
|
|
||||||
|
$this->assertEquals(200, $status);
|
||||||
|
|
||||||
|
$requests = iterator_to_array($this->history);
|
||||||
|
|
||||||
|
$cmdCopy = $requests[0]['command'];
|
||||||
|
$this->assertEquals('new/file.txt', $cmdCopy['Key']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,219 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Service;
|
||||||
|
|
||||||
|
use App\Entity\Cgu;
|
||||||
|
use App\Entity\CguUser;
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Repository\CguRepository; // <--- Import your actual repository
|
||||||
|
use App\Service\CguUserService;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Doctrine\ORM\EntityRepository;
|
||||||
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class CguUserServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
private CguUserService $service;
|
||||||
|
private MockObject|EntityManagerInterface $entityManager;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->entityManager = $this->createMock(EntityManagerInterface::class);
|
||||||
|
$this->service = new CguUserService($this->entityManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// TEST: isLatestCguAccepted
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
public function testIsLatestCguAcceptedReturnsFalseIfNoCguExists(): void
|
||||||
|
{
|
||||||
|
$user = $this->createMock(User::class);
|
||||||
|
|
||||||
|
// 1. Create a mock of your ACTUAL custom repository
|
||||||
|
// Since 'findLatestCgu' exists in CguRepository, PHPUnit allows this.
|
||||||
|
$cguRepo = $this->createMock(CguRepository::class);
|
||||||
|
$cguRepo->method('findLatestCgu')->willReturn(null);
|
||||||
|
|
||||||
|
$this->entityManager->method('getRepository')
|
||||||
|
->with(Cgu::class)
|
||||||
|
->willReturn($cguRepo);
|
||||||
|
|
||||||
|
$this->assertFalse($this->service->isLatestCguAccepted($user));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsLatestCguAcceptedReturnsFalseIfRelationDoesNotExist(): void
|
||||||
|
{
|
||||||
|
$user = $this->createMock(User::class);
|
||||||
|
$latestCgu = new Cgu();
|
||||||
|
|
||||||
|
// Mock CguRepository
|
||||||
|
$cguRepo = $this->createMock(CguRepository::class);
|
||||||
|
$cguRepo->method('findLatestCgu')->willReturn($latestCgu);
|
||||||
|
|
||||||
|
// Mock Generic Repository for CguUser (standard findOneBy)
|
||||||
|
$cguUserRepo = $this->createMock(EntityRepository::class);
|
||||||
|
$cguUserRepo->method('findOneBy')
|
||||||
|
->with(['users' => $user, 'cgu' => $latestCgu])
|
||||||
|
->willReturn(null);
|
||||||
|
|
||||||
|
$this->entityManager->method('getRepository')->willReturnMap([
|
||||||
|
[Cgu::class, $cguRepo],
|
||||||
|
[CguUser::class, $cguUserRepo],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertFalse($this->service->isLatestCguAccepted($user));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsLatestCguAcceptedReturnsTrueIfAccepted(): void
|
||||||
|
{
|
||||||
|
$user = $this->createMock(User::class);
|
||||||
|
$latestCgu = new Cgu();
|
||||||
|
|
||||||
|
$cguUser = new CguUser();
|
||||||
|
$cguUser->setIsAccepted(true);
|
||||||
|
|
||||||
|
$cguRepo = $this->createMock(CguRepository::class);
|
||||||
|
$cguRepo->method('findLatestCgu')->willReturn($latestCgu);
|
||||||
|
|
||||||
|
$cguUserRepo = $this->createMock(EntityRepository::class);
|
||||||
|
$cguUserRepo->method('findOneBy')->willReturn($cguUser);
|
||||||
|
|
||||||
|
$this->entityManager->method('getRepository')->willReturnMap([
|
||||||
|
[Cgu::class, $cguRepo],
|
||||||
|
[CguUser::class, $cguUserRepo],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertTrue($this->service->isLatestCguAccepted($user));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// TEST: acceptLatestCgu
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
public function testAcceptLatestCguDoNothingIfNoCgu(): void
|
||||||
|
{
|
||||||
|
$user = $this->createMock(User::class);
|
||||||
|
|
||||||
|
$cguRepo = $this->createMock(CguRepository::class);
|
||||||
|
$cguRepo->method('findLatestCgu')->willReturn(null);
|
||||||
|
|
||||||
|
$this->entityManager->method('getRepository')->willReturn($cguRepo);
|
||||||
|
|
||||||
|
$this->entityManager->expects($this->never())->method('persist');
|
||||||
|
$this->entityManager->expects($this->never())->method('flush');
|
||||||
|
|
||||||
|
$this->service->acceptLatestCgu($user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAcceptLatestCguCreatesNewRelation(): void
|
||||||
|
{
|
||||||
|
$user = $this->createMock(User::class);
|
||||||
|
$latestCgu = new Cgu();
|
||||||
|
|
||||||
|
$cguRepo = $this->createMock(CguRepository::class);
|
||||||
|
$cguRepo->method('findLatestCgu')->willReturn($latestCgu);
|
||||||
|
|
||||||
|
$cguUserRepo = $this->createMock(EntityRepository::class);
|
||||||
|
$cguUserRepo->method('findOneBy')->willReturn(null);
|
||||||
|
|
||||||
|
$this->entityManager->method('getRepository')->willReturnMap([
|
||||||
|
[Cgu::class, $cguRepo],
|
||||||
|
[CguUser::class, $cguUserRepo],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Capture logic for persist
|
||||||
|
$capturedCguUser = null;
|
||||||
|
$this->entityManager->expects($this->once())
|
||||||
|
->method('persist')
|
||||||
|
->with($this->callback(function ($entity) use ($latestCgu, $user, &$capturedCguUser) {
|
||||||
|
// Check basic structure
|
||||||
|
if ($entity instanceof CguUser && $entity->getCgu() === $latestCgu && $entity->getUsers() === $user) {
|
||||||
|
$capturedCguUser = $entity;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}));
|
||||||
|
|
||||||
|
$this->entityManager->expects($this->once())->method('flush');
|
||||||
|
|
||||||
|
$this->service->acceptLatestCgu($user);
|
||||||
|
|
||||||
|
// Assert Final State (after setIsAccepted(true) was called)
|
||||||
|
$this->assertNotNull($capturedCguUser);
|
||||||
|
$this->assertTrue($capturedCguUser->isAccepted());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAcceptLatestCguUpdatesExistingRelation(): void
|
||||||
|
{
|
||||||
|
$user = $this->createMock(User::class);
|
||||||
|
$latestCgu = new Cgu();
|
||||||
|
|
||||||
|
$cguUser = new CguUser();
|
||||||
|
$cguUser->setIsAccepted(false);
|
||||||
|
|
||||||
|
$cguRepo = $this->createMock(CguRepository::class);
|
||||||
|
$cguRepo->method('findLatestCgu')->willReturn($latestCgu);
|
||||||
|
|
||||||
|
$cguUserRepo = $this->createMock(EntityRepository::class);
|
||||||
|
$cguUserRepo->method('findOneBy')->willReturn($cguUser);
|
||||||
|
|
||||||
|
$this->entityManager->method('getRepository')->willReturnMap([
|
||||||
|
[Cgu::class, $cguRepo],
|
||||||
|
[CguUser::class, $cguUserRepo],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->entityManager->expects($this->never())->method('persist');
|
||||||
|
$this->entityManager->expects($this->once())->method('flush');
|
||||||
|
|
||||||
|
$this->service->acceptLatestCgu($user);
|
||||||
|
|
||||||
|
$this->assertTrue($cguUser->isAccepted());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// TEST: declineCgu
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
public function testDeclineCguSuccess(): void
|
||||||
|
{
|
||||||
|
$user = $this->createMock(User::class);
|
||||||
|
$cgu = new Cgu();
|
||||||
|
|
||||||
|
$cguUser = new CguUser();
|
||||||
|
$cguUser->setIsAccepted(true);
|
||||||
|
|
||||||
|
$cguUserRepo = $this->createMock(EntityRepository::class);
|
||||||
|
$cguUserRepo->expects($this->once())
|
||||||
|
->method('findOneBy')
|
||||||
|
->with(['users' => $user, 'cgu' => $cgu])
|
||||||
|
->willReturn($cguUser);
|
||||||
|
|
||||||
|
$this->entityManager->method('getRepository')
|
||||||
|
->with(CguUser::class)
|
||||||
|
->willReturn($cguUserRepo);
|
||||||
|
|
||||||
|
$this->entityManager->expects($this->once())->method('flush');
|
||||||
|
|
||||||
|
$this->service->declineCgu($user, $cgu);
|
||||||
|
|
||||||
|
$this->assertFalse($cguUser->isAccepted());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDeclineCguThrowsExceptionIfNotFound(): void
|
||||||
|
{
|
||||||
|
$user = $this->createMock(User::class);
|
||||||
|
$cgu = new Cgu();
|
||||||
|
|
||||||
|
$cguUserRepo = $this->createMock(EntityRepository::class);
|
||||||
|
$cguUserRepo->method('findOneBy')->willReturn(null);
|
||||||
|
|
||||||
|
$this->entityManager->method('getRepository')->willReturn($cguUserRepo);
|
||||||
|
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
$this->expectExceptionMessage('CGU not found for this user');
|
||||||
|
|
||||||
|
$this->service->declineCgu($user, $cgu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,225 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Service;
|
||||||
|
|
||||||
|
use App\Entity\Organizations;
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Service\EmailService;
|
||||||
|
use App\Service\LoggerService;
|
||||||
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||||
|
use Symfony\Component\Mailer\Exception\TransportException;
|
||||||
|
use Symfony\Component\Mailer\MailerInterface;
|
||||||
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
|
|
||||||
|
class EmailServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
private EmailService $service;
|
||||||
|
|
||||||
|
// Mocks
|
||||||
|
private MockObject|MailerInterface $mailer;
|
||||||
|
private MockObject|LoggerInterface $logger; // PSR Logger
|
||||||
|
private MockObject|UrlGeneratorInterface $urlGenerator;
|
||||||
|
private MockObject|LoggerService $loggerService; // Custom Business Logger
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->mailer = $this->createMock(MailerInterface::class);
|
||||||
|
$this->logger = $this->createMock(LoggerInterface::class);
|
||||||
|
$this->urlGenerator = $this->createMock(UrlGeneratorInterface::class);
|
||||||
|
$this->loggerService = $this->createMock(LoggerService::class);
|
||||||
|
|
||||||
|
$this->service = new EmailService(
|
||||||
|
$this->mailer,
|
||||||
|
$this->logger,
|
||||||
|
$this->urlGenerator,
|
||||||
|
$this->loggerService
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to set private ID property on entities.
|
||||||
|
*/
|
||||||
|
private function setEntityId(object $entity, int $id): void
|
||||||
|
{
|
||||||
|
$reflection = new \ReflectionClass($entity);
|
||||||
|
if ($reflection->hasProperty('id')) {
|
||||||
|
$property = $reflection->getProperty('id');
|
||||||
|
// $property->setAccessible(true); // Uncomment for PHP < 8.1
|
||||||
|
$property->setValue($entity, $id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// TEST: sendPasswordSetupEmail
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
public function testSendPasswordSetupEmailSuccess(): void
|
||||||
|
{
|
||||||
|
// 1. Setup Data
|
||||||
|
$user = new User();
|
||||||
|
$this->setEntityId($user, 10);
|
||||||
|
$user->setEmail('new@user.com');
|
||||||
|
|
||||||
|
// Token format: "o{OrgId}@{RandomHex}"
|
||||||
|
// We use "o50@abcdef" to test that Org ID 50 is correctly extracted
|
||||||
|
$token = 'o50@abcdef123456';
|
||||||
|
|
||||||
|
// 2. Expect URL Generation
|
||||||
|
$this->urlGenerator->expects($this->once())
|
||||||
|
->method('generate')
|
||||||
|
->with(
|
||||||
|
'password_setup',
|
||||||
|
['id' => 10, 'token' => $token],
|
||||||
|
UrlGeneratorInterface::ABSOLUTE_URL
|
||||||
|
)
|
||||||
|
->willReturn('https://sudalys.fr/setup/10/token');
|
||||||
|
|
||||||
|
// 3. Expect Mailer Send
|
||||||
|
$this->mailer->expects($this->once())
|
||||||
|
->method('send')
|
||||||
|
->with($this->callback(function (TemplatedEmail $email) use ($user, $token) {
|
||||||
|
// Verify Email Construction
|
||||||
|
$context = $email->getContext();
|
||||||
|
|
||||||
|
return $email->getTo()[0]->getAddress() === 'new@user.com'
|
||||||
|
&& $email->getSubject() === 'Définissez votre mot de passe'
|
||||||
|
&& $email->getHtmlTemplate() === 'emails/password_setup.html.twig'
|
||||||
|
&& $context['user'] === $user
|
||||||
|
&& $context['token'] === $token
|
||||||
|
&& $context['linkUrl'] === 'https://sudalys.fr/setup/10/token';
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 4. Expect Business Log (Success)
|
||||||
|
// Ensure the Org ID '50' was extracted from the token 'o50@...'
|
||||||
|
$this->loggerService->expects($this->once())
|
||||||
|
->method('logEmailSent')
|
||||||
|
->with(10, 50, 'Password setup email sent.');
|
||||||
|
|
||||||
|
// 5. Run
|
||||||
|
$this->service->sendPasswordSetupEmail($user, $token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSendPasswordSetupEmailWithoutOrgIdInToken(): void
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
$this->setEntityId($user, 10);
|
||||||
|
$user->setEmail('user@test.com');
|
||||||
|
|
||||||
|
// Token WITHOUT 'o' prefix -> Org ID should be null
|
||||||
|
$token = 'abcdef123456';
|
||||||
|
|
||||||
|
$this->urlGenerator->method('generate')->willReturn('https://link.com');
|
||||||
|
|
||||||
|
// Verify log receives null for Org ID
|
||||||
|
$this->loggerService->expects($this->once())
|
||||||
|
->method('logEmailSent')
|
||||||
|
->with(10, null, 'Password setup email sent.');
|
||||||
|
|
||||||
|
$this->service->sendPasswordSetupEmail($user, $token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSendPasswordSetupEmailHandlesException(): void
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
$this->setEntityId($user, 10);
|
||||||
|
$user->setEmail('fail@test.com');
|
||||||
|
$token = 'token';
|
||||||
|
|
||||||
|
$this->urlGenerator->method('generate')->willReturn('http://link');
|
||||||
|
|
||||||
|
// Simulate Mailer Failure
|
||||||
|
$this->mailer->expects($this->once())
|
||||||
|
->method('send')
|
||||||
|
->willThrowException(new TransportException('SMTP Error'));
|
||||||
|
|
||||||
|
// Expect System Error Log
|
||||||
|
$this->logger->expects($this->once())
|
||||||
|
->method('error')
|
||||||
|
->with($this->stringContains('Failed to send password setup email: SMTP Error'));
|
||||||
|
|
||||||
|
// Ensure business log is NOT called (or called depending on where failure happens,
|
||||||
|
// in your code business log is AFTER mailer, so it should NOT be called)
|
||||||
|
$this->loggerService->expects($this->never())->method('logEmailSent');
|
||||||
|
|
||||||
|
// No exception should bubble up (caught in catch block)
|
||||||
|
$this->service->sendPasswordSetupEmail($user, $token);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// TEST: sendExistingUserNotificationEmail
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
public function testSendExistingUserNotificationEmailSuccess(): void
|
||||||
|
{
|
||||||
|
// 1. Setup Data
|
||||||
|
$user = new User();
|
||||||
|
$this->setEntityId($user, 20);
|
||||||
|
$user->setEmail('existing@user.com');
|
||||||
|
|
||||||
|
$org = new Organizations();
|
||||||
|
$this->setEntityId($org, 99);
|
||||||
|
$org->setName('My Organization');
|
||||||
|
|
||||||
|
$token = 'some-token';
|
||||||
|
|
||||||
|
// 2. Expect URL Generation
|
||||||
|
$this->urlGenerator->expects($this->once())
|
||||||
|
->method('generate')
|
||||||
|
->with(
|
||||||
|
'user_accept',
|
||||||
|
['id' => 20, 'token' => $token],
|
||||||
|
UrlGeneratorInterface::ABSOLUTE_URL
|
||||||
|
)
|
||||||
|
->willReturn('https://sudalys.fr/accept/20');
|
||||||
|
|
||||||
|
// 3. Expect Mailer Send
|
||||||
|
$this->mailer->expects($this->once())
|
||||||
|
->method('send')
|
||||||
|
->with($this->callback(function (TemplatedEmail $email) use ($org) {
|
||||||
|
return $email->getTo()[0]->getAddress() === 'existing@user.com'
|
||||||
|
&& $email->getSubject() === "Invitation à rejoindre l'organisation My Organization"
|
||||||
|
&& $email->getContext()['expirationDays'] === 15;
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 4. Expect Business Log
|
||||||
|
$this->loggerService->expects($this->once())
|
||||||
|
->method('logEmailSent')
|
||||||
|
->with(20, 99, 'Existing user notification email sent.');
|
||||||
|
|
||||||
|
// 5. Run
|
||||||
|
$this->service->sendExistingUserNotificationEmail($user, $org, $token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSendExistingUserNotificationEmailHandlesException(): void
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
$this->setEntityId($user, 20);
|
||||||
|
$user->setEmail('fail@user.com');
|
||||||
|
|
||||||
|
$org = new Organizations();
|
||||||
|
$this->setEntityId($org, 99);
|
||||||
|
|
||||||
|
$this->urlGenerator->method('generate')->willReturn('link');
|
||||||
|
|
||||||
|
// In this specific method, your code logs success BEFORE sending email?
|
||||||
|
// Looking at source:
|
||||||
|
// $this->loggerService->logEmailSent(...);
|
||||||
|
// $this->mailer->send($email);
|
||||||
|
|
||||||
|
// So we expect logEmailSent to be called even if mailer fails
|
||||||
|
$this->loggerService->expects($this->once())->method('logEmailSent');
|
||||||
|
|
||||||
|
$this->mailer->method('send')
|
||||||
|
->willThrowException(new TransportException('Connection refused'));
|
||||||
|
|
||||||
|
// Expect System Error Log
|
||||||
|
$this->logger->expects($this->once())
|
||||||
|
->method('error')
|
||||||
|
->with($this->stringContains('Failed to send existing user notification email'));
|
||||||
|
|
||||||
|
$this->service->sendExistingUserNotificationEmail($user, $org, 'token');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,295 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Service;
|
||||||
|
|
||||||
|
use App\Service\LoggerService;
|
||||||
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
|
||||||
|
class LoggerServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
private LoggerService $service;
|
||||||
|
|
||||||
|
// Mocks for all the channels
|
||||||
|
private MockObject|LoggerInterface $userManagementLogger;
|
||||||
|
private MockObject|LoggerInterface $organizationManagementLogger;
|
||||||
|
private MockObject|LoggerInterface $accessControlLogger;
|
||||||
|
private MockObject|LoggerInterface $emailNotificationLogger;
|
||||||
|
private MockObject|LoggerInterface $adminActionsLogger;
|
||||||
|
private MockObject|LoggerInterface $securityLogger;
|
||||||
|
private MockObject|LoggerInterface $errorLogger;
|
||||||
|
private MockObject|LoggerInterface $awsLogger;
|
||||||
|
private MockObject|RequestStack $requestStack;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
// Create mocks for all dependencies
|
||||||
|
$this->userManagementLogger = $this->createMock(LoggerInterface::class);
|
||||||
|
$this->organizationManagementLogger = $this->createMock(LoggerInterface::class);
|
||||||
|
$this->accessControlLogger = $this->createMock(LoggerInterface::class);
|
||||||
|
$this->emailNotificationLogger = $this->createMock(LoggerInterface::class);
|
||||||
|
$this->adminActionsLogger = $this->createMock(LoggerInterface::class);
|
||||||
|
$this->securityLogger = $this->createMock(LoggerInterface::class);
|
||||||
|
$this->errorLogger = $this->createMock(LoggerInterface::class);
|
||||||
|
$this->awsLogger = $this->createMock(LoggerInterface::class);
|
||||||
|
$this->requestStack = $this->createMock(RequestStack::class);
|
||||||
|
|
||||||
|
$this->service = new LoggerService(
|
||||||
|
$this->userManagementLogger,
|
||||||
|
$this->organizationManagementLogger,
|
||||||
|
$this->accessControlLogger,
|
||||||
|
$this->emailNotificationLogger,
|
||||||
|
$this->adminActionsLogger,
|
||||||
|
$this->securityLogger,
|
||||||
|
$this->errorLogger,
|
||||||
|
$this->awsLogger,
|
||||||
|
$this->requestStack
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to simulate a request with a specific IP.
|
||||||
|
*/
|
||||||
|
private function mockRequestIp(?string $ip): void
|
||||||
|
{
|
||||||
|
if ($ip === null) {
|
||||||
|
$this->requestStack->method('getCurrentRequest')->willReturn(null);
|
||||||
|
} else {
|
||||||
|
$request = $this->createMock(Request::class);
|
||||||
|
$request->method('getClientIp')->willReturn($ip);
|
||||||
|
$this->requestStack->method('getCurrentRequest')->willReturn($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper assertion to check context contains basic fields + specific data.
|
||||||
|
*/
|
||||||
|
private function assertContextContains(array $expectedSubset): \PHPUnit\Framework\Constraint\Callback
|
||||||
|
{
|
||||||
|
return $this->callback(function (array $context) use ($expectedSubset) {
|
||||||
|
// Check Timestamp exists (we can't check exact value easily)
|
||||||
|
if (!isset($context['timestamp'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check IP exists
|
||||||
|
if (!isset($context['ip'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check specific keys
|
||||||
|
foreach ($expectedSubset as $key => $value) {
|
||||||
|
if (!array_key_exists($key, $context) || $context[$key] !== $value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// TESTS FOR USER MANAGEMENT LOGS
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
public function testLogUserCreated(): void
|
||||||
|
{
|
||||||
|
$this->mockRequestIp('127.0.0.1');
|
||||||
|
|
||||||
|
$this->userManagementLogger->expects($this->once())
|
||||||
|
->method('notice')
|
||||||
|
->with(
|
||||||
|
"New user created: 10",
|
||||||
|
$this->assertContextContains([
|
||||||
|
'target_user_id' => 10,
|
||||||
|
'acting_user_id' => 99,
|
||||||
|
'ip' => '127.0.0.1'
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->service->logUserCreated(10, 99);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLogCGUAcceptanceLogsToTwoChannels(): void
|
||||||
|
{
|
||||||
|
$this->mockRequestIp('192.168.1.1');
|
||||||
|
$userId = 55;
|
||||||
|
|
||||||
|
// Expect call on User Logger
|
||||||
|
$this->userManagementLogger->expects($this->once())
|
||||||
|
->method('info')
|
||||||
|
->with("User accepted CGU", $this->assertContextContains(['user_id' => $userId]));
|
||||||
|
|
||||||
|
// Expect call on Security Logger
|
||||||
|
$this->securityLogger->expects($this->once())
|
||||||
|
->method('info')
|
||||||
|
->with("User accepted CGU", $this->assertContextContains(['user_id' => $userId]));
|
||||||
|
|
||||||
|
$this->service->logCGUAcceptance($userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// TESTS FOR ORGANIZATION LOGS
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
public function testLogUserOrganizationLinkCreated(): void
|
||||||
|
{
|
||||||
|
$this->mockRequestIp('10.0.0.1');
|
||||||
|
|
||||||
|
$this->organizationManagementLogger->expects($this->once())
|
||||||
|
->method('notice')
|
||||||
|
->with(
|
||||||
|
'User-Organization link created',
|
||||||
|
$this->assertContextContains([
|
||||||
|
'target_user_id' => 1,
|
||||||
|
'organization_id' => 2,
|
||||||
|
'acting_user_id' => 3,
|
||||||
|
'uo_id' => 4
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->service->logUserOrganizationLinkCreated(1, 2, 3, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// TESTS FOR ERROR LOGS
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
public function testLogError(): void
|
||||||
|
{
|
||||||
|
$this->mockRequestIp('127.0.0.1');
|
||||||
|
|
||||||
|
$this->errorLogger->expects($this->once())
|
||||||
|
->method('error')
|
||||||
|
->with(
|
||||||
|
'Something failed',
|
||||||
|
$this->assertContextContains(['details' => 'foo'])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->service->logError('Something failed', ['details' => 'foo']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLogEntityNotFoundHandlesGlobals(): void
|
||||||
|
{
|
||||||
|
$this->mockRequestIp('127.0.0.1');
|
||||||
|
|
||||||
|
// Simulate global server variable for REQUEST_URI
|
||||||
|
$_SERVER['REQUEST_URI'] = '/some/path';
|
||||||
|
|
||||||
|
$this->errorLogger->expects($this->once())
|
||||||
|
->method('error')
|
||||||
|
->with(
|
||||||
|
'Entity not found',
|
||||||
|
$this->assertContextContains([
|
||||||
|
'entity_type' => 'User',
|
||||||
|
'id' => 123,
|
||||||
|
'page_accessed' => '/some/path'
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->service->logEntityNotFound('User', ['id' => 123], 1);
|
||||||
|
|
||||||
|
// Cleanup global
|
||||||
|
unset($_SERVER['REQUEST_URI']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// TESTS FOR SECURITY LOGS
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
public function testLogAccessDenied(): void
|
||||||
|
{
|
||||||
|
$this->mockRequestIp('10.10.10.10');
|
||||||
|
|
||||||
|
$this->securityLogger->expects($this->once())
|
||||||
|
->method('warning')
|
||||||
|
->with(
|
||||||
|
'Access denied',
|
||||||
|
$this->assertContextContains(['acting_user_id' => 5])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->service->logAccessDenied(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLogTokenRevocation(): void
|
||||||
|
{
|
||||||
|
$this->mockRequestIp(null); // Test with NO REQUEST (e.g. CLI)
|
||||||
|
|
||||||
|
$this->securityLogger->expects($this->once())
|
||||||
|
->method('warning')
|
||||||
|
->with(
|
||||||
|
'Token revoked',
|
||||||
|
$this->callback(function($context) {
|
||||||
|
return $context['ip'] === 'unknown' && $context['reason'] === 'expired';
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->service->logTokenRevocation('Token revoked', ['reason' => 'expired']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// TESTS FOR ADMIN ACTIONS
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
public function testLogSuperAdmin(): void
|
||||||
|
{
|
||||||
|
$this->mockRequestIp('1.2.3.4');
|
||||||
|
|
||||||
|
$this->adminActionsLogger->expects($this->once())
|
||||||
|
->method('notice')
|
||||||
|
->with(
|
||||||
|
'Global reset',
|
||||||
|
$this->assertContextContains([
|
||||||
|
'target_user_id' => 10,
|
||||||
|
'acting_user_id' => 1,
|
||||||
|
'organization_id' => null
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->service->logSuperAdmin(10, 1, 'Global reset');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// TESTS FOR AWS LOGS
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
public function testLogAWSAction(): void
|
||||||
|
{
|
||||||
|
$this->mockRequestIp('8.8.8.8');
|
||||||
|
|
||||||
|
$this->awsLogger->expects($this->once())
|
||||||
|
->method('info')
|
||||||
|
->with(
|
||||||
|
'AWS action performed: Upload',
|
||||||
|
$this->assertContextContains(['bucket' => 'my-bucket'])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->service->logAWSAction('Upload', ['bucket' => 'my-bucket']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// TESTS FOR ACCESS CONTROL
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
public function testLogRoleEntityAssignment(): void
|
||||||
|
{
|
||||||
|
$this->mockRequestIp('127.0.0.1');
|
||||||
|
|
||||||
|
$this->accessControlLogger->expects($this->once())
|
||||||
|
->method('info')
|
||||||
|
->with(
|
||||||
|
'Role Assigned',
|
||||||
|
$this->assertContextContains([
|
||||||
|
'target_user_id' => 2,
|
||||||
|
'organization_id' => 3,
|
||||||
|
'role_id' => 4,
|
||||||
|
'acting_user_id' => 1
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->service->logRoleEntityAssignment(2, 3, 4, 1, 'Role Assigned');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,239 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Service;
|
||||||
|
|
||||||
|
use App\Entity\Organizations;
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Message\NotificationMessage;
|
||||||
|
use App\Service\NotificationService;
|
||||||
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Messenger\Envelope;
|
||||||
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
|
|
||||||
|
class NotificationServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
private NotificationService $service;
|
||||||
|
private MockObject|MessageBusInterface $messageBus;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->messageBus = $this->createMock(MessageBusInterface::class);
|
||||||
|
$this->service = new NotificationService($this->messageBus);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to inject IDs into entities without setters.
|
||||||
|
* Prevents "getId() on null" errors since we are not using a real DB.
|
||||||
|
*/
|
||||||
|
private function setEntityId(object $entity, int $id): void
|
||||||
|
{
|
||||||
|
$reflection = new \ReflectionClass($entity);
|
||||||
|
if ($reflection->hasProperty('id')) {
|
||||||
|
$property = $reflection->getProperty('id');
|
||||||
|
// $property->setAccessible(true); // Uncomment if using PHP < 8.1
|
||||||
|
$property->setValue($entity, $id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNotifyUserInvited(): void
|
||||||
|
{
|
||||||
|
// 1. Setup Data
|
||||||
|
$recipient = new User();
|
||||||
|
$this->setEntityId($recipient, 1);
|
||||||
|
|
||||||
|
$invitedUser = new User();
|
||||||
|
$this->setEntityId($invitedUser, 2);
|
||||||
|
$invitedUser->setName('John');
|
||||||
|
$invitedUser->setSurname('Doe');
|
||||||
|
$invitedUser->setEmail('john@doe.com');
|
||||||
|
|
||||||
|
$org = new Organizations();
|
||||||
|
$this->setEntityId($org, 100);
|
||||||
|
$org->setName('Acme Corp');
|
||||||
|
|
||||||
|
// 2. Expect Dispatch
|
||||||
|
$this->messageBus->expects($this->once())
|
||||||
|
->method('dispatch')
|
||||||
|
->with($this->callback(function (NotificationMessage $message) {
|
||||||
|
// Verify the content of the dispatched message object
|
||||||
|
return $message->getType() === NotificationService::TYPE_USER_INVITED
|
||||||
|
&& $message->getTitle() === 'Invitation envoyée'
|
||||||
|
&& str_contains($message->getMessage(), 'John Doe a été invité à rejoindre Acme Corp')
|
||||||
|
&& $message->getData()['userEmail'] === 'john@doe.com'
|
||||||
|
&& $message->getOrganizationId() === 100;
|
||||||
|
}))
|
||||||
|
->willReturn(new Envelope(new \stdClass())); // Dispatch returns an Envelope
|
||||||
|
|
||||||
|
// 3. Run
|
||||||
|
$this->service->notifyUserInvited($recipient, $invitedUser, $org);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNotifyUserAcceptedInvite(): void
|
||||||
|
{
|
||||||
|
$recipient = new User(); $this->setEntityId($recipient, 1);
|
||||||
|
$acceptedUser = new User(); $this->setEntityId($acceptedUser, 2);
|
||||||
|
$acceptedUser->setName('Jane');
|
||||||
|
$acceptedUser->setSurname('Smith');
|
||||||
|
$acceptedUser->setEmail('jane@smith.com');
|
||||||
|
|
||||||
|
$org = new Organizations(); $this->setEntityId($org, 200);
|
||||||
|
$org->setName('TechGlobal');
|
||||||
|
|
||||||
|
$this->messageBus->expects($this->once())
|
||||||
|
->method('dispatch')
|
||||||
|
->with($this->callback(function (NotificationMessage $message) {
|
||||||
|
return $message->getType() === NotificationService::TYPE_USER_ACCEPTED
|
||||||
|
&& $message->getTitle() === 'Invitation acceptée'
|
||||||
|
&& str_contains($message->getMessage(), 'Jane Smith a accepté l\'invitation à TechGlobal')
|
||||||
|
&& $message->getData()['organizationName'] === 'TechGlobal';
|
||||||
|
}))
|
||||||
|
->willReturn(new Envelope(new \stdClass()));
|
||||||
|
|
||||||
|
$this->service->notifyUserAcceptedInvite($recipient, $acceptedUser, $org);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNotifyUserDeactivated(): void
|
||||||
|
{
|
||||||
|
$recipient = new User(); $this->setEntityId($recipient, 1);
|
||||||
|
$removedUser = new User(); $this->setEntityId($removedUser, 3);
|
||||||
|
$removedUser->setName('Bob');
|
||||||
|
$removedUser->setSurname('Builder');
|
||||||
|
|
||||||
|
$org = new Organizations(); $this->setEntityId($org, 300);
|
||||||
|
$org->setName('BuildIt');
|
||||||
|
|
||||||
|
$this->messageBus->expects($this->once())
|
||||||
|
->method('dispatch')
|
||||||
|
->with($this->callback(function (NotificationMessage $message) {
|
||||||
|
return $message->getType() === NotificationService::TYPE_USER_DEACTIVATED
|
||||||
|
&& $message->getTitle() === 'Membre retiré'
|
||||||
|
&& str_contains($message->getMessage(), 'Bob Builder a été désactivé de BuildIt')
|
||||||
|
&& $message->getData()['userId'] === 3;
|
||||||
|
}))
|
||||||
|
->willReturn(new Envelope(new \stdClass()));
|
||||||
|
|
||||||
|
$this->service->notifyUserDeactivated($recipient, $removedUser, $org);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNotifyUserActivated(): void
|
||||||
|
{
|
||||||
|
$recipient = new User(); $this->setEntityId($recipient, 1);
|
||||||
|
$activatedUser = new User(); $this->setEntityId($activatedUser, 4);
|
||||||
|
$activatedUser->setName('Alice');
|
||||||
|
$activatedUser->setSurname('Wonder');
|
||||||
|
|
||||||
|
$org = new Organizations(); $this->setEntityId($org, 400);
|
||||||
|
$org->setName('Wonderland');
|
||||||
|
|
||||||
|
$this->messageBus->expects($this->once())
|
||||||
|
->method('dispatch')
|
||||||
|
->with($this->callback(function (NotificationMessage $message) {
|
||||||
|
return $message->getType() === 'user_activated'
|
||||||
|
&& $message->getTitle() === 'Membre réactivé'
|
||||||
|
&& str_contains($message->getMessage(), 'Alice Wonder a été réactivé dans Wonderland');
|
||||||
|
}))
|
||||||
|
->willReturn(new Envelope(new \stdClass()));
|
||||||
|
|
||||||
|
$this->service->notifyUserActivated($recipient, $activatedUser, $org);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNotifyOrganizationUpdate(): void
|
||||||
|
{
|
||||||
|
$recipient = new User(); $this->setEntityId($recipient, 1);
|
||||||
|
$org = new Organizations(); $this->setEntityId($org, 500);
|
||||||
|
$org->setName('OrgUpdate');
|
||||||
|
|
||||||
|
$this->messageBus->expects($this->once())
|
||||||
|
->method('dispatch')
|
||||||
|
->with($this->callback(function (NotificationMessage $message) {
|
||||||
|
return $message->getType() === NotificationService::TYPE_ORG_UPDATE
|
||||||
|
&& $message->getTitle() === 'Organisation mise à jour'
|
||||||
|
&& str_contains($message->getMessage(), 'L\'organisation OrgUpdate a été Renamed')
|
||||||
|
&& $message->getData()['action'] === 'Renamed';
|
||||||
|
}))
|
||||||
|
->willReturn(new Envelope(new \stdClass()));
|
||||||
|
|
||||||
|
$this->service->notifyOrganizationUpdate($recipient, $org, 'Renamed');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNotifyAppAccessChangedGranted(): void
|
||||||
|
{
|
||||||
|
$recipient = new User(); $this->setEntityId($recipient, 1);
|
||||||
|
$org = new Organizations(); $this->setEntityId($org, 600);
|
||||||
|
$org->setName('AppCorp');
|
||||||
|
|
||||||
|
$this->messageBus->expects($this->once())
|
||||||
|
->method('dispatch')
|
||||||
|
->with($this->callback(function (NotificationMessage $message) {
|
||||||
|
return $message->getType() === NotificationService::TYPE_APP_ACCESS
|
||||||
|
&& str_contains($message->getMessage(), 'L\'accès à Portal a été autorisé pour AppCorp')
|
||||||
|
&& $message->getData()['granted'] === true;
|
||||||
|
}))
|
||||||
|
->willReturn(new Envelope(new \stdClass()));
|
||||||
|
|
||||||
|
$this->service->notifyAppAccessChanged($recipient, $org, 'Portal', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNotifyAppAccessChangedRevoked(): void
|
||||||
|
{
|
||||||
|
$recipient = new User(); $this->setEntityId($recipient, 1);
|
||||||
|
$org = new Organizations(); $this->setEntityId($org, 600);
|
||||||
|
$org->setName('AppCorp');
|
||||||
|
|
||||||
|
$this->messageBus->expects($this->once())
|
||||||
|
->method('dispatch')
|
||||||
|
->with($this->callback(function (NotificationMessage $message) {
|
||||||
|
return $message->getData()['granted'] === false
|
||||||
|
&& str_contains($message->getMessage(), 'retiré');
|
||||||
|
}))
|
||||||
|
->willReturn(new Envelope(new \stdClass()));
|
||||||
|
|
||||||
|
$this->service->notifyAppAccessChanged($recipient, $org, 'Portal', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNotifyRoleChanged(): void
|
||||||
|
{
|
||||||
|
$recipient = new User(); $this->setEntityId($recipient, 1);
|
||||||
|
$targetUser = new User(); $this->setEntityId($targetUser, 5);
|
||||||
|
$targetUser->setName('Tom');
|
||||||
|
$targetUser->setSurname('Role');
|
||||||
|
|
||||||
|
$org = new Organizations(); $this->setEntityId($org, 700);
|
||||||
|
$org->setName('RoleOrg');
|
||||||
|
|
||||||
|
$this->messageBus->expects($this->once())
|
||||||
|
->method('dispatch')
|
||||||
|
->with($this->callback(function (NotificationMessage $message) {
|
||||||
|
return $message->getType() === NotificationService::TYPE_ROLE_CHANGED
|
||||||
|
&& $message->getTitle() === 'Rôle modifié'
|
||||||
|
&& str_contains($message->getMessage(), 'Tom Role a été changé en ADMIN')
|
||||||
|
&& $message->getData()['newRole'] === 'ADMIN';
|
||||||
|
}))
|
||||||
|
->willReturn(new Envelope(new \stdClass()));
|
||||||
|
|
||||||
|
$this->service->notifyRoleChanged($recipient, $targetUser, $org, 'ADMIN');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNotifyUserDeleted(): void
|
||||||
|
{
|
||||||
|
$recipient = new User(); $this->setEntityId($recipient, 1);
|
||||||
|
$deletedUser = new User(); $this->setEntityId($deletedUser, 99);
|
||||||
|
$deletedUser->setName('Del');
|
||||||
|
$deletedUser->setSurname('User');
|
||||||
|
$deletedUser->setEmail('del@test.com');
|
||||||
|
|
||||||
|
// Test without organization (null)
|
||||||
|
$this->messageBus->expects($this->once())
|
||||||
|
->method('dispatch')
|
||||||
|
->with($this->callback(function (NotificationMessage $message) {
|
||||||
|
return $message->getType() === NotificationService::TYPE_USER_REMOVED
|
||||||
|
&& $message->getTitle() === 'Utilisateur supprimé'
|
||||||
|
&& $message->getOrganizationId() === null
|
||||||
|
&& $message->getData()['userEmail'] === 'del@test.com';
|
||||||
|
}))
|
||||||
|
->willReturn(new Envelope(new \stdClass()));
|
||||||
|
|
||||||
|
$this->service->notifyUserDeleted($recipient, $deletedUser, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,271 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Service;
|
||||||
|
|
||||||
|
use App\Entity\Apps;
|
||||||
|
use App\Entity\Organizations;
|
||||||
|
use App\Entity\Roles;
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Entity\UserOrganizatonApp;
|
||||||
|
use App\Entity\UsersOrganizations;
|
||||||
|
use App\Repository\UsersOrganizationsRepository;
|
||||||
|
use App\Service\AwsService;
|
||||||
|
use App\Service\LoggerService;
|
||||||
|
use App\Service\NotificationService;
|
||||||
|
use App\Service\OrganizationsService;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Doctrine\ORM\EntityRepository;
|
||||||
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\File\Exception\FileException;
|
||||||
|
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||||
|
|
||||||
|
class OrganizationsServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
private OrganizationsService $service;
|
||||||
|
|
||||||
|
// Mocks
|
||||||
|
private MockObject|AwsService $awsService;
|
||||||
|
private MockObject|EntityManagerInterface $entityManager;
|
||||||
|
private MockObject|UsersOrganizationsRepository $uoRepository;
|
||||||
|
private MockObject|NotificationService $notificationService;
|
||||||
|
private MockObject|LoggerInterface $emailNotificationLogger;
|
||||||
|
private MockObject|LoggerService $loggerService;
|
||||||
|
|
||||||
|
private string $logoDirectory = '/tmp/logos';
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->awsService = $this->createMock(AwsService::class);
|
||||||
|
$this->entityManager = $this->createMock(EntityManagerInterface::class);
|
||||||
|
$this->uoRepository = $this->createMock(UsersOrganizationsRepository::class);
|
||||||
|
$this->notificationService = $this->createMock(NotificationService::class);
|
||||||
|
$this->emailNotificationLogger = $this->createMock(LoggerInterface::class);
|
||||||
|
$this->loggerService = $this->createMock(LoggerService::class);
|
||||||
|
|
||||||
|
// Set the ENV variable used in the service
|
||||||
|
$_ENV['S3_PORTAL_BUCKET'] = 'test-bucket';
|
||||||
|
|
||||||
|
$this->service = new OrganizationsService(
|
||||||
|
$this->logoDirectory,
|
||||||
|
$this->awsService,
|
||||||
|
$this->entityManager,
|
||||||
|
$this->uoRepository,
|
||||||
|
$this->notificationService,
|
||||||
|
$this->emailNotificationLogger,
|
||||||
|
$this->loggerService
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to set private ID property via Reflection
|
||||||
|
*/
|
||||||
|
private function setEntityId(object $entity, int $id): void
|
||||||
|
{
|
||||||
|
$reflection = new \ReflectionClass($entity);
|
||||||
|
if ($reflection->hasProperty('id')) {
|
||||||
|
$property = $reflection->getProperty('id');
|
||||||
|
// $property->setAccessible(true); // PHP < 8.1
|
||||||
|
$property->setValue($entity, $id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// TEST: handleLogo
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
public function testHandleLogoSuccess(): void
|
||||||
|
{
|
||||||
|
$org = new Organizations();
|
||||||
|
$this->setEntityId($org, 1);
|
||||||
|
$org->setName('MyOrg');
|
||||||
|
|
||||||
|
$file = $this->createMock(UploadedFile::class);
|
||||||
|
$file->method('guessExtension')->willReturn('png');
|
||||||
|
|
||||||
|
// Expect AWS Upload
|
||||||
|
$this->awsService->expects($this->once())
|
||||||
|
->method('PutDocObj')
|
||||||
|
->with(
|
||||||
|
'test-bucket',
|
||||||
|
$file,
|
||||||
|
$this->stringContains('MyOrg_'), // Filename check
|
||||||
|
'png',
|
||||||
|
'logo/'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Expect Log
|
||||||
|
$this->loggerService->expects($this->once())->method('logAWSAction');
|
||||||
|
|
||||||
|
$this->service->handleLogo($org, $file);
|
||||||
|
|
||||||
|
// Assert URL is set on entity
|
||||||
|
$this->assertStringContainsString('logo/MyOrg_', $org->getLogoUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHandleLogoThrowsException(): void
|
||||||
|
{
|
||||||
|
$org = new Organizations();
|
||||||
|
$this->setEntityId($org, 1);
|
||||||
|
$org->setName('MyOrg');
|
||||||
|
|
||||||
|
$file = $this->createMock(UploadedFile::class);
|
||||||
|
$file->method('guessExtension')->willReturn('png');
|
||||||
|
|
||||||
|
// Simulate AWS Failure
|
||||||
|
$this->awsService->method('PutDocObj')
|
||||||
|
->willThrowException(new FileException('S3 Down'));
|
||||||
|
|
||||||
|
// Expect Error Log
|
||||||
|
$this->loggerService->expects($this->once())
|
||||||
|
->method('logError')
|
||||||
|
->with('Failed to upload organization logo to S3', $this->anything());
|
||||||
|
|
||||||
|
$this->expectException(FileException::class);
|
||||||
|
$this->expectExceptionMessage('Failed to upload logo to S3: S3 Down');
|
||||||
|
|
||||||
|
$this->service->handleLogo($org, $file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// TEST: appsAccess
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
public function testAppsAccess(): void
|
||||||
|
{
|
||||||
|
$app1 = new Apps(); $this->setEntityId($app1, 10);
|
||||||
|
$app2 = new Apps(); $this->setEntityId($app2, 20);
|
||||||
|
$app3 = new Apps(); $this->setEntityId($app3, 30);
|
||||||
|
|
||||||
|
$allApps = [$app1, $app2, $app3];
|
||||||
|
$orgApps = [$app2]; // Org only has access to App 2
|
||||||
|
|
||||||
|
$result = $this->service->appsAccess($allApps, $orgApps);
|
||||||
|
|
||||||
|
$this->assertCount(3, $result);
|
||||||
|
|
||||||
|
// App 1 -> False
|
||||||
|
$this->assertSame($app1, $result[0]['entity']);
|
||||||
|
$this->assertFalse($result[0]['hasAccess']);
|
||||||
|
|
||||||
|
// App 2 -> True
|
||||||
|
$this->assertSame($app2, $result[1]['entity']);
|
||||||
|
$this->assertTrue($result[1]['hasAccess']);
|
||||||
|
|
||||||
|
// App 3 -> False
|
||||||
|
$this->assertSame($app3, $result[2]['entity']);
|
||||||
|
$this->assertFalse($result[2]['hasAccess']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// TEST: notifyOrganizationAdmins
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
public function testNotifyOrganizationAdminsUserAccepted(): void
|
||||||
|
{
|
||||||
|
// 1. Setup Data
|
||||||
|
$targetUser = new User(); $this->setEntityId($targetUser, 100);
|
||||||
|
|
||||||
|
$adminUser = new User(); $this->setEntityId($adminUser, 999);
|
||||||
|
|
||||||
|
$org = new Organizations(); $this->setEntityId($org, 50);
|
||||||
|
|
||||||
|
$data = ['user' => $targetUser, 'organization' => $org];
|
||||||
|
|
||||||
|
// 2. Setup Admin Link (The user who IS admin)
|
||||||
|
$adminUO = new UsersOrganizations();
|
||||||
|
$this->setEntityId($adminUO, 555);
|
||||||
|
$adminUO->setUsers($adminUser);
|
||||||
|
$adminUO->setOrganization($org);
|
||||||
|
|
||||||
|
// 3. Setup Role Logic
|
||||||
|
$adminRole = new Roles(); $this->setEntityId($adminRole, 1);
|
||||||
|
$adminRole->setName('ADMIN');
|
||||||
|
|
||||||
|
// 4. Setup UOA Logic (Proof that user is Admin of an App)
|
||||||
|
$uoa = new UserOrganizatonApp();
|
||||||
|
$this->setEntityId($uoa, 777);
|
||||||
|
$uoa->setUserOrganization($adminUO);
|
||||||
|
$uoa->setRole($adminRole);
|
||||||
|
$uoa->setIsActive(true);
|
||||||
|
|
||||||
|
// 5. Mocks
|
||||||
|
// Mock Roles Repo
|
||||||
|
$rolesRepo = $this->createMock(EntityRepository::class);
|
||||||
|
$rolesRepo->method('findOneBy')->with(['name' => 'ADMIN'])->willReturn($adminRole);
|
||||||
|
|
||||||
|
// Mock UO Repo (Find potential admins in org)
|
||||||
|
$this->uoRepository->expects($this->once())
|
||||||
|
->method('findBy')
|
||||||
|
->with(['organization' => $org, 'isActive' => true])
|
||||||
|
->willReturn([$adminUO]);
|
||||||
|
|
||||||
|
// Mock UOA Repo (Check if they have ADMIN role)
|
||||||
|
$uoaRepo = $this->createMock(EntityRepository::class);
|
||||||
|
$uoaRepo->method('findOneBy')->willReturn($uoa);
|
||||||
|
|
||||||
|
$this->entityManager->method('getRepository')->willReturnMap([
|
||||||
|
[Roles::class, $rolesRepo],
|
||||||
|
[UserOrganizatonApp::class, $uoaRepo],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 6. Expectations
|
||||||
|
$this->notificationService->expects($this->once())
|
||||||
|
->method('notifyUserAcceptedInvite')
|
||||||
|
->with($adminUser, $targetUser, $org);
|
||||||
|
|
||||||
|
$this->loggerService->expects($this->once())
|
||||||
|
->method('logAdminNotified')
|
||||||
|
->with([
|
||||||
|
'admin_user_id' => 999,
|
||||||
|
'target_user_id' => 100,
|
||||||
|
'organization_id' => 50,
|
||||||
|
'case' => 'USER_ACCEPTED'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 7. Run
|
||||||
|
$result = $this->service->notifyOrganizationAdmins($data, 'USER_ACCEPTED');
|
||||||
|
|
||||||
|
// The service returns the last admin UO processed (based on loop)
|
||||||
|
$this->assertSame($adminUO, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This test ensures that if the admin is the SAME person as the target user,
|
||||||
|
* they do not get notified (Skip Self Check).
|
||||||
|
*/
|
||||||
|
public function testNotifyOrganizationAdminsSkipsSelf(): void
|
||||||
|
{
|
||||||
|
$user = new User(); $this->setEntityId($user, 100);
|
||||||
|
$org = new Organizations(); $this->setEntityId($org, 50);
|
||||||
|
|
||||||
|
// Admin IS the user
|
||||||
|
$adminUO = new UsersOrganizations();
|
||||||
|
$adminUO->setUsers($user);
|
||||||
|
|
||||||
|
$roleAdmin = new Roles();
|
||||||
|
|
||||||
|
$uoa = new UserOrganizatonApp(); // active admin link
|
||||||
|
|
||||||
|
// Mocks setup
|
||||||
|
$rolesRepo = $this->createMock(EntityRepository::class);
|
||||||
|
$rolesRepo->method('findOneBy')->willReturn($roleAdmin);
|
||||||
|
|
||||||
|
$this->uoRepository->method('findBy')->willReturn([$adminUO]);
|
||||||
|
|
||||||
|
$uoaRepo = $this->createMock(EntityRepository::class);
|
||||||
|
$uoaRepo->method('findOneBy')->willReturn($uoa);
|
||||||
|
|
||||||
|
$this->entityManager->method('getRepository')->willReturnMap([
|
||||||
|
[Roles::class, $rolesRepo],
|
||||||
|
[UserOrganizatonApp::class, $uoaRepo],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Expectations: Notification service should NEVER be called
|
||||||
|
$this->notificationService->expects($this->never())->method('notifyUserAcceptedInvite');
|
||||||
|
$this->loggerService->expects($this->never())->method('logAdminNotified');
|
||||||
|
|
||||||
|
$this->service->notifyOrganizationAdmins(['user' => $user, 'organization' => $org], 'USER_ACCEPTED');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,320 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Service;
|
||||||
|
|
||||||
|
use App\Entity\Apps;
|
||||||
|
use App\Entity\Organizations;
|
||||||
|
use App\Entity\Roles;
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Entity\UserOrganizatonApp;
|
||||||
|
use App\Entity\UsersOrganizations;
|
||||||
|
use App\Service\ActionService;
|
||||||
|
use App\Service\LoggerService;
|
||||||
|
use App\Service\UserOrganizationAppService;
|
||||||
|
use App\Service\UserService;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Doctrine\ORM\EntityRepository;
|
||||||
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
|
|
||||||
|
class UserOrganizationAppServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
private UserOrganizationAppService $service;
|
||||||
|
|
||||||
|
// Mocks
|
||||||
|
private MockObject|EntityManagerInterface $entityManager;
|
||||||
|
private MockObject|ActionService $actionService;
|
||||||
|
private MockObject|Security $security;
|
||||||
|
private MockObject|UserService $userService;
|
||||||
|
private MockObject|LoggerInterface $psrLogger;
|
||||||
|
private MockObject|LoggerService $loggerService;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->entityManager = $this->createMock(EntityManagerInterface::class);
|
||||||
|
$this->actionService = $this->createMock(ActionService::class);
|
||||||
|
$this->security = $this->createMock(Security::class);
|
||||||
|
$this->userService = $this->createMock(UserService::class);
|
||||||
|
$this->psrLogger = $this->createMock(LoggerInterface::class);
|
||||||
|
$this->loggerService = $this->createMock(LoggerService::class);
|
||||||
|
|
||||||
|
$this->service = new UserOrganizationAppService(
|
||||||
|
$this->entityManager,
|
||||||
|
$this->actionService,
|
||||||
|
$this->security,
|
||||||
|
$this->userService,
|
||||||
|
$this->psrLogger,
|
||||||
|
$this->loggerService
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to set private ID property on entities.
|
||||||
|
*/
|
||||||
|
private function setEntityId(object $entity, int $id): void
|
||||||
|
{
|
||||||
|
$reflection = new \ReflectionClass($entity);
|
||||||
|
if ($reflection->hasProperty('id')) {
|
||||||
|
$property = $reflection->getProperty('id');
|
||||||
|
// $property->setAccessible(true); // Needed for PHP < 8.1
|
||||||
|
$property->setValue($entity, $id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// TEST: groupUserOrganizationAppsByApplication
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
public function testGroupUserOrganizationAppsByApplication(): void
|
||||||
|
{
|
||||||
|
// 1. Setup Apps
|
||||||
|
$app1 = new Apps(); $this->setEntityId($app1, 1);
|
||||||
|
$app2 = new Apps(); $this->setEntityId($app2, 2); // No roles for this one
|
||||||
|
|
||||||
|
// 2. Setup Existing Link
|
||||||
|
$role = new Roles(); $this->setEntityId($role, 10);
|
||||||
|
|
||||||
|
$uo = new UsersOrganizations(); $this->setEntityId($uo, 99);
|
||||||
|
|
||||||
|
$uoa = new UserOrganizatonApp();
|
||||||
|
$this->setEntityId($uoa, 500);
|
||||||
|
$uoa->setApplication($app1);
|
||||||
|
$uoa->setRole($role);
|
||||||
|
$uoa->setUserOrganization($uo);
|
||||||
|
|
||||||
|
// 3. Run
|
||||||
|
$result = $this->service->groupUserOrganizationAppsByApplication(
|
||||||
|
[$uoa],
|
||||||
|
[$app1, $app2],
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Assert
|
||||||
|
$this->assertArrayHasKey(1, $result);
|
||||||
|
$this->assertArrayHasKey(2, $result);
|
||||||
|
|
||||||
|
// Check App 1 (Has existing link)
|
||||||
|
$this->assertEquals(99, $result[1]['uoId']);
|
||||||
|
$this->assertEquals([10], $result[1]['selectedRoleIds']);
|
||||||
|
|
||||||
|
// Check App 2 (Empty default)
|
||||||
|
$this->assertNull($result[2]['uoId']);
|
||||||
|
$this->assertEmpty($result[2]['selectedRoleIds']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// TEST: deactivateAllUserOrganizationsAppLinks
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
public function testDeactivateAllLinksSuccess(): void
|
||||||
|
{
|
||||||
|
$uo = new UsersOrganizations();
|
||||||
|
$user = new User();
|
||||||
|
$org = new Organizations();
|
||||||
|
$uo->setUsers($user);
|
||||||
|
$uo->setOrganization($org);
|
||||||
|
|
||||||
|
$app = new Apps();
|
||||||
|
$this->setEntityId($app, 1);
|
||||||
|
$role = new Roles();
|
||||||
|
$this->setEntityId($role, 10);
|
||||||
|
|
||||||
|
$uoa = new UserOrganizatonApp();
|
||||||
|
$this->setEntityId($uoa, 555);
|
||||||
|
$uoa->setApplication($app);
|
||||||
|
$uoa->setRole($role);
|
||||||
|
$uoa->setIsActive(true);
|
||||||
|
|
||||||
|
// Mock Repository
|
||||||
|
$repo = $this->createMock(EntityRepository::class);
|
||||||
|
$repo->method('findBy')->willReturn([$uoa]);
|
||||||
|
$this->entityManager->method('getRepository')->willReturn($repo);
|
||||||
|
|
||||||
|
// Expectations
|
||||||
|
$this->actionService->expects($this->once())->method('createAction');
|
||||||
|
$this->entityManager->expects($this->once())->method('persist')->with($uoa);
|
||||||
|
$this->loggerService->expects($this->once())->method('logUOALinkDeactivated');
|
||||||
|
|
||||||
|
$this->service->deactivateAllUserOrganizationsAppLinks($uo, null);
|
||||||
|
|
||||||
|
$this->assertFalse($uoa->isActive());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDeactivateHandlesException(): void
|
||||||
|
{
|
||||||
|
$uo = new UsersOrganizations();
|
||||||
|
|
||||||
|
// The service needs a User to create an Action log
|
||||||
|
$user = new User();
|
||||||
|
$this->setEntityId($user, 99);
|
||||||
|
$uo->setUsers($user); // <--- Assign the user!
|
||||||
|
|
||||||
|
// Also needs an Org for the Action log
|
||||||
|
$org = new Organizations();
|
||||||
|
$this->setEntityId($org, 88);
|
||||||
|
$uo->setOrganization($org);
|
||||||
|
|
||||||
|
$app = new Apps(); $this->setEntityId($app, 1);
|
||||||
|
$role = new Roles(); $this->setEntityId($role, 1);
|
||||||
|
|
||||||
|
$realUoa = new UserOrganizatonApp();
|
||||||
|
$this->setEntityId($realUoa, 100);
|
||||||
|
$realUoa->setApplication($app);
|
||||||
|
$realUoa->setRole($role);
|
||||||
|
$realUoa->setIsActive(true);
|
||||||
|
|
||||||
|
$repo = $this->createMock(EntityRepository::class);
|
||||||
|
$repo->method('findBy')->willReturn([$realUoa]);
|
||||||
|
$this->entityManager->method('getRepository')->willReturn($repo);
|
||||||
|
|
||||||
|
// Throw exception on persist
|
||||||
|
$this->entityManager->method('persist')->willThrowException(new \Exception('DB Error'));
|
||||||
|
|
||||||
|
// Expect Logger Critical
|
||||||
|
$this->loggerService->expects($this->once())->method('logCritical');
|
||||||
|
|
||||||
|
$this->service->deactivateAllUserOrganizationsAppLinks($uo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// TEST: syncRolesForUserOrganizationApp
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
public function testSyncRolesAddsNewRole(): void
|
||||||
|
{
|
||||||
|
// Setup
|
||||||
|
$actingUser = new User(); $this->setEntityId($actingUser, 1);
|
||||||
|
$targetUser = new User(); $this->setEntityId($targetUser, 2);
|
||||||
|
|
||||||
|
$org = new Organizations(); $this->setEntityId($org, 10);
|
||||||
|
$uo = new UsersOrganizations();
|
||||||
|
$uo->setOrganization($org);
|
||||||
|
$uo->setUsers($targetUser);
|
||||||
|
|
||||||
|
$app = new Apps(); $this->setEntityId($app, 5);
|
||||||
|
$app->setName('App1');
|
||||||
|
|
||||||
|
$roleId = 20;
|
||||||
|
$role = new Roles();
|
||||||
|
$role->setName('EDITOR');
|
||||||
|
$this->setEntityId($role, $roleId);
|
||||||
|
|
||||||
|
// Mock Repositories
|
||||||
|
$uoaRepo = $this->createMock(EntityRepository::class);
|
||||||
|
$uoaRepo->method('findBy')->willReturn([]); // No existing roles
|
||||||
|
|
||||||
|
$roleRepo = $this->createMock(EntityRepository::class);
|
||||||
|
$roleRepo->method('find')->with($roleId)->willReturn($role);
|
||||||
|
|
||||||
|
$this->entityManager->method('getRepository')->willReturnMap([
|
||||||
|
[UserOrganizatonApp::class, $uoaRepo],
|
||||||
|
[Roles::class, $roleRepo],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Expect creation
|
||||||
|
$this->entityManager->expects($this->once())->method('persist')->with($this->isInstanceOf(UserOrganizatonApp::class));
|
||||||
|
$this->entityManager->expects($this->once())->method('flush');
|
||||||
|
$this->actionService->expects($this->once())->method('createAction');
|
||||||
|
|
||||||
|
// Run
|
||||||
|
$this->service->syncRolesForUserOrganizationApp($uo, $app, [(string)$roleId], $actingUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSyncRolesDeactivatesUnselectedRole(): void
|
||||||
|
{
|
||||||
|
$actingUser = new User(); $this->setEntityId($actingUser, 1);
|
||||||
|
$targetUser = new User(); $this->setEntityId($targetUser, 2);
|
||||||
|
$org = new Organizations(); $this->setEntityId($org, 10);
|
||||||
|
|
||||||
|
$uo = new UsersOrganizations();
|
||||||
|
$uo->setOrganization($org);
|
||||||
|
$uo->setUsers($targetUser);
|
||||||
|
|
||||||
|
$app = new Apps(); $this->setEntityId($app, 5);
|
||||||
|
$app->setName('App1');
|
||||||
|
|
||||||
|
// Existing active role
|
||||||
|
$role = new Roles(); $this->setEntityId($role, 30);
|
||||||
|
$role->setName('VIEWER');
|
||||||
|
|
||||||
|
$existingUoa = new UserOrganizatonApp();
|
||||||
|
$this->setEntityId($existingUoa, 999);
|
||||||
|
$existingUoa->setRole($role);
|
||||||
|
$existingUoa->setApplication($app);
|
||||||
|
$existingUoa->setUserOrganization($uo);
|
||||||
|
$existingUoa->setIsActive(true);
|
||||||
|
|
||||||
|
// Repos
|
||||||
|
$uoaRepo = $this->createMock(EntityRepository::class);
|
||||||
|
$uoaRepo->method('findBy')->willReturn([$existingUoa]);
|
||||||
|
|
||||||
|
$this->entityManager->method('getRepository')->willReturnMap([
|
||||||
|
[UserOrganizatonApp::class, $uoaRepo],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// We pass empty array [] as selected roles -> expect deactivation
|
||||||
|
$this->service->syncRolesForUserOrganizationApp($uo, $app, [], $actingUser);
|
||||||
|
|
||||||
|
$this->assertFalse($existingUoa->isActive());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSyncRolesHandlesSuperAdminLogic(): void
|
||||||
|
{
|
||||||
|
// Setup
|
||||||
|
$actingUser = new User(); $this->setEntityId($actingUser, 1);
|
||||||
|
$targetUser = new User(); $this->setEntityId($targetUser, 2);
|
||||||
|
$uo = new UsersOrganizations();
|
||||||
|
$uo->setUsers($targetUser);
|
||||||
|
|
||||||
|
$org = new Organizations();
|
||||||
|
$this->setEntityId($org, 500); // <--- Give the Org an ID!
|
||||||
|
$uo->setOrganization($org);
|
||||||
|
|
||||||
|
$app = new Apps(); $this->setEntityId($app, 1);
|
||||||
|
$app->setName('Portal');
|
||||||
|
|
||||||
|
// Roles
|
||||||
|
$superAdminRole = new Roles();
|
||||||
|
$superAdminRole->setName('SUPER ADMIN');
|
||||||
|
$this->setEntityId($superAdminRole, 100);
|
||||||
|
|
||||||
|
$adminRole = new Roles();
|
||||||
|
$adminRole->setName('ADMIN');
|
||||||
|
$this->setEntityId($adminRole, 101);
|
||||||
|
|
||||||
|
// Repositories Configuration
|
||||||
|
$uoaRepo = $this->createMock(EntityRepository::class);
|
||||||
|
// 1. findBy (initial check) -> returns empty
|
||||||
|
// 2. findOneBy (inside ensureAdminRoleForSuperAdmin) -> returns null (Admin link doesn't exist yet)
|
||||||
|
$uoaRepo->method('findBy')->willReturn([]);
|
||||||
|
$uoaRepo->method('findOneBy')->willReturn(null);
|
||||||
|
|
||||||
|
$roleRepo = $this->createMock(EntityRepository::class);
|
||||||
|
$roleRepo->method('find')->with(100)->willReturn($superAdminRole);
|
||||||
|
$roleRepo->method('findOneBy')->with(['name' => 'ADMIN'])->willReturn($adminRole);
|
||||||
|
|
||||||
|
$this->entityManager->method('getRepository')->willReturnMap([
|
||||||
|
[UserOrganizatonApp::class, $uoaRepo],
|
||||||
|
[Roles::class, $roleRepo],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Expectations
|
||||||
|
|
||||||
|
// 1. UserService should be called to sync SUPER ADMIN
|
||||||
|
$this->userService->expects($this->once())
|
||||||
|
->method('syncUserRoles')
|
||||||
|
->with($targetUser, 'SUPER ADMIN', true);
|
||||||
|
|
||||||
|
// 2. EntityManager should persist:
|
||||||
|
// - The new SUPER ADMIN link
|
||||||
|
// - The new ADMIN link (automatically created)
|
||||||
|
$this->entityManager->expects($this->exactly(2))
|
||||||
|
->method('persist')
|
||||||
|
->with($this->isInstanceOf(UserOrganizatonApp::class));
|
||||||
|
|
||||||
|
// Run
|
||||||
|
$this->service->syncRolesForUserOrganizationApp($uo, $app, ['100'], $actingUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,186 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Service;
|
||||||
|
|
||||||
|
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 App\Service\UserOrganizationService;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Doctrine\ORM\EntityRepository;
|
||||||
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class UserOrganizationServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
private UserOrganizationService $service;
|
||||||
|
|
||||||
|
private MockObject|UserOrganizationAppService $userOrganizationAppService;
|
||||||
|
private MockObject|EntityManagerInterface $entityManager;
|
||||||
|
private MockObject|ActionService $actionService;
|
||||||
|
private MockObject|LoggerService $loggerService;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->userOrganizationAppService = $this->createMock(UserOrganizationAppService::class);
|
||||||
|
$this->entityManager = $this->createMock(EntityManagerInterface::class);
|
||||||
|
$this->actionService = $this->createMock(ActionService::class);
|
||||||
|
$this->loggerService = $this->createMock(LoggerService::class);
|
||||||
|
|
||||||
|
$this->service = new UserOrganizationService(
|
||||||
|
$this->userOrganizationAppService,
|
||||||
|
$this->entityManager,
|
||||||
|
$this->actionService,
|
||||||
|
$this->loggerService
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to set private ID property on entities via Reflection.
|
||||||
|
* Essential because your service calls getId() on entities.
|
||||||
|
*/
|
||||||
|
private function setEntityId(object $entity, int $id): void
|
||||||
|
{
|
||||||
|
$reflection = new \ReflectionClass($entity);
|
||||||
|
if ($reflection->hasProperty('id')) {
|
||||||
|
$property = $reflection->getProperty('id');
|
||||||
|
$property->setValue($entity, $id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDeactivateAllLinksByUser(): void
|
||||||
|
{
|
||||||
|
// 1. Setup Data
|
||||||
|
$actingUser = new User();
|
||||||
|
$this->setEntityId($actingUser, 1);
|
||||||
|
|
||||||
|
$targetUser = new User();
|
||||||
|
$this->setEntityId($targetUser, 2);
|
||||||
|
|
||||||
|
$org = new Organizations();
|
||||||
|
$this->setEntityId($org, 100);
|
||||||
|
$org->setName('Test Org');
|
||||||
|
|
||||||
|
// Create a dummy UsersOrganizations link
|
||||||
|
$uo = new UsersOrganizations();
|
||||||
|
$uo->setUsers($targetUser);
|
||||||
|
$uo->setOrganization($org);
|
||||||
|
$uo->setIsActive(true);
|
||||||
|
// Assuming there is an ID on UO, though not strictly used in the logic provided
|
||||||
|
$this->setEntityId($uo, 555);
|
||||||
|
|
||||||
|
// 2. Mock Repository
|
||||||
|
$repo = $this->createMock(EntityRepository::class);
|
||||||
|
$repo->expects($this->once())
|
||||||
|
->method('findBy')
|
||||||
|
->with(['users' => $targetUser, 'isActive' => true])
|
||||||
|
->willReturn([$uo]);
|
||||||
|
|
||||||
|
$this->entityManager->expects($this->once())
|
||||||
|
->method('getRepository')
|
||||||
|
->with(UsersOrganizations::class)
|
||||||
|
->willReturn($repo);
|
||||||
|
|
||||||
|
// 3. Expect Side Effects on Dependencies
|
||||||
|
|
||||||
|
// Expect deactivation of app links
|
||||||
|
$this->userOrganizationAppService->expects($this->once())
|
||||||
|
->method('deactivateAllUserOrganizationsAppLinks')
|
||||||
|
->with($uo);
|
||||||
|
|
||||||
|
// Expect Logging
|
||||||
|
$this->loggerService->expects($this->once())
|
||||||
|
->method('logOrganizationInformation')
|
||||||
|
->with(100, 1, 'Uo link deactivated'); // OrgID, ActingUserID
|
||||||
|
|
||||||
|
// Expect Persist
|
||||||
|
$this->entityManager->expects($this->once())
|
||||||
|
->method('persist')
|
||||||
|
->with($uo);
|
||||||
|
|
||||||
|
// Expect Action Creation
|
||||||
|
$this->actionService->expects($this->once())
|
||||||
|
->method('createAction')
|
||||||
|
->with("Deactivate UO link", $actingUser, $org, 'Test Org');
|
||||||
|
|
||||||
|
// 4. Run Method
|
||||||
|
$this->service->deactivateAllUserOrganizationLinks($actingUser, $targetUser, null);
|
||||||
|
|
||||||
|
// 5. Assert State Change
|
||||||
|
$this->assertFalse($uo->isActive(), 'The user-organization link should have been set to inactive.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDeactivateAllLinksByOrganization(): void
|
||||||
|
{
|
||||||
|
// 1. Setup Data
|
||||||
|
$actingUser = new User();
|
||||||
|
$this->setEntityId($actingUser, 1);
|
||||||
|
|
||||||
|
$org = new Organizations();
|
||||||
|
$this->setEntityId($org, 200);
|
||||||
|
$org->setName('Org B');
|
||||||
|
|
||||||
|
$uo1 = new UsersOrganizations();
|
||||||
|
$uo1->setOrganization($org);
|
||||||
|
$uo1->setIsActive(true);
|
||||||
|
|
||||||
|
$uo2 = new UsersOrganizations();
|
||||||
|
$uo2->setOrganization($org);
|
||||||
|
$uo2->setIsActive(true);
|
||||||
|
|
||||||
|
// 2. Mock Repository to return 2 items
|
||||||
|
$repo = $this->createMock(EntityRepository::class);
|
||||||
|
$repo->expects($this->once())
|
||||||
|
->method('findBy')
|
||||||
|
->with(['organization' => $org, 'isActive' => true])
|
||||||
|
->willReturn([$uo1, $uo2]);
|
||||||
|
|
||||||
|
$this->entityManager->expects($this->once())
|
||||||
|
->method('getRepository')
|
||||||
|
->with(UsersOrganizations::class)
|
||||||
|
->willReturn($repo);
|
||||||
|
|
||||||
|
// 3. Expect Side Effects (Called twice, once for each UO)
|
||||||
|
$this->userOrganizationAppService->expects($this->exactly(2))
|
||||||
|
->method('deactivateAllUserOrganizationsAppLinks');
|
||||||
|
|
||||||
|
$this->loggerService->expects($this->exactly(2))
|
||||||
|
->method('logOrganizationInformation');
|
||||||
|
|
||||||
|
$this->entityManager->expects($this->exactly(2))
|
||||||
|
->method('persist');
|
||||||
|
|
||||||
|
$this->actionService->expects($this->exactly(2))
|
||||||
|
->method('createAction');
|
||||||
|
|
||||||
|
// 4. Run Method (User is null, Organization is provided)
|
||||||
|
$this->service->deactivateAllUserOrganizationLinks($actingUser, null, $org);
|
||||||
|
|
||||||
|
// 5. Assert State
|
||||||
|
$this->assertFalse($uo1->isActive());
|
||||||
|
$this->assertFalse($uo2->isActive());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDeactivateDoesNothingIfNoLinksFound(): void
|
||||||
|
{
|
||||||
|
$actingUser = new User();
|
||||||
|
$targetUser = new User();
|
||||||
|
|
||||||
|
// Repo returns empty array
|
||||||
|
$repo = $this->createMock(EntityRepository::class);
|
||||||
|
$repo->method('findBy')->willReturn([]);
|
||||||
|
|
||||||
|
$this->entityManager->method('getRepository')->willReturn($repo);
|
||||||
|
|
||||||
|
// Ensure services are NEVER called
|
||||||
|
$this->userOrganizationAppService->expects($this->never())->method('deactivateAllUserOrganizationsAppLinks');
|
||||||
|
$this->loggerService->expects($this->never())->method('logOrganizationInformation');
|
||||||
|
$this->entityManager->expects($this->never())->method('persist');
|
||||||
|
$this->actionService->expects($this->never())->method('createAction');
|
||||||
|
|
||||||
|
$this->service->deactivateAllUserOrganizationLinks($actingUser, $targetUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,369 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Service;
|
||||||
|
|
||||||
|
use App\Entity\Organizations;
|
||||||
|
use App\Entity\Roles;
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Entity\UserOrganizatonApp;
|
||||||
|
use App\Entity\UsersOrganizations;
|
||||||
|
use App\Service\ActionService;
|
||||||
|
use App\Service\AwsService;
|
||||||
|
use App\Service\EmailService;
|
||||||
|
use App\Service\LoggerService;
|
||||||
|
use App\Service\OrganizationsService;
|
||||||
|
use App\Service\UserService;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Doctrine\ORM\EntityNotFoundException;
|
||||||
|
use Doctrine\ORM\EntityRepository;
|
||||||
|
use League\Bundle\OAuth2ServerBundle\Model\AccessToken;
|
||||||
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
|
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||||
|
|
||||||
|
class UserServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
private UserService $userService;
|
||||||
|
|
||||||
|
// Mocks
|
||||||
|
private MockObject|EntityManagerInterface $entityManager;
|
||||||
|
private MockObject|Security $security;
|
||||||
|
private MockObject|AwsService $awsService;
|
||||||
|
private MockObject|LoggerService $loggerService;
|
||||||
|
private MockObject|ActionService $actionService;
|
||||||
|
private MockObject|EmailService $emailService;
|
||||||
|
private MockObject|OrganizationsService $organizationsService;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->entityManager = $this->createMock(EntityManagerInterface::class);
|
||||||
|
$this->security = $this->createMock(Security::class);
|
||||||
|
$this->awsService = $this->createMock(AwsService::class);
|
||||||
|
$this->actionService = $this->createMock(ActionService::class);
|
||||||
|
$this->emailService = $this->createMock(EmailService::class);
|
||||||
|
$this->organizationsService = $this->createMock(OrganizationsService::class);
|
||||||
|
|
||||||
|
// HANDLING READONLY LOGGER SERVICE
|
||||||
|
// PHPUnit 10+ generally handles readonly classes fine.
|
||||||
|
// If your LoggerService is 'final readonly', you cannot mock it easily.
|
||||||
|
// Assuming it is just 'readonly class LoggerService':
|
||||||
|
$this->loggerService = $this->createMock(LoggerService::class);
|
||||||
|
|
||||||
|
$this->userService = new UserService(
|
||||||
|
$this->entityManager,
|
||||||
|
$this->security,
|
||||||
|
$this->awsService,
|
||||||
|
$this->loggerService,
|
||||||
|
$this->actionService,
|
||||||
|
$this->emailService,
|
||||||
|
$this->organizationsService
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGenerateRandomPassword(): void
|
||||||
|
{
|
||||||
|
$password = $this->userService->generateRandomPassword();
|
||||||
|
$this->assertEquals(50, strlen($password));
|
||||||
|
$this->assertMatchesRegularExpression('/[a-zA-Z0-9!@#$%^&*()_+]+/', $password);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsUserConnectedReturnsTrueIfTokenValid(): void
|
||||||
|
{
|
||||||
|
$userIdentifier = 'test@example.com';
|
||||||
|
|
||||||
|
// Mock the Repository for AccessToken
|
||||||
|
$repo = $this->createMock(EntityRepository::class);
|
||||||
|
|
||||||
|
// Mock a token that expires in the future
|
||||||
|
$token = $this->createMock(AccessToken::class);
|
||||||
|
$token->method('getExpiry')->willReturn(new \DateTimeImmutable('+1 hour'));
|
||||||
|
|
||||||
|
$repo->expects($this->once())
|
||||||
|
->method('findBy')
|
||||||
|
->with(['userIdentifier' => $userIdentifier, 'revoked' => false])
|
||||||
|
->willReturn([$token]);
|
||||||
|
|
||||||
|
$this->entityManager->expects($this->once())
|
||||||
|
->method('getRepository')
|
||||||
|
->with(AccessToken::class)
|
||||||
|
->willReturn($repo);
|
||||||
|
|
||||||
|
$result = $this->userService->isUserConnected($userIdentifier);
|
||||||
|
$this->assertTrue($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsUserConnectedReturnsFalseIfTokenExpired(): void
|
||||||
|
{
|
||||||
|
$userIdentifier = 'test@example.com';
|
||||||
|
$repo = $this->createMock(EntityRepository::class);
|
||||||
|
|
||||||
|
$token = $this->createMock(AccessToken::class);
|
||||||
|
$token->method('getExpiry')->willReturn(new \DateTimeImmutable('-1 hour'));
|
||||||
|
|
||||||
|
$repo->method('findBy')->willReturn([$token]);
|
||||||
|
$this->entityManager->method('getRepository')->willReturn($repo);
|
||||||
|
|
||||||
|
$result = $this->userService->isUserConnected($userIdentifier);
|
||||||
|
$this->assertFalse($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetUserByIdentifierFound(): void
|
||||||
|
{
|
||||||
|
$identifier = 'user@test.com';
|
||||||
|
$user = new User();
|
||||||
|
$user->setEmail($identifier);
|
||||||
|
|
||||||
|
$repo = $this->createMock(EntityRepository::class);
|
||||||
|
$repo->expects($this->once())
|
||||||
|
->method('findOneBy')
|
||||||
|
->with(['email' => $identifier])
|
||||||
|
->willReturn($user);
|
||||||
|
|
||||||
|
$this->entityManager->method('getRepository')->with(User::class)->willReturn($repo);
|
||||||
|
|
||||||
|
$result = $this->userService->getUserByIdentifier($identifier);
|
||||||
|
$this->assertSame($user, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetUserByIdentifierNotFound(): void
|
||||||
|
{
|
||||||
|
$identifier = 'unknown@test.com';
|
||||||
|
|
||||||
|
$repo = $this->createMock(EntityRepository::class);
|
||||||
|
$repo->method('findOneBy')->willReturn(null);
|
||||||
|
$this->entityManager->method('getRepository')->with(User::class)->willReturn($repo);
|
||||||
|
|
||||||
|
// Expect Logger to be called
|
||||||
|
$this->loggerService->expects($this->once())
|
||||||
|
->method('logEntityNotFound')
|
||||||
|
->with('User', ['user_identifier' => $identifier], null);
|
||||||
|
|
||||||
|
$this->expectException(EntityNotFoundException::class);
|
||||||
|
$this->expectExceptionMessage(UserService::NOT_FOUND);
|
||||||
|
|
||||||
|
$this->userService->getUserByIdentifier($identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHasAccessToReturnsTrueForSuperAdmin(): void
|
||||||
|
{
|
||||||
|
$this->security->method('isGranted')->with('ROLE_SUPER_ADMIN')->willReturn(true);
|
||||||
|
$user = new User(); // Dummy user
|
||||||
|
$this->assertTrue($this->userService->hasAccessTo($user));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHasAccessToReturnsTrueForSelf(): void
|
||||||
|
{
|
||||||
|
$this->security->method('isGranted')->willReturn(false);
|
||||||
|
|
||||||
|
$currentUser = new User();
|
||||||
|
$currentUser->setEmail('me@test.com');
|
||||||
|
|
||||||
|
$targetUser = new User();
|
||||||
|
$targetUser->setEmail('me@test.com');
|
||||||
|
|
||||||
|
$this->security->method('getUser')->willReturn($currentUser);
|
||||||
|
|
||||||
|
// skipSelfCheck = false (default)
|
||||||
|
$this->assertTrue($this->userService->hasAccessTo($targetUser));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHandleProfilePictureUploadsAndLogs(): void
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
$user->setName('John');
|
||||||
|
$user->setSurname('Doe');
|
||||||
|
|
||||||
|
// Mock UploadedFile
|
||||||
|
$file = $this->createMock(UploadedFile::class);
|
||||||
|
$file->method('guessExtension')->willReturn('jpg');
|
||||||
|
|
||||||
|
// Expect AWS Call
|
||||||
|
$this->awsService->expects($this->once())
|
||||||
|
->method('PutDocObj')
|
||||||
|
->with(
|
||||||
|
$this->anything(), // ENV variable usually
|
||||||
|
$file,
|
||||||
|
$this->stringContains('JohnDoe_'),
|
||||||
|
'jpg',
|
||||||
|
'profile/'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Expect Logger Call
|
||||||
|
$this->loggerService->expects($this->once())
|
||||||
|
->method('logAWSAction');
|
||||||
|
|
||||||
|
// Set fake ENV for test context if needed, or ignore the argument in mock
|
||||||
|
$_ENV['S3_PORTAL_BUCKET'] = 'test-bucket';
|
||||||
|
|
||||||
|
$this->userService->handleProfilePicture($user, $file);
|
||||||
|
|
||||||
|
$this->assertStringContainsString('profile/JohnDoe_', $user->getPictureUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSyncUserRolesAddsRole(): void
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
$user->setRoles(['ROLE_USER']);
|
||||||
|
|
||||||
|
$this->loggerService->expects($this->once())->method('logRoleAssignment');
|
||||||
|
|
||||||
|
$this->userService->syncUserRoles($user, 'ADMIN', true);
|
||||||
|
|
||||||
|
$this->assertContains('ROLE_ADMIN', $user->getRoles());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSyncUserRolesRemovesRole(): void
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
$user->setRoles(['ROLE_USER', 'ROLE_ADMIN']);
|
||||||
|
|
||||||
|
// Mock repositories to ensure no other org gives this role
|
||||||
|
$repoUO = $this->createMock(EntityRepository::class);
|
||||||
|
$repoUO->method('findBy')->willReturn([]); // No active org links
|
||||||
|
|
||||||
|
$this->entityManager->method('getRepository')
|
||||||
|
->willReturnMap([
|
||||||
|
[UsersOrganizations::class, $repoUO]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->userService->syncUserRoles($user, 'ADMIN', false);
|
||||||
|
|
||||||
|
$this->assertNotContains('ROLE_ADMIN', $user->getRoles());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsPasswordStrong(): void
|
||||||
|
{
|
||||||
|
$this->assertTrue($this->userService->isPasswordStrong('StrongP@ss1')); // Chars + Digits + Special + Length
|
||||||
|
$this->assertFalse($this->userService->isPasswordStrong('weak')); // Too short
|
||||||
|
$this->assertFalse($this->userService->isPasswordStrong('123456789')); // No letters
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCreateNewUserSuccess(): void
|
||||||
|
{
|
||||||
|
$newUser = new User();
|
||||||
|
$newUser->setName('jane');
|
||||||
|
$newUser->setSurname('doe');
|
||||||
|
$newUser->setEmail('jane@doe.com');
|
||||||
|
|
||||||
|
$actingUser = new User();
|
||||||
|
$this->setEntityId($actingUser, 99); // Give acting user an ID
|
||||||
|
$actingUser->setEmail('admin@test.com');
|
||||||
|
|
||||||
|
// When persist is called, we force an ID onto $newUser to simulate DB insertion
|
||||||
|
$this->entityManager->expects($this->exactly(2))
|
||||||
|
->method('persist')
|
||||||
|
->with($newUser)
|
||||||
|
->willReturnCallback(function ($entity) {
|
||||||
|
$this->setEntityId($entity, 123); // Simulate DB assigning ID 123
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->entityManager->expects($this->exactly(2))->method('flush');
|
||||||
|
|
||||||
|
// Now expects ID 123
|
||||||
|
$this->loggerService->expects($this->once())
|
||||||
|
->method('logUserCreated')
|
||||||
|
->with(123, 99);
|
||||||
|
|
||||||
|
$this->emailService->expects($this->once())->method('sendPasswordSetupEmail');
|
||||||
|
$this->actionService->expects($this->once())->method('createAction');
|
||||||
|
|
||||||
|
$this->userService->createNewUser($newUser, $actingUser, null);
|
||||||
|
|
||||||
|
// Assertions
|
||||||
|
$this->assertEquals('Jane', $newUser->getName());
|
||||||
|
$this->assertEquals(123, $newUser->getId()); // Verify ID was "generated"
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLinkUserToOrganization(): void
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
$this->setEntityId($user, 10); // Pre-set ID for existing user
|
||||||
|
|
||||||
|
$org = new Organizations();
|
||||||
|
$this->setEntityId($org, 50); // Pre-set ID for org
|
||||||
|
|
||||||
|
$actingUser = new User();
|
||||||
|
$this->setEntityId($actingUser, 99);
|
||||||
|
|
||||||
|
// Capture the UsersOrganizations entity when it is persisted to give it an ID
|
||||||
|
$this->entityManager->expects($this->exactly(2))
|
||||||
|
->method('persist')
|
||||||
|
->willReturnCallback(function ($entity) use ($user) {
|
||||||
|
if ($entity instanceof UsersOrganizations) {
|
||||||
|
// This is the UO entity link (Call 1)
|
||||||
|
$this->setEntityId($entity, 555);
|
||||||
|
} elseif ($entity instanceof User && $entity === $user) {
|
||||||
|
// This is the User entity inside generatePasswordToken (Call 2)
|
||||||
|
// The ID is already set, so we do nothing here.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->entityManager->expects($this->exactly(2))->method('flush');
|
||||||
|
|
||||||
|
|
||||||
|
// Now the logger will receive valid Integers instead of null
|
||||||
|
$this->loggerService->expects($this->once())
|
||||||
|
->method('logUserOrganizationLinkCreated')
|
||||||
|
->with(10, 50, 99, 555);
|
||||||
|
|
||||||
|
$this->emailService->expects($this->once())->method('sendPasswordSetupEmail');
|
||||||
|
$this->organizationsService->expects($this->once())->method('notifyOrganizationAdmins');
|
||||||
|
|
||||||
|
$result = $this->userService->linkUserToOrganization($user, $org, $actingUser);
|
||||||
|
|
||||||
|
$this->assertInstanceOf(UsersOrganizations::class, $result);
|
||||||
|
$this->assertEquals(555, $result->getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsAdminOfOrganizationReturnsTrue(): void
|
||||||
|
{
|
||||||
|
$org = new Organizations();
|
||||||
|
|
||||||
|
$currentUser = new User();
|
||||||
|
$currentUser->setEmail('admin@test.com');
|
||||||
|
|
||||||
|
// Mock Security User
|
||||||
|
$this->security->method('getUser')->willReturn($currentUser);
|
||||||
|
$this->security->method('isGranted')->with('ROLE_ADMIN')->willReturn(true);
|
||||||
|
|
||||||
|
// 1. getUserByIdentifier (internal call) mocks
|
||||||
|
$userRepo = $this->createMock(EntityRepository::class);
|
||||||
|
$userRepo->method('findOneBy')->with(['email' => 'admin@test.com'])->willReturn($currentUser);
|
||||||
|
|
||||||
|
// 2. UsersOrganizations mock
|
||||||
|
$uoRepo = $this->createMock(EntityRepository::class);
|
||||||
|
$uo = new UsersOrganizations();
|
||||||
|
$uoRepo->method('findOneBy')->willReturn($uo);
|
||||||
|
|
||||||
|
// 3. Roles mock
|
||||||
|
$rolesRepo = $this->createMock(EntityRepository::class);
|
||||||
|
$adminRole = new Roles();
|
||||||
|
$adminRole->setName('ADMIN');
|
||||||
|
$rolesRepo->method('findOneBy')->with(['name' => 'ADMIN'])->willReturn($adminRole);
|
||||||
|
|
||||||
|
// 4. UserOrganizatonApp mock (The link checking if they are admin active)
|
||||||
|
$uoaRepo = $this->createMock(EntityRepository::class);
|
||||||
|
$uoa = new UserOrganizatonApp();
|
||||||
|
$uoaRepo->method('findOneBy')->willReturn($uoa); // Returns an object, so true
|
||||||
|
|
||||||
|
// Configure EntityManager to return these repos based on class
|
||||||
|
$this->entityManager->method('getRepository')->willReturnMap([
|
||||||
|
[User::class, $userRepo],
|
||||||
|
[UsersOrganizations::class, $uoRepo],
|
||||||
|
[Roles::class, $rolesRepo],
|
||||||
|
[UserOrganizatonApp::class, $uoaRepo],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = $this->userService->isAdminOfOrganization($org);
|
||||||
|
$this->assertTrue($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setEntityId(object $entity, int $id): void
|
||||||
|
{
|
||||||
|
$reflection = new \ReflectionClass($entity);
|
||||||
|
$property = $reflection->getProperty('id');
|
||||||
|
// $property->setAccessible(true); // Required for PHP < 8.1
|
||||||
|
$property->setValue($entity, $id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
"Too many failed login attempts, please try again later.": "Trop de tentatives de connexion. Veuillez réessayer plus tard."
|
||||||
Loading…
Reference in New Issue