Merge branch 'develop' into dockerize-portal
This commit is contained in:
commit
3113313ad3
|
|
@ -1,6 +1,7 @@
|
|||
|
||||
###> symfony/framework-bundle ###
|
||||
/.env.local
|
||||
/.env.test
|
||||
/.env.local.php
|
||||
/.env.*.local
|
||||
/config/secrets/prod/prod.decrypt.private.php
|
||||
|
|
@ -15,11 +16,6 @@
|
|||
.phpunit.result.cache
|
||||
###< phpunit/phpunit ###
|
||||
|
||||
###> symfony/phpunit-bridge ###
|
||||
.phpunit.result.cache
|
||||
/phpunit.xml
|
||||
###< symfony/phpunit-bridge ###
|
||||
|
||||
###> symfony/asset-mapper ###
|
||||
/public/assets/
|
||||
/assets/vendor/
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
<excludeFolder url="file://$MODULE_DIR$/vendor/mtdowling/jmespath.php" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-client" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/ralouphie/getallheaders" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/rate-limiter" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
|
|
|
|||
|
|
@ -179,6 +179,8 @@
|
|||
<path value="$PROJECT_DIR$/vendor/mtdowling/jmespath.php" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/http-client" />
|
||||
<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>
|
||||
</component>
|
||||
<component name="PhpProjectSharedConfiguration" php_language_level="8.2" />
|
||||
|
|
|
|||
|
|
@ -36,3 +36,6 @@
|
|||
``` html
|
||||
<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";
|
||||
|
||||
export default class extends Controller {
|
||||
static values = {aws: String};
|
||||
static values = {aws: String,
|
||||
id: String,
|
||||
activities: Boolean,
|
||||
table: Boolean,
|
||||
sadmin: Boolean,
|
||||
user: Number
|
||||
};
|
||||
|
||||
static targets = ["activityList", "emptyMessage"]
|
||||
connect() {
|
||||
this.table();
|
||||
if(this.activitiesValue){
|
||||
this.loadActivities();
|
||||
setInterval(() => {
|
||||
this.loadActivities();
|
||||
}, 60000); // Refresh every 60 seconds
|
||||
}
|
||||
if (this.tableValue && this.sadminValue) {
|
||||
this.table();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
table(){
|
||||
const table = new Tabulator("#tabulator-org", {
|
||||
// Register locales here
|
||||
langs: TABULATOR_FR_LANG,
|
||||
|
||||
placeholder: "Aucun résultat trouvé pour cette recherche",
|
||||
locale: "fr", //'en' for English, 'fr' for French (en is default, no need to include it)
|
||||
|
||||
ajaxURL: "/organization/data",
|
||||
ajaxURL: `/organization/data/${this.userValue}`,
|
||||
ajaxConfig: "GET",
|
||||
pagination: true,
|
||||
paginationMode: "remote",
|
||||
|
|
@ -82,4 +98,59 @@ export default class extends Controller {
|
|||
}],
|
||||
});
|
||||
}
|
||||
|
||||
async loadActivities() {
|
||||
try {
|
||||
// 1. Fetch the data using the ID from values
|
||||
const response = await fetch(`/actions/organization/${this.idValue}/activities-ajax`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
|
||||
const activities = await response.json();
|
||||
|
||||
// 2. Render
|
||||
this.renderActivities(activities);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching activities:', error);
|
||||
this.activityListTarget.innerHTML = `<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() {
|
||||
const columns = [
|
||||
{
|
||||
placeholder: "Aucun utilisateur trouvé",
|
||||
title: "",
|
||||
field: "isConnected",
|
||||
width: 40, // small column
|
||||
|
|
@ -365,7 +366,8 @@ export default class extends Controller {
|
|||
vertAlign: "middle",
|
||||
headerSort: false,
|
||||
formatter: (cell) => {
|
||||
const url = cell.getValue();
|
||||
const url = cell.getValue() + '?organizationId=' + this.orgIdValue;
|
||||
console.log(url);
|
||||
if (url) {
|
||||
return eyeIconLink(url);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@
|
|||
"symfony/process": "7.2.*",
|
||||
"symfony/property-access": "7.2.*",
|
||||
"symfony/property-info": "7.2.*",
|
||||
"symfony/rate-limiter": "7.2.*",
|
||||
"symfony/runtime": "7.2.*",
|
||||
"runtime/frankenphp-symfony": "^0.2.0",
|
||||
"symfony/security-bundle": "7.2.*",
|
||||
|
|
@ -107,7 +108,8 @@
|
|||
}
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"dama/doctrine-test-bundle": "^8.3",
|
||||
"phpunit/phpunit": "^11.0",
|
||||
"symfony/browser-kit": "7.2.*",
|
||||
"symfony/css-selector": "7.2.*",
|
||||
"symfony/debug-bundle": "7.2.*",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -19,4 +19,5 @@ return [
|
|||
Symfony\Bundle\MercureBundle\MercureBundle::class => ['all' => true],
|
||||
Knp\Bundle\TimeBundle\KnpTimeBundle::class => ['all' => true],
|
||||
Aws\Symfony\AwsBundle::class => ['all' => true],
|
||||
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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
|
||||
- php
|
||||
- error
|
||||
- aws_management
|
||||
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
|
||||
|
||||
when@dev:
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
channels: ["!event"]
|
||||
# uncomment to get logging in your browser
|
||||
# you may have to allow bigger header sizes in your Web server configuration
|
||||
#firephp:
|
||||
# type: firephp
|
||||
# level: info
|
||||
#chromephp:
|
||||
# type: chromephp
|
||||
# level: info
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine", "!console"]
|
||||
monolog:
|
||||
handlers:
|
||||
critical_errors:
|
||||
type: fingers_crossed
|
||||
action_level: critical
|
||||
handler: error_nested
|
||||
buffer_size: 50
|
||||
|
||||
error_nested:
|
||||
type: rotating_file
|
||||
path: "%kernel.logs_dir%/error.log"
|
||||
level: debug
|
||||
max_files: 30
|
||||
|
||||
error:
|
||||
type: rotating_file
|
||||
path: "%kernel.logs_dir%/error.log"
|
||||
level: error # logs error, critical, alert, emergency
|
||||
max_files: 30
|
||||
channels: [ error ]
|
||||
php_errors:
|
||||
type: rotating_file
|
||||
path: "%kernel.logs_dir%/php_error.log"
|
||||
level: warning # warnings, errors, fatals…
|
||||
max_files: 30
|
||||
channels: [ php ]
|
||||
# User Management
|
||||
user_management:
|
||||
type: rotating_file
|
||||
path: "%kernel.logs_dir%/user_management.log"
|
||||
level: debug
|
||||
channels: [ user_management ]
|
||||
max_files: 30
|
||||
|
||||
# Authentication
|
||||
authentication:
|
||||
type: rotating_file
|
||||
path: "%kernel.logs_dir%/authentication.log"
|
||||
level: debug
|
||||
channels: [ authentication ]
|
||||
max_files: 30
|
||||
|
||||
# Organization Management
|
||||
organization_management:
|
||||
type: rotating_file
|
||||
path: "%kernel.logs_dir%/organization_management.log"
|
||||
level: debug
|
||||
channels: [ organization_management ]
|
||||
max_files: 30
|
||||
|
||||
# Access Control
|
||||
access_control:
|
||||
type: rotating_file
|
||||
path: "%kernel.logs_dir%/access_control.log"
|
||||
level: debug
|
||||
channels: [ access_control ]
|
||||
max_files: 30
|
||||
|
||||
# Email Notifications
|
||||
email_notifications:
|
||||
type: rotating_file
|
||||
path: "%kernel.logs_dir%/email_notifications.log"
|
||||
level: debug
|
||||
channels: [ email_notifications ]
|
||||
max_files: 30
|
||||
|
||||
# Admin Actions
|
||||
admin_actions:
|
||||
type: rotating_file
|
||||
path: "%kernel.logs_dir%/admin_actions.log"
|
||||
level: debug
|
||||
channels: [ admin_actions ]
|
||||
max_files: 30
|
||||
|
||||
# Security
|
||||
security:
|
||||
type: rotating_file
|
||||
path: "%kernel.logs_dir%/security.log"
|
||||
level: debug
|
||||
channels: [ security ]
|
||||
max_files: 30
|
||||
|
||||
when@test:
|
||||
monolog:
|
||||
|
|
@ -57,7 +120,7 @@ when@prod:
|
|||
|
||||
error_nested:
|
||||
type: rotating_file
|
||||
path: "%kernel.logs_dir%/error.log"
|
||||
path: "%kernel.logs_dir%/critical.log"
|
||||
level: debug
|
||||
max_files: 30
|
||||
|
||||
|
|
@ -76,12 +139,18 @@ when@prod:
|
|||
channels: [ php ]
|
||||
# User Management
|
||||
user_management:
|
||||
type: stream
|
||||
type: rotating_file
|
||||
path: "%kernel.logs_dir%/user_management.log"
|
||||
level: info
|
||||
channels: [user_management]
|
||||
max_files: 30
|
||||
|
||||
#AWS
|
||||
aws_management:
|
||||
type: rotating_file
|
||||
path: "%kernel.logs_dir%/aws_management.log"
|
||||
level: info
|
||||
channels: [aws_management]
|
||||
max_files: 30
|
||||
# Authentication
|
||||
authentication:
|
||||
type: rotating_file
|
||||
|
|
|
|||
|
|
@ -42,6 +42,9 @@ security:
|
|||
user_checker: App\Security\UserChecker
|
||||
lazy: true
|
||||
provider: app_user_provider
|
||||
login_throttling:
|
||||
max_attempts: 3
|
||||
interval: '1 minute'
|
||||
form_login:
|
||||
login_path: app_login
|
||||
check_path: app_login
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
framework:
|
||||
default_locale: en
|
||||
default_locale: fr
|
||||
translator:
|
||||
default_path: '%kernel.project_dir%/translations'
|
||||
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"?>
|
||||
|
||||
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
backupGlobals="false"
|
||||
colors="true"
|
||||
bootstrap="tests/bootstrap.php"
|
||||
convertDeprecationsToExceptions="false"
|
||||
>
|
||||
colors="true"
|
||||
cacheDirectory=".phpunit.cache">
|
||||
|
||||
<php>
|
||||
<ini name="display_errors" value="1" />
|
||||
<ini name="error_reporting" value="-1" />
|
||||
<server name="APP_ENV" value="test" force="true" />
|
||||
<server name="SHELL_VERBOSITY" value="-1" />
|
||||
<server name="SYMFONY_PHPUNIT_REMOVE" value="" />
|
||||
<server name="SYMFONY_PHPUNIT_VERSION" value="9.5" />
|
||||
</php>
|
||||
|
||||
<testsuites>
|
||||
|
|
@ -23,16 +18,9 @@
|
|||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<coverage processUncoveredFiles="true">
|
||||
<source>
|
||||
<include>
|
||||
<directory suffix=".php">src</directory>
|
||||
</include>
|
||||
</coverage>
|
||||
|
||||
<listeners>
|
||||
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener" />
|
||||
</listeners>
|
||||
|
||||
<extensions>
|
||||
</extensions>
|
||||
</source>
|
||||
</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\Organizations;
|
||||
use App\Service\ActionService;
|
||||
use App\Service\LoggerService;
|
||||
use App\Service\UserService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
|
||||
|
||||
#[Route(path: '/application', name: 'application_')]
|
||||
|
||||
class ApplicationController extends AbstractController
|
||||
{
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager, private readonly UserService $userService, private readonly ActionService $actionService)
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager, private readonly UserService $userService, private readonly ActionService $actionService, private readonly LoggerService $loggerService)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -37,7 +39,11 @@ class ApplicationController extends AbstractController
|
|||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$application = $this->entityManager->getRepository(Apps::class)->find($id);
|
||||
if (!$application) {
|
||||
$this->addFlash('error', "L'application n'existe pas ou n'est pas reconnu.");
|
||||
$this->loggerService->logEntityNotFound('Application', [
|
||||
'applicationId' => $id,
|
||||
'message' => "Application not found for editing."
|
||||
], $actingUser->getId());
|
||||
$this->addFlash('danger', "L'application n'existe pas ou n'est pas reconnu.");
|
||||
return $this->redirectToRoute('application_index');
|
||||
}
|
||||
$applicationData = [
|
||||
|
|
@ -50,12 +56,26 @@ class ApplicationController extends AbstractController
|
|||
|
||||
|
||||
if ($request->isMethod('POST')) {
|
||||
$data = $request->request->all();
|
||||
$application->setName($data['name']);
|
||||
$application->setDescription($data['description']);
|
||||
$application->setDescriptionSmall($data['descriptionSmall']);
|
||||
$this->entityManager->persist($application);
|
||||
$this->actionService->createAction("Modification de l'application ", $actingUser, null, $application->getId());
|
||||
try{
|
||||
$data = $request->request->all();
|
||||
$application->setName($data['name']);
|
||||
$application->setDescription($data['description']);
|
||||
$application->setDescriptionSmall($data['descriptionSmall']);
|
||||
$this->entityManager->persist($application);
|
||||
$this->actionService->createAction("Modification de l'application ", $actingUser, null, $application->getId());
|
||||
$this->loggerService->logApplicationInformation('Application Edited', [
|
||||
'applicationId' => $application->getId(),
|
||||
'applicationName' => $application->getName(),
|
||||
'message' => "Application edited successfully."
|
||||
], $actingUser->getId());
|
||||
}catch (\Exception $e){
|
||||
$this->loggerService->logError('Application Edit Failed', [
|
||||
'applicationId' => $application->getId(),
|
||||
'applicationName' => $application->getName(),
|
||||
'error' => $e->getMessage(),
|
||||
'message' => "Failed to edit application."
|
||||
], $actingUser);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('application_index');
|
||||
}
|
||||
|
|
@ -66,36 +86,82 @@ class ApplicationController extends AbstractController
|
|||
}
|
||||
|
||||
#[Route(path: '/authorize/{id}', name: 'authorize', methods: ['POST'])]
|
||||
public function authorize(int $id, Request $request)
|
||||
public function authorize(int $id, Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$application = $this->entityManager->getRepository(Apps::class)->find($id);
|
||||
if (!$application) {
|
||||
throw $this->createNotFoundException("L'application n'existe pas.");
|
||||
try{
|
||||
$application = $this->entityManager->getRepository(Apps::class)->find($id);
|
||||
if (!$application) {
|
||||
$this->loggerService->logEntityNotFound('Application', [
|
||||
'applicationId' => $id,
|
||||
'message' => "Application not found for authorization."
|
||||
], $actingUser->getId());
|
||||
throw $this->createNotFoundException("L'application n'existe pas.");
|
||||
}
|
||||
$orgId = $request->get('organizationId');
|
||||
|
||||
$organization = $this->entityManager->getRepository(Organizations::Class)->find($orgId);
|
||||
if (!$organization) {
|
||||
$this->loggerService->logEntityNotFound('Organization', [
|
||||
'Organization_id' => $orgId,
|
||||
'message' => "Organization not found for authorization."
|
||||
], $actingUser->getId());
|
||||
throw $this->createNotFoundException("L'Organization n'existe pas.");
|
||||
}
|
||||
$application->addOrganization($organization);
|
||||
$this->loggerService->logApplicationInformation('Application Authorized', [
|
||||
'applicationId' => $application->getId(),
|
||||
'applicationName' => $application->getName(),
|
||||
'organizationId' => $organization->getId(),
|
||||
'message' => "Application authorized for organization."
|
||||
], $actingUser->getId());
|
||||
$this->entityManager->persist($application);
|
||||
$this->entityManager->flush();
|
||||
$this->actionService->createAction("Authorization d'accès", $actingUser, $organization, $application->getName());
|
||||
return new Response('', Response::HTTP_OK);
|
||||
}catch (\Exception $e){
|
||||
$this->loggerService->logError('Application Authorization Failed', [
|
||||
'applicationId' => $id,
|
||||
'error' => $e->getMessage(),
|
||||
'message' => "Failed to authorize application.",
|
||||
'acting_user_id' => $actingUser->getId()
|
||||
]);
|
||||
return new Response('Erreur lors de l\'autorisation de l\'application.', Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
$orgId = $request->get('organizationId');
|
||||
|
||||
$organization = $this->entityManager->getRepository(Organizations::Class)->find($orgId);
|
||||
$application->addOrganization($organization);
|
||||
|
||||
$this->actionService->createAction("Authorization d'accès", $actingUser, $organization, $application->getName());
|
||||
return new Response('', Response::HTTP_OK);
|
||||
}
|
||||
|
||||
#[Route(path: '/remove/{id}', name: 'remove', methods: ['POST'])]
|
||||
public function remove(int $id, Request $request)
|
||||
#[Route(path: '/revoke/{id}', name: 'revoke', methods: ['POST'])]
|
||||
public function revoke(int $id, Request $request)
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$application = $this->entityManager->getRepository(Apps::class)->find($id);
|
||||
if (!$application) {
|
||||
$this->loggerService->logEntityNotFound('Application', [
|
||||
'applicationId' => $id,
|
||||
'message' => "Application not found for authorization removal."
|
||||
], $actingUser->getId());
|
||||
throw $this->createNotFoundException("L'application n'existe pas.");
|
||||
}
|
||||
$orgId = $request->get('organizationId');
|
||||
$organization = $this->entityManager->getRepository(Organizations::Class)->find($orgId);
|
||||
if (!$organization) {
|
||||
$this->loggerService->logEntityNotFound('Organization', [
|
||||
'Organization_id' => $orgId,
|
||||
'message' => "Organization not found for authorization removal."
|
||||
], $actingUser->getId());
|
||||
throw $this->createNotFoundException("L'Organization n'existe pas.");
|
||||
}
|
||||
$application->removeOrganization($organization);
|
||||
|
||||
$this->loggerService->logApplicationInformation('Application Authorized removed', [
|
||||
'applicationId' => $application->getId(),
|
||||
'applicationName' => $application->getName(),
|
||||
'organizationId' => $organization->getId(),
|
||||
'message' => "Application authorized removed for organization."
|
||||
], $actingUser->getId());
|
||||
$this->actionService->createAction("Authorization retirer", $actingUser, $organization, $application->getName());
|
||||
|
||||
return new Response('', Response::HTTP_OK);
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class NotificationController extends AbstractController
|
|||
#[Route(path: '/', name: 'index', methods: ['GET'])]
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
|
||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
||||
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
|
||||
$notifications = $this->notificationRepository->findRecentByUser($user, 50);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
namespace App\Controller;
|
||||
|
||||
use App\Service\AccessTokenService;
|
||||
use App\Service\LoggerService;
|
||||
use App\Service\UserService;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\LogLevel;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
|
|
@ -18,17 +20,20 @@ class OAuth2Controller extends AbstractController
|
|||
{
|
||||
|
||||
|
||||
|
||||
public function __construct(private readonly LoggerService $loggerService, private readonly UserService $userService)
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/oauth2/userinfo', name: 'userinfo', methods: ['GET'])]
|
||||
public function userinfo(Request $request): JsonResponse
|
||||
{
|
||||
$user = $this->getUser();
|
||||
// dd($user);
|
||||
if (!$user) {
|
||||
$this->loggerService->logAccessDenied($user->getId());
|
||||
return new JsonResponse(['error' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
$this->loggerService->logUserAction($user->getId(), $user->getId(), 'Accessed userinfo endpoint');
|
||||
return new JsonResponse([
|
||||
'id' => $user->getId(),
|
||||
'name' => $user->getName(),
|
||||
|
|
@ -66,7 +71,7 @@ class OAuth2Controller extends AbstractController
|
|||
if (!$userIdentifier) {
|
||||
return new JsonResponse(["ERROR" => "User identifier is required"], Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
$accessTokenService->revokeTokens($userIdentifier);
|
||||
$accessTokenService->revokeUserTokens($userIdentifier);
|
||||
$logger->info("Revoke tokens successfully");
|
||||
|
||||
return new JsonResponse(["SUCCESS" => "Tokens revoked successfully"], Response::HTTP_OK);
|
||||
|
|
|
|||
|
|
@ -12,14 +12,20 @@ use App\Form\OrganizationForm;
|
|||
use App\Repository\OrganizationsRepository;
|
||||
use App\Service\ActionService;
|
||||
use App\Service\AwsService;
|
||||
use App\Service\LoggerService;
|
||||
use App\Service\OrganizationsService;
|
||||
use App\Service\UserOrganizationService;
|
||||
use App\Service\UserService;
|
||||
use Doctrine\DBAL\Exception\NonUniqueFieldNameException;
|
||||
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\NonUniqueResultException;
|
||||
use Exception;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use App\Entity\Organizations;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
|
@ -37,7 +43,7 @@ class OrganizationController extends AbstractController
|
|||
private readonly ActionService $actionService,
|
||||
private readonly UserOrganizationService $userOrganizationService,
|
||||
private readonly OrganizationsRepository $organizationsRepository,
|
||||
private readonly AwsService $awsService)
|
||||
private readonly AwsService $awsService, private readonly LoggerService $loggerService, private readonly LoggerInterface $logger)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -45,45 +51,30 @@ class OrganizationController extends AbstractController
|
|||
public function index(): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
|
||||
if ($this->isGranted("ROLE_SUPER_ADMIN")) {
|
||||
$organizations = $this->organizationsRepository->findBy(['isDeleted' => false]);
|
||||
|
||||
|
||||
} else {
|
||||
//get all the UO of the user
|
||||
$uos = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['users' => $user]);
|
||||
$organizations = [];
|
||||
$roleAdmin = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN']);
|
||||
foreach ($uos as $uo) {
|
||||
$uoaAdmin = $this->entityManager->getRepository(UserOrganizatonApp::class)->findOneBy(['userOrganization' => $uo, 'role' => $roleAdmin]);
|
||||
if ($uoaAdmin) {
|
||||
$organizations[] = $uo->getOrganization();
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
if($this->userService->hasAccessTo($actingUser, true)){
|
||||
$orgCount = $this->organizationsRepository->count(['isDeleted' => false]);
|
||||
if(!$this->isGranted("ROLE_SUPER_ADMIN")){
|
||||
$userUO = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['users' => $actingUser, 'isActive' => true]);
|
||||
$uoAdmin = 0;
|
||||
foreach($userUO as $u){
|
||||
if($this->userService->isAdminOfOrganization($u->getOrganization())){
|
||||
$uoAdmin++;
|
||||
}
|
||||
}
|
||||
if($uoAdmin === 1){
|
||||
return $this->redirectToRoute('organization_show', ['id' => $userUO[0]->getOrganization()->getId()]);
|
||||
}
|
||||
}
|
||||
if (count($organizations) === 1 && $organizations[0]->isActive() === true) {
|
||||
return $this->redirectToRoute('organization_show', ['id' => $organizations[0]->getId()]);
|
||||
}
|
||||
|
||||
return $this->render('organization/index.html.twig', [
|
||||
'hasOrganizations' => $orgCount > 0
|
||||
]);
|
||||
}
|
||||
// Map the entities for tabulator
|
||||
$organizationsData = array_map(function ($org) {
|
||||
return [
|
||||
'id' => $org->getId(),
|
||||
'name' => $org->getName(),
|
||||
'email' => $org->getEmail(),
|
||||
'logoUrl' => $org->getLogoUrl() ? $org->getLogoUrl() : null,
|
||||
'active' => $org->isActive(),
|
||||
'showUrl' => $this->generateUrl('organization_show', ['id' => $org->getId()]),
|
||||
];
|
||||
}, $organizations);
|
||||
return $this->render('organization/index.html.twig', [
|
||||
'organizationsData' => $organizationsData,
|
||||
]);
|
||||
$this->loggerService->logAccessDenied($actingUser->getId());
|
||||
throw new AccessDeniedHttpException('Access denied');
|
||||
}
|
||||
|
||||
#[Route(path: '/new', name: 'new', methods: ['GET', 'POST'])]
|
||||
#[Route(path: '/create', name: 'create', methods: ['GET', 'POST'])]
|
||||
public function new(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
||||
|
|
@ -100,10 +91,14 @@ class OrganizationController extends AbstractController
|
|||
try {
|
||||
$this->entityManager->persist($organization);
|
||||
$this->entityManager->flush();
|
||||
$this->loggerService->logOrganizationInformation($organization->getId(), $actingUser->getId(), "Organization Created");
|
||||
$this->loggerService->logSuperAdmin($actingUser->getId(), $actingUser->getId(), "Organization Created", $organization->getId());
|
||||
$this->actionService->createAction("Create Organization", $actingUser, $organization, $organization->getName());
|
||||
$this->addFlash('success', 'Organisation crée avec succès.');
|
||||
return $this->redirectToRoute('organization_index');
|
||||
} catch (Exception $e) {
|
||||
$this->addFlash('error', 'Error creating organization: ' . $e->getMessage());
|
||||
$this->addFlash('error', 'Erreur lors de la création de l\'organization');
|
||||
$this->loggerService->logError('Error creating organization', ['acting_user_id' => $actingUser->getId(), 'error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
return $this->render('organization/new.html.twig', [
|
||||
|
|
@ -124,21 +119,34 @@ class OrganizationController extends AbstractController
|
|||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$organization = $this->organizationsRepository->find($id);
|
||||
if (!$organization) {
|
||||
$this->addFlash('error', self::NOT_FOUND);
|
||||
$this->loggerService->logEntityNotFound('Organization', [
|
||||
'org_id' => $id,
|
||||
'message' => 'Organization not found for edit'], $actingUser->getId()
|
||||
);
|
||||
$this->addFlash('error', 'Erreur, l\'organization est introuvable.');
|
||||
return $this->redirectToRoute('organization_index');
|
||||
}
|
||||
if (!$this->isGranted("ROLE_SUPER_ADMIN")) {
|
||||
//check if the user is admin of the organization
|
||||
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$uo = $this->entityManager->getRepository(UsersOrganizations::class)->findOneBy(['users' => $user, 'organization' => $organization]);
|
||||
$uo = $this->entityManager->getRepository(UsersOrganizations::class)->findOneBy(['users' => $actingUser, 'organization' => $organization]);
|
||||
if (!$uo) {
|
||||
$this->addFlash('error', self::ACCESS_DENIED);
|
||||
$this->loggerService->logEntityNotFound('UO link', [
|
||||
'user_id' => $actingUser->getId(),
|
||||
'org_id' => $organization->getId(),
|
||||
'message' => 'UO link not found for edit organization'
|
||||
], $actingUser->getId());
|
||||
$this->addFlash('error', 'Erreur, accès refusé.');
|
||||
return $this->redirectToRoute('organization_index');
|
||||
}
|
||||
$roleAdmin = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN']);
|
||||
$uoaAdmin = $this->entityManager->getRepository(UserOrganizatonApp::class)->findOneBy(['userOrganization' => $uo, 'role' => $roleAdmin]);
|
||||
if (!$uoaAdmin) {
|
||||
$this->addFlash('error', self::ACCESS_DENIED);
|
||||
$this->loggerService->logEntityNotFound('UOA link', [
|
||||
'uo_id' => $uo->getId(),
|
||||
'role_id' => $roleAdmin->getId(),
|
||||
'message' => 'UOA link not found for edit organization, user is not admin of organization'
|
||||
], $actingUser->getId());
|
||||
$this->addFlash('error', 'Erreur, accès refusé.');
|
||||
return $this->redirectToRoute('organization_index');
|
||||
}
|
||||
}
|
||||
|
|
@ -152,10 +160,16 @@ class OrganizationController extends AbstractController
|
|||
try {
|
||||
$this->entityManager->persist($organization);
|
||||
$this->entityManager->flush();
|
||||
$this->loggerService->logOrganizationInformation($organization->getId(), $actingUser->getId(), "Organization Edited");
|
||||
if ($this->isGranted("ROLE_SUPER_ADMIN")) {
|
||||
$this->loggerService->logSuperAdmin($actingUser->getId(), $actingUser->getId(), "Organization Edited", $organization->getId());
|
||||
}
|
||||
$this->actionService->createAction("Edit Organization", $actingUser, $organization, $organization->getName());
|
||||
$this->addFlash('success', 'Organisation modifiée avec succès.');
|
||||
return $this->redirectToRoute('organization_index');
|
||||
} catch (Exception $e) {
|
||||
$this->addFlash('error', 'Error editing organization: ' . $e->getMessage());
|
||||
}catch (Exception $e) {
|
||||
$this->addFlash('error', 'Erreur lors de la modification de l\'organization');
|
||||
$this->loggerService->logError('Error editing organization', ['acting_user_id' => $actingUser->getId(), 'error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
return $this->render('organization/edit.html.twig', [
|
||||
|
|
@ -171,43 +185,31 @@ class OrganizationController extends AbstractController
|
|||
$organization = $this->organizationsRepository->find($id);
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
if (!$organization) {
|
||||
$this->addFlash('error', self::NOT_FOUND);
|
||||
$this->loggerService->logEntityNotFound('Organization', [
|
||||
'org_id' => $id,
|
||||
'message' => 'Organization not found for view'
|
||||
], $actingUser->getId());
|
||||
$this->addFlash('error', 'Erreur, l\'organization est introuvable.');
|
||||
return $this->redirectToRoute('organization_index');
|
||||
}
|
||||
//check if the user is admin of the organization
|
||||
if (!$this->isGranted("ROLE_SUPER_ADMIN") && !$this->userService->isAdminOfOrganization($organization)) {
|
||||
$this->createNotFoundException(self::NOT_FOUND);
|
||||
if (!$this->userService->isAdminOfOrganization($organization) && !$this->isGranted("ROLE_SUPER_ADMIN")) {
|
||||
$this->loggerService->logAccessDenied($actingUser->getId());
|
||||
$this->addFlash('error', 'Erreur, accès refusé.');
|
||||
throw new AccessDeniedHttpException('Access denied');
|
||||
}
|
||||
$newUO = $this->entityManager->getRepository(UsersOrganizations::class)->findNewestUO($organization);
|
||||
$newUsers = [];
|
||||
foreach ($newUO as $uo) {
|
||||
$newUsers[] = $uo->getUsers();
|
||||
}
|
||||
$adminUO = $this->entityManager->getRepository(UsersOrganizations::class)->findAdminsInOrganization($organization);
|
||||
$adminUsers = [];
|
||||
foreach ($adminUO as $uo) {
|
||||
$adminUsers[] = $uo->getUsers();
|
||||
}
|
||||
$uos = $this->entityManager
|
||||
->getRepository(UsersOrganizations::class)
|
||||
->findBy(['organization' => $organization]);
|
||||
|
||||
$users = $this->userService->formatOrgUsers($uos);
|
||||
|
||||
$allApps = $this->entityManager->getRepository(Apps::class)->findAll(); // appsAll
|
||||
$orgApps = $organization->getApps()->toArray(); // apps
|
||||
|
||||
$apps = $this->organizationsService->appsAccess($allApps, $orgApps);
|
||||
|
||||
$actions = $this->entityManager->getRepository(Actions::class)->findBy(['Organization' => $organization], limit: 15);
|
||||
$actions = $this->entityManager->getRepository(Actions::class)->findBy(['Organization' => $organization], orderBy: ['date' => 'DESC'], limit: 15);
|
||||
$activities = $this->actionService->formatActivities($actions);
|
||||
|
||||
$this->actionService->createAction("View Organization", $actingUser, $organization, $organization->getName());
|
||||
return $this->render('organization/show.html.twig', [
|
||||
'organization' => $organization,
|
||||
'newUsers' => $newUsers,
|
||||
'adminUsers' => $adminUsers,
|
||||
'users' => $users,
|
||||
'applications' => $apps,
|
||||
'activities' => $activities,
|
||||
]);
|
||||
|
|
@ -220,15 +222,32 @@ class OrganizationController extends AbstractController
|
|||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$organization = $this->organizationsRepository->find($id);
|
||||
if (!$organization) {
|
||||
$this->loggerService->logEntityNotFound('Organization', [
|
||||
'org_id' => $id,
|
||||
'message' => 'Organization not found for delete'
|
||||
], $actingUser->getId());
|
||||
$this->addFlash('error', 'Erreur, l\'organization est introuvable.');
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
$organization->setIsActive(false);
|
||||
$organization->setIsDeleted(true);
|
||||
// Deactivate all associated UsersOrganizations
|
||||
$this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, null, $organization);
|
||||
try {
|
||||
$organization->setIsActive(false);
|
||||
$organization->setIsDeleted(true);
|
||||
// Deactivate all associated UsersOrganizations
|
||||
$this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, null, $organization);
|
||||
|
||||
$this->entityManager->persist($organization);
|
||||
$this->actionService->createAction("Delete Organization", $actingUser, $organization, $organization->getName());
|
||||
$this->entityManager->flush();
|
||||
$this->loggerService->logOrganizationInformation($organization->getId(), $actingUser->getId(),'Organization Deleted');
|
||||
if ($this->isGranted("ROLE_SUPER_ADMIN")) {
|
||||
$this->loggerService->logSuperAdmin($actingUser->getId(), $actingUser->getId(),'Organization Deleted', $organization->getId());
|
||||
}
|
||||
$this->addFlash('success', 'Organisation supprimée avec succès.');
|
||||
}catch (\Exception $e){
|
||||
$this->loggerService->logError($actingUser->getId(), ['message' => 'Error deleting organization: '.$e->getMessage()]);
|
||||
$this->addFlash('error', 'Erreur lors de la suppression de l\'organization.');
|
||||
}
|
||||
|
||||
$this->entityManager->persist($organization);
|
||||
$this->actionService->createAction("Delete Organization", $actingUser, $organization, $organization->getName());
|
||||
return $this->redirectToRoute('organization_index');
|
||||
}
|
||||
|
||||
|
|
@ -239,12 +258,20 @@ class OrganizationController extends AbstractController
|
|||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$organization = $this->organizationsRepository->find($id);
|
||||
if (!$organization) {
|
||||
$this->loggerService->logEntityNotFound('Organization', [
|
||||
'org_id' => $id,
|
||||
'message' => 'Organization not found for deactivate'
|
||||
], $actingUser->getId());
|
||||
$this->addFlash('error', 'Erreur, l\'organization est introuvable.');
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
|
||||
$organization->setIsActive(false);
|
||||
// $this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, null, $organization);
|
||||
$this->entityManager->persist($organization);
|
||||
$this->actionService->createAction("Deactivate Organization", $actingUser, $organization, $organization->getName());
|
||||
$this->loggerService->logSuperAdmin($actingUser->getId(), $actingUser->getId(),'Organization deactivated', $organization->getId());
|
||||
$this->addFlash('success', 'Organisation désactivé avec succès.');
|
||||
return $this->redirectToRoute('organization_index');
|
||||
}
|
||||
|
||||
|
|
@ -255,16 +282,24 @@ class OrganizationController extends AbstractController
|
|||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$organization = $this->organizationsRepository->find($id);
|
||||
if (!$organization) {
|
||||
$this->loggerService->logEntityNotFound('Organization', [
|
||||
'org_id' => $id,
|
||||
'message' => 'Organization not found for activate'
|
||||
], $actingUser->getId());
|
||||
$this->addFlash('error', 'Erreur, l\'organization est introuvable.');
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
$organization->setIsActive(true);
|
||||
$this->entityManager->persist($organization);
|
||||
$this->loggerService->logOrganizationInformation($organization->getId(), $actingUser->getId(),'Organization Activated');
|
||||
$this->loggerService->logSuperAdmin($actingUser->getId(), $actingUser->getId(),'Organization Activated', $organization->getId());
|
||||
$this->actionService->createAction("Activate Organization", $actingUser, $organization, $organization->getName());
|
||||
$this->addFlash('success', 'Organisation activée avec succès.');
|
||||
return $this->redirectToRoute('organization_index');
|
||||
}
|
||||
|
||||
// API endpoint to fetch organization data for Tabulator
|
||||
#[Route(path: '/data', name: 'data', methods: ['GET'])]
|
||||
#[Route(path: '/data/{id}', name: 'data', methods: ['GET'])]
|
||||
public function data(Request $request): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
|
|
@ -276,8 +311,6 @@ class OrganizationController extends AbstractController
|
|||
$filters = $request->query->all('filter');
|
||||
|
||||
|
||||
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
|
||||
$qb = $this->organizationsRepository->createQueryBuilder('o')
|
||||
->where('o.isDeleted = :del')->setParameter('del', false);
|
||||
|
||||
|
|
@ -289,6 +322,17 @@ class OrganizationController extends AbstractController
|
|||
$qb->andWhere('o.email LIKE :email')
|
||||
->setParameter('email', '%' . $filters['email'] . '%');
|
||||
}
|
||||
if(!$this->isGranted('ROLE_SUPER_ADMIN')) {
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$uo = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['users' => $actingUser]);
|
||||
foreach ($uo as $item) {
|
||||
if($this->userService->isAdminOfOrganization($item->getOrganization())) {
|
||||
$qb->andWhere('o.id = :orgId')
|
||||
->setParameter('orgId', $item->getOrganization()->getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Count total
|
||||
$countQb = clone $qb;
|
||||
|
|
@ -311,7 +355,6 @@ class OrganizationController extends AbstractController
|
|||
];
|
||||
}, $rows);
|
||||
|
||||
// Tabulator expects: data, last_page (total pages), or total row count depending on config
|
||||
$lastPage = (int)ceil($total / $size);
|
||||
|
||||
return $this->json([
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ namespace App\Controller;
|
|||
use App\Repository\UserRepository;
|
||||
use App\Repository\UsersOrganizationsRepository;
|
||||
use App\Service\AccessTokenService;
|
||||
use App\Service\LoggerService;
|
||||
use App\Service\OrganizationsService;
|
||||
use App\Service\UserService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
|
@ -30,7 +31,7 @@ class SecurityController extends AbstractController
|
|||
private readonly UsersOrganizationsRepository $uoRepository,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly OrganizationsService $organizationsService)
|
||||
private readonly OrganizationsService $organizationsService, private readonly LoggerService $loggerService, private readonly Security $security)
|
||||
{
|
||||
$this->cguUserService = $cguUserService;
|
||||
}
|
||||
|
|
@ -50,14 +51,16 @@ class SecurityController extends AbstractController
|
|||
#[Route(path: '/sso_logout', name: 'sso_logout')]
|
||||
public function ssoLogout(RequestStack $stack, LoggerInterface $logger, AccessTokenService $accessTokenService, Security $security): Response
|
||||
{
|
||||
try{
|
||||
if( $stack->getSession()->invalidate()){
|
||||
$accessTokenService->revokeTokens($security->getUser()->getUserIdentifier());
|
||||
try {
|
||||
$user = $this->userService->getUserByIdentifier($this->security->getUser()->getUserIdentifier());
|
||||
$id = $user->getId();
|
||||
if ($stack->getSession()->invalidate()) {
|
||||
$accessTokenService->revokeUserTokens($security->getUser()->getUserIdentifier());
|
||||
$security->logout(false);
|
||||
$logger->info("Logout successfully");
|
||||
return $this->redirect('/');
|
||||
$this->loggerService->logUserConnection('User logged out', ['user_id' => $id]);
|
||||
return $this->redirect('/');
|
||||
}
|
||||
}catch (\Exception $e){
|
||||
} catch (\Exception $e) {
|
||||
$logger->log(LogLevel::ERROR, 'Error invalidating session: ' . $e->getMessage());
|
||||
}
|
||||
return $this->redirectToRoute('app_index');
|
||||
|
|
@ -69,6 +72,7 @@ class SecurityController extends AbstractController
|
|||
if ($request->isMethod('POST')) {
|
||||
if (!$request->request->has('decline')) {
|
||||
$this->cguUserService->acceptLatestCgu($this->getUser());
|
||||
$this->loggerService->logCGUAcceptance($this->getUser()->getId());
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('oauth2_authorize', $request->query->all());
|
||||
|
|
@ -83,12 +87,24 @@ class SecurityController extends AbstractController
|
|||
$error = $request->get('error');
|
||||
$user = $this->userRepository->find($id);
|
||||
if (!$user) {
|
||||
$this->loggerService->logEntityNotFound('User', ['user_id' => $id,
|
||||
'error' => $error ?? null,
|
||||
'message' => 'user not found for password setup'], $id);
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
$token = $request->get('token');
|
||||
if (empty($token) || !$this->userService->isPasswordTokenValid($user, $token)) {
|
||||
if (empty($token)) {
|
||||
$error = 'Le lien de définition du mot de passe est invalide ou a expiré. Veuillez en demander un nouveau.';
|
||||
$this->logger->warning($user->getUserIdentifier(). " tried to use an invalid or expired password setup token.");
|
||||
$this->loggerService->logTokenError('Token empty while trying to setup password', ['token' => $token,
|
||||
'token_empty' => true,
|
||||
'user_id' => $id,
|
||||
'message' => 'empty token provided for password setup']);
|
||||
}
|
||||
|
||||
if (!$this->userService->isPasswordTokenValid($user, $token)) {
|
||||
$error = 'Le lien de définition du mot de passe est invalide ou a expiré. Veuillez en demander un nouveau.';
|
||||
$this->loggerService->logTokenError('invalid or expired token for password setup', ['user_id' => $id,
|
||||
'token' => $token,]);
|
||||
}
|
||||
return $this->render('security/password_setup.html.twig', [
|
||||
'id' => $id,
|
||||
|
|
@ -98,36 +114,43 @@ class SecurityController extends AbstractController
|
|||
}
|
||||
|
||||
#[Route('/password_reset/{id}', name: 'password_reset', methods: ['POST'])]
|
||||
public function password_reset(int $id): Response
|
||||
public function password_reset(int $id, Request $request): Response
|
||||
{
|
||||
$user = $this->userRepository->find($id);
|
||||
if (!$user) {
|
||||
$this->loggerService->logEntityNotFound('User', ['user_id' => $id,
|
||||
'message' => 'user not found for password reset'], $id);
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
$newPassword = $_POST['_password'] ?? null;
|
||||
$confirmPassword = $_POST['_passwordConfirm'] ?? null;
|
||||
if ($newPassword !== $confirmPassword) {
|
||||
$error = 'Les mots de passe ne correspondent pas. Veuillez réessayer.';
|
||||
$this->logger->warning($user->getUserIdentifier(). " provided non-matching passwords during password reset.");
|
||||
$this->loggerService->logUserAction($id, $id, 'Password confirmation does not match during password reset.');
|
||||
return $this->redirectToRoute('password_setup', [
|
||||
'id' => $id,
|
||||
'token' => $_POST['token'] ?? '',
|
||||
'error'=> $error]);
|
||||
'error' => $error]);
|
||||
}
|
||||
if (!$this->userService->isPasswordStrong($newPassword)) {
|
||||
$error = 'Le mot de passe ne respecte pas les critères de sécurité. Veuillez en choisir un autre.';
|
||||
$this->logger->warning($user->getUserIdentifier(). " provided a weak password during password reset.");
|
||||
return $this->redirectToRoute('password_setup', ['id' => $id, 'token' => $_POST['token'] ?? '', 'error'=> $error]);
|
||||
$this->loggerService->logUserAction($id, $id, ' provided a weak password during password reset.');
|
||||
return $this->redirectToRoute('password_setup', ['id' => $id, 'token' => $_POST['token'] ?? '', 'error' => $error]);
|
||||
}
|
||||
$this->userService->updateUserPassword($user, $newPassword);
|
||||
$orgId = $this->userService->getOrgFromToken( $_POST['token']);
|
||||
$this->loggerService->logUserAction($id, $id, 'Password reset user successfully.');
|
||||
$orgId = $this->userService->getOrgFromToken($_POST['token']);
|
||||
$uo = $this->uoRepository->findOneBy(['users' => $user, 'organization' => $orgId]);
|
||||
if($uo){
|
||||
if ($uo) {
|
||||
$uo->setStatut("ACCEPTED");
|
||||
$uo->setIsActive(true);
|
||||
$this->entityManager->persist($uo);
|
||||
$this->entityManager->flush();
|
||||
$data = ['user' => $user, 'organization' => $uo->getOrganization()];
|
||||
$this->loggerService->logOrganizationInformation($orgId, $user->getId(), 'User accepted organization invitation during password reset.');
|
||||
$this->loggerService->logUserAction($id, $id, "User accepted organization invitation successfully with uo link id : {$uo->getId()}");
|
||||
$data = ['user' => $user,
|
||||
'organization' => $uo->getOrganization(),
|
||||
];
|
||||
|
||||
$this->organizationsService->notifyOrganizationAdmins($data, "USER_ACCEPTED");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,26 +13,25 @@ use App\Repository\OrganizationsRepository;
|
|||
use App\Repository\RolesRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Repository\UsersOrganizationsRepository;
|
||||
use App\Service\AccessTokenService;
|
||||
use App\Service\ActionService;
|
||||
use App\Service\AwsService;
|
||||
use App\Service\EmailService;
|
||||
use App\Service\LoggerService;
|
||||
use App\Service\OrganizationsService;
|
||||
use App\Service\UserOrganizationAppService;
|
||||
use App\Service\UserOrganizationService;
|
||||
use App\Service\UserService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use mysql_xdevapi\Exception;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Mailer\Mailer;
|
||||
use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
|
||||
#[Route(path: '/user', name: 'user_')]
|
||||
class UserController extends AbstractController
|
||||
|
|
@ -48,19 +47,17 @@ class UserController extends AbstractController
|
|||
private readonly UserOrganizationService $userOrganizationService,
|
||||
private readonly UserRepository $userRepository,
|
||||
private readonly UsersOrganizationsRepository $uoRepository,
|
||||
private readonly OrganizationsRepository $organizationRepository,
|
||||
private readonly LoggerInterface $userManagementLogger,
|
||||
private readonly LoggerInterface $organizationManagementLogger,
|
||||
private readonly LoggerInterface $accessControlLogger,
|
||||
private readonly LoggerInterface $EmailNotificationLogger,
|
||||
private readonly LoggerInterface $adminActionsLogger,
|
||||
private readonly LoggerInterface $errorLogger,
|
||||
private readonly LoggerInterface $SecurityLogger,
|
||||
private readonly EmailService $emailService,
|
||||
private readonly AwsService $awsService,
|
||||
private readonly OrganizationsService $organizationsService,
|
||||
private readonly AppsRepository $appsRepository,
|
||||
private readonly RolesRepository $rolesRepository,
|
||||
private readonly OrganizationsRepository $organizationRepository,
|
||||
private readonly LoggerInterface $userManagementLogger,
|
||||
private readonly LoggerInterface $organizationManagementLogger,
|
||||
private readonly LoggerInterface $errorLogger,
|
||||
private readonly LoggerInterface $securityLogger,
|
||||
private readonly LoggerService $loggerService,
|
||||
private readonly EmailService $emailService,
|
||||
private readonly AwsService $awsService,
|
||||
private readonly OrganizationsService $organizationsService,
|
||||
private readonly AppsRepository $appsRepository,
|
||||
private readonly RolesRepository $rolesRepository, private readonly AccessTokenService $accessTokenService,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
|
@ -76,13 +73,20 @@ class UserController extends AbstractController
|
|||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
|
||||
// Vérification des droits d'accès supplémentaires
|
||||
if (!$this->userService->hasAccessTo($actingUser)) {
|
||||
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||
}
|
||||
|
||||
|
||||
// Chargement de l'utilisateur cible à afficher
|
||||
$user = $this->userRepository->find($id);
|
||||
|
||||
if (!$user) {
|
||||
$this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getId());
|
||||
$this->addFlash('error', "L'utilisateur demandé n'existe pas.");
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
if (!$this->userService->hasAccessTo($user)) {
|
||||
$this->loggerService->logAccessDenied($actingUser->getId());
|
||||
$this->addFlash('error', "L'utilisateur demandé n'existe pas.");
|
||||
throw new AccessDeniedHttpException (self::ACCESS_DENIED);
|
||||
}
|
||||
try {
|
||||
// Paramètre optionnel de contexte organisationnel
|
||||
$orgId = $request->query->get('organizationId');
|
||||
|
|
@ -105,6 +109,11 @@ class UserController extends AbstractController
|
|||
]);
|
||||
|
||||
if (!$uoList) {
|
||||
$this->loggerService->logEntityNotFound('UsersOrganization', [
|
||||
'user_id' => $user->getId(),
|
||||
'organization_id' => $orgId],
|
||||
$actingUser->getId());
|
||||
$this->addFlash('error', "L'utilisateur n'est pas actif dans cette organisation.");
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
|
||||
|
|
@ -118,8 +127,15 @@ class UserController extends AbstractController
|
|||
'users' => $user,
|
||||
'isActive' => true,
|
||||
]);
|
||||
if (!$uoList) {
|
||||
$this->loggerService->logEntityNotFound('UsersOrganization', [
|
||||
'user_id' => $user->getId(),
|
||||
'organization_id' => $orgId],
|
||||
$actingUser->getId());
|
||||
$this->addFlash('error', "L'utilisateur n'est pas actif dans une organisation.");
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
// Charger les liens UserOrganizationApp (UOA) actifs pour les UO trouvées
|
||||
// Load user-organization-app roles (can be empty)
|
||||
$uoa = $this->entityManager
|
||||
|
|
@ -128,7 +144,6 @@ class UserController extends AbstractController
|
|||
'userOrganization' => $uoList,
|
||||
'isActive' => true,
|
||||
]);
|
||||
|
||||
// Group UOA by app and ensure every app has a group
|
||||
$data['uoas'] = $this->userOrganizationAppService
|
||||
->groupUserOrganizationAppsByApplication(
|
||||
|
|
@ -150,12 +165,13 @@ class UserController extends AbstractController
|
|||
// -------------------------------------------------------------------
|
||||
|
||||
// Calcul du flag de modification : utilisateur admin ET exactement 1 UO
|
||||
$canEdit = $this->userService->canEditRolesCheck($actingUser, $user, $organization, $this->isGranted('ROLE_ADMIN'));
|
||||
$canEdit = $this->userService->canEditRolesCheck($actingUser, $user,$this->isGranted('ROLE_ADMIN'), $singleUo, $organization);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
// En cas d'erreur, désactiver l'édition et logger l'exception
|
||||
$canEdit = false;
|
||||
$this->errorLogger->error($e->getMessage());
|
||||
$this->addFlash('error', 'Une erreur est survenue lors du chargement des informations utilisateur.');
|
||||
$referer = $request->headers->get('referer');
|
||||
return $this->redirect($referer ?? $this->generateUrl('app_index'));
|
||||
}
|
||||
return $this->render('user/show.html.twig', [
|
||||
'user' => $user,
|
||||
|
|
@ -171,63 +187,60 @@ class UserController extends AbstractController
|
|||
public function edit(int $id, Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_USER');
|
||||
try{
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$user = $this->userRepository->find($id);
|
||||
if (!$user) {
|
||||
$this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getId());
|
||||
$this->addFlash('error', "L'utilisateur demandé n'existe pas.");
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
try {
|
||||
if ($this->userService->hasAccessTo($user)) {
|
||||
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
if ($this->userService->hasAccessTo($actingUser)) {
|
||||
$user = $this->userRepository->find($id);
|
||||
if (!$user) {
|
||||
$this->userManagementLogger->notice('User not found for edit', [
|
||||
'target_user_id' => $user->getId(),
|
||||
'acting_user_id' => $actingUser->getId(),
|
||||
'ip' => $request->getClientIp(),
|
||||
'timestamp' => (new \DateTimeImmutable('now'))->format(DATE_ATOM),
|
||||
]);
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
$form = $this->createForm(UserForm::class, $user);
|
||||
$form->handleRequest($request);
|
||||
$this->userManagementLogger->notice('Format test', [
|
||||
'target_user_id' => $user->getId(),
|
||||
'acting_user_id' => $actingUser->getId(),
|
||||
'ip' => $request->getClientIp(),
|
||||
'timestamp' => (new \DateTimeImmutable('now'))->format(DATE_ATOM),
|
||||
]);
|
||||
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
// Handle user edit
|
||||
$picture = $form->get('pictureUrl')->getData();
|
||||
$this->userService->formatNewUserData($user, $picture);
|
||||
$this->userService->formatUserData($user, $picture);
|
||||
$user->setModifiedAt(new \DateTimeImmutable('now'));
|
||||
|
||||
$this->entityManager->persist($user);
|
||||
$this->entityManager->flush();
|
||||
|
||||
//log and action
|
||||
$this->userManagementLogger->notice('User information edited', [
|
||||
'target_user_id' => $user->getId(),
|
||||
'acting_user_id' => $actingUser->getId(),
|
||||
'organization_id' => $request->get('organizationId'),
|
||||
'ip' => $request->getClientIp(),
|
||||
'timestamp' => (new \DateTimeImmutable('now'))->format(DATE_ATOM),
|
||||
]);
|
||||
if ($request->get('organizationId')) {
|
||||
$org = $this->organizationRepository->find($request->get('organizationId'));
|
||||
$this->loggerService->logUserAction($user->getId(), $actingUser->getId(), 'User information edited');
|
||||
$orgId = $request->get('organizationId');
|
||||
if ($orgId) {
|
||||
$org = $this->organizationRepository->find($orgId);
|
||||
if ($org) {
|
||||
$this->actionService->createAction("Edit user information", $actingUser, $org, $user->getUserIdentifier());
|
||||
$this->organizationManagementLogger->info('User edited within organization context', [
|
||||
'target_user_id' => $user->getId(),
|
||||
'organization_id' => $org->getId(),
|
||||
'acting_user' => $actingUser->getUserIdentifier(),
|
||||
'ip' => $request->getClientIp(),
|
||||
]);
|
||||
return $this->redirectToRoute('user_show', ['id' => $user->getId(), 'organizationId' => $request->get('organizationId')]);
|
||||
$this->loggerService->logUserAction($user->getId(), $actingUser->getId(), 'User information edited');
|
||||
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
|
||||
$this->loggerService->logSuperAdmin(
|
||||
$user->getId(),
|
||||
$actingUser->getId(),
|
||||
"Super Admin accessed user edit page",
|
||||
);
|
||||
}
|
||||
$this->addFlash('success', 'Information modifié avec success.');
|
||||
return $this->redirectToRoute('user_show', ['id' => $user->getId(), 'organizationId' => $orgId]);
|
||||
}
|
||||
} else {
|
||||
$this->actionService->createAction("Edit user information", $actingUser, null, $user->getUserIdentifier());
|
||||
return $this->redirectToRoute('user_show', ['id' => $user->getId()]);
|
||||
$this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getId());
|
||||
$this->addFlash('error', "L'organisation n'existe pas.");
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
|
||||
$this->loggerService->logSuperAdmin(
|
||||
$user->getId(),
|
||||
$actingUser->getId(),
|
||||
"Super Admin accessed user edit page",
|
||||
);
|
||||
}
|
||||
$this->addFlash('success', 'Information modifié avec success.');
|
||||
$this->actionService->createAction("Edit user information", $actingUser, null, $user->getUserIdentifier());
|
||||
return $this->redirectToRoute('user_show', ['id' => $user->getId()]);
|
||||
}
|
||||
|
||||
return $this->render('user/edit.html.twig', [
|
||||
|
|
@ -236,15 +249,16 @@ class UserController extends AbstractController
|
|||
'organizationId' => $request->get('organizationId')
|
||||
]);
|
||||
}
|
||||
}catch (\Exception $e){
|
||||
$this->loggerService->logAccessDenied($actingUser->getId());
|
||||
$this->addFlash('error', "Accès non autorisé.");
|
||||
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||
} catch (\Exception $e) {
|
||||
$this->addFlash('error', 'Une erreur est survenue lors de la modification des informations utilisateur.');
|
||||
$this->errorLogger->critical($e->getMessage());
|
||||
}
|
||||
$this->SecurityLogger->warning('Access denied on user edit', [
|
||||
'target_user_id' => $id,
|
||||
'acting_user' => $actingUser?->getId(),
|
||||
'ip' => $request->getClientIp(),
|
||||
]);
|
||||
// Default deny access. shouldn't reach here normally.
|
||||
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||
|
||||
}
|
||||
|
||||
#[Route('/new', name: 'new', methods: ['GET', 'POST'])]
|
||||
|
|
@ -253,70 +267,128 @@ class UserController extends AbstractController
|
|||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
try {
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
if ($this->userService->hasAccessTo($actingUser)) {
|
||||
$user = new User();
|
||||
$form = $this->createForm(UserForm::class, $user);
|
||||
$form->handleRequest($request);
|
||||
$orgId = $request->get('organizationId');
|
||||
if ($orgId){
|
||||
$org = $this->organizationRepository->find($orgId) ?? throw new NotFoundHttpException(sprintf('%s not found', $orgId));
|
||||
}
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$existingUser = $this->userRepository->findOneBy(['email' => $user->getEmail()]);
|
||||
if ($existingUser && $orgId) {
|
||||
$this->userService->handleExistingUser($existingUser, $org);
|
||||
|
||||
$this->actionService->createAction("Create new user", $existingUser, $org, "Added user to organization" . $existingUser->getUserIdentifier() . " for organization " . $org->getName());
|
||||
$this->logger->notice("User added to organization " . $org->getName());
|
||||
$this->emailService->sendExistingUserNotificationEmail($existingUser, $org);
|
||||
$this->logger->notice("Existing user notification email sent to " . $existingUser->getUserIdentifier());
|
||||
$data = ['user' => $existingUser, 'organization' => $org];
|
||||
$this->organizationsService->notifyOrganizationAdmins($data, 'USER_INVITED');
|
||||
return $this->redirectToRoute('organization_show', ['id' => $orgId]);
|
||||
}
|
||||
|
||||
|
||||
// Handle file upload
|
||||
$picture = $form->get('pictureUrl')->getData();
|
||||
$this->userService->formatNewUserData($user, $picture, true);
|
||||
|
||||
if ($orgId) {
|
||||
$uo = new UsersOrganizations();
|
||||
$uo->setUsers($user);
|
||||
$uo->setOrganization($org);
|
||||
$uo->setStatut("INVITED");
|
||||
$uo->setIsActive(false);
|
||||
$uo->setModifiedAt(new \DateTimeImmutable('now'));
|
||||
$this->entityManager->persist($uo);
|
||||
$this->actionService->createAction("Create new user", $user, $org, "Added user to organization" . $user->getUserIdentifier() . " for organization " . $org->getName());
|
||||
$this->logger->notice("User added to organization " . $org->getName());
|
||||
$this->emailService->sendPasswordSetupEmail($user, $orgId);
|
||||
$this->logger->notice("Password setup email sent to " . $user->getUserIdentifier());
|
||||
$data = ['user' => $uo->getUsers(), 'organization' => $uo->getOrganization()];
|
||||
$this->organizationsService->notifyOrganizationAdmins($data, 'USER_INVITED');
|
||||
}
|
||||
$this->actionService->createAction("Create new user", $actingUser, null, $user->getUserIdentifier());
|
||||
$this->logger->notice("User created " . $user->getUserIdentifier());
|
||||
$this->entityManager->persist($user);
|
||||
$this->entityManager->flush();
|
||||
|
||||
if ($orgId) {
|
||||
return $this->redirectToRoute('organization_show', ['organizationId' => $orgId]);
|
||||
}
|
||||
return $this->redirectToRoute('user_index');
|
||||
}
|
||||
if (!$this->userService->hasAccessTo($actingUser)) {
|
||||
$this->loggerService->logAccessDenied($actingUser->getId());
|
||||
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||
}
|
||||
|
||||
$user = new User();
|
||||
$form = $this->createForm(UserForm::class, $user);
|
||||
$form->handleRequest($request);
|
||||
|
||||
$orgId = $request->get('organizationId');
|
||||
if ($orgId) {
|
||||
$org = $this->organizationRepository->find($orgId);
|
||||
if (!$org) {
|
||||
$this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getId());
|
||||
$this->addFlash('error', "L'organisation n'existe pas.");
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
if($this->isGranted('ROLE_ADMIN') && !$this->userService->isAdminOfOrganization($org) && !$this->isGranted('ROLE_SUPER_ADMIN')) {
|
||||
$this->loggerService->logAccessDenied($actingUser->getId());
|
||||
$this->addFlash('error', "Accès non autorisé.");
|
||||
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||
}
|
||||
}elseif($this->isGranted('ROLE_ADMIN')) {
|
||||
$this->loggerService->logAccessDenied($actingUser->getId());
|
||||
$this->addFlash('error', "Accès non autorisé.");
|
||||
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||
}
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$existingUser = $this->userRepository->findOneBy(['email' => $user->getEmail()]);
|
||||
|
||||
// Case : User exists + has organization context
|
||||
if ($existingUser && $org) {
|
||||
$this->userService->addExistingUserToOrganization(
|
||||
$existingUser,
|
||||
$org,
|
||||
$actingUser,
|
||||
);
|
||||
|
||||
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
|
||||
$this->loggerService->logSuperAdmin(
|
||||
$existingUser->getId(),
|
||||
$actingUser->getId(),
|
||||
"Super Admin linked user to organization",
|
||||
$org->getId(),
|
||||
);
|
||||
}
|
||||
$this->addFlash('success', 'Utilisateur ajouté avec succès à l\'organisation. ');
|
||||
return $this->redirectToRoute('organization_show', ['id' => $orgId]);
|
||||
}
|
||||
|
||||
//Code semi-mort : On ne peut plus créer un utilisateur sans organisation
|
||||
// Case : User exists but NO organization context -> throw error on email field.
|
||||
|
||||
// if ($existingUser) {
|
||||
// $this->loggerService->logError('Attempt to create user with existing email without organization', [
|
||||
// 'target_user_email' => $user->getid(),
|
||||
// 'acting_user_id' => $actingUser->getId(),
|
||||
// ]);
|
||||
//
|
||||
// $form->get('email')->addError(
|
||||
// new \Symfony\Component\Form\FormError(
|
||||
// 'This email is already in use. Add the user to an organization instead.'
|
||||
// )
|
||||
// );
|
||||
//
|
||||
// return $this->render('user/new.html.twig', [
|
||||
// 'user' => $user,
|
||||
// 'form' => $form->createView(),
|
||||
// 'organizationId' => $orgId,
|
||||
// ]);
|
||||
// }
|
||||
|
||||
$picture = $form->get('pictureUrl')->getData();
|
||||
$this->userService->createNewUser($user, $actingUser, $picture);
|
||||
|
||||
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
|
||||
$this->loggerService->logSuperAdmin(
|
||||
$user->getId(),
|
||||
$actingUser->getId(),
|
||||
"Super Admin created new user",
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
// Case : Organization provided and user doesn't already exist
|
||||
if ($orgId) {
|
||||
$this->userService->linkUserToOrganization(
|
||||
$user,
|
||||
$org,
|
||||
$actingUser,
|
||||
);
|
||||
|
||||
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
|
||||
$this->loggerService->logSuperAdmin(
|
||||
$user->getId(),
|
||||
$actingUser->getId(),
|
||||
"Super Admin linked user to organization during creation",
|
||||
$org->getId()
|
||||
);
|
||||
}
|
||||
$this->addFlash('success', 'Nouvel utilisateur créé et ajouté à l\'organisation avec succès. ');
|
||||
return $this->redirectToRoute('organization_show', ['id' => $orgId]);
|
||||
}
|
||||
$this->addFlash('success', 'Nouvel utilisateur créé avec succès. ');
|
||||
return $this->redirectToRoute('user_index');
|
||||
}
|
||||
|
||||
return $this->render('user/new.html.twig', [
|
||||
'user' => $user,
|
||||
'form' => $form->createView(),
|
||||
'organizationId' => $orgId
|
||||
'organizationId' => $orgId,
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error($e->getMessage());
|
||||
$this->errorLogger->critical($e->getMessage());
|
||||
|
||||
if ($orgId) {
|
||||
$this->addFlash('error', 'Une erreur est survenue lors de la création de l\'utilisateur pour l\'organisation .');
|
||||
return $this->redirectToRoute('organization_show', ['id' => $orgId]);
|
||||
}
|
||||
$this->addFlash('error', 'Une erreur est survenue lors de la création de l\'utilisateur.');
|
||||
return $this->redirectToRoute('user_index');
|
||||
}
|
||||
}
|
||||
|
|
@ -326,44 +398,98 @@ class UserController extends AbstractController
|
|||
public function activeStatus(int $id, Request $request): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
try{
|
||||
if ($this->userService->hasAccessTo($actingUser, true)) {
|
||||
$user = $this->userRepository->find($id);
|
||||
if (!$user) {
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
$status = $request->get('status');
|
||||
if ($status === 'deactivate') {
|
||||
$user->setIsActive(false);
|
||||
$this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, $user);
|
||||
if ($this->userService->isUserConnected($user->getUserIdentifier())) {
|
||||
$this->userService->revokeUserTokens($user->getUserIdentifier());
|
||||
}
|
||||
$user->setModifiedAt(new \DateTimeImmutable('now'));
|
||||
$this->entityManager->persist($user);
|
||||
$this->entityManager->flush();
|
||||
$this->logger->notice("User deactivated " . $user->getUserIdentifier());
|
||||
$this->actionService->createAction("Deactivate user", $actingUser, null, $user->getUserIdentifier());
|
||||
return new JsonResponse(['status' => 'deactivated'], Response::HTTP_OK);
|
||||
$status = $request->get('status');
|
||||
try {
|
||||
// Access control
|
||||
if (!$this->userService->hasAccessTo($actingUser, true)) {
|
||||
$this->loggerService->logAccessDenied($actingUser->getId());
|
||||
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||
}
|
||||
|
||||
// Load target user
|
||||
$user = $this->userRepository->find($id);
|
||||
if (!$user) {
|
||||
$this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getId());
|
||||
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
|
||||
// Deactivate
|
||||
if ($status === 'deactivate') {
|
||||
$user->setIsActive(false);
|
||||
|
||||
$this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, $user);
|
||||
|
||||
if ($this->userService->isUserConnected($user->getUserIdentifier())) {
|
||||
$this->accessTokenService->revokeUserTokens($user->getUserIdentifier());
|
||||
}
|
||||
|
||||
if ($status === 'activate') {
|
||||
$user->setIsActive(true);
|
||||
$user->setModifiedAt(new \DateTimeImmutable('now'));
|
||||
$this->logger->notice("User activated " . $user->getUserIdentifier());
|
||||
$this->actionService->createAction("Activate user", $actingUser, null, $user->getUserIdentifier());
|
||||
return new JsonResponse(['status' => 'activated'], Response::HTTP_OK);
|
||||
$user->setModifiedAt(new \DateTimeImmutable('now'));
|
||||
$this->entityManager->persist($user);
|
||||
$this->entityManager->flush();
|
||||
$this->loggerService->logUserAction($user->getId(), $actingUser->getId(), 'User deactivated');
|
||||
|
||||
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
|
||||
$this->loggerService->logSuperAdmin(
|
||||
$user->getId(),
|
||||
$actingUser->getId(),
|
||||
'Super admin deactivated user'
|
||||
);
|
||||
}
|
||||
|
||||
$this->actionService->createAction('Deactivate user', $actingUser, null, $user->getUserIdentifier());
|
||||
|
||||
return new JsonResponse(['status' => 'deactivated'], Response::HTTP_OK);
|
||||
}
|
||||
}catch (\Exception $e){
|
||||
$this->logger->error($e->getMessage());
|
||||
|
||||
// Activate
|
||||
if ($status === 'activate') {
|
||||
$user->setIsActive(true);
|
||||
$user->setModifiedAt(new \DateTimeImmutable('now'));
|
||||
$this->entityManager->persist($user);
|
||||
$this->entityManager->flush();
|
||||
$this->loggerService->logUserAction($user->getId(), $actingUser->getId(), 'User activated');
|
||||
|
||||
|
||||
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
|
||||
$this->loggerService->logSuperAdmin(
|
||||
$user->getId(),
|
||||
$actingUser->getId(),
|
||||
'Super admin activated user'
|
||||
);
|
||||
}
|
||||
|
||||
$this->actionService->createAction('Activate user', $actingUser, null, $user->getUserIdentifier());
|
||||
|
||||
return new JsonResponse(['status' => 'activated'], Response::HTTP_OK);
|
||||
}
|
||||
|
||||
// Invalid status
|
||||
$this->loggerService->logError('Invalid status provided for activeStatus', [
|
||||
'requested_status' => $status,
|
||||
'target_user_id' => $id,
|
||||
]);
|
||||
|
||||
return new JsonResponse(['error' => 'Status invalide'], Response::HTTP_BAD_REQUEST);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
// Application-level error logging → error.log (via error channel)
|
||||
$this->errorLogger->critical($e->getMessage());
|
||||
|
||||
// Preserve 403/404 semantics, 500 for everything else
|
||||
if ($e instanceof NotFoundHttpException || $e instanceof AccessDeniedException) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return new JsonResponse(['error' => 'Une erreur est survenue'], Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[Route('/organization/activateStatus/{id}', name: 'activate_organization', methods: ['GET', 'POST'])]
|
||||
public function activateStatusOrganization(int $id, Request $request): JsonResponse{
|
||||
public function activateStatusOrganization(int $id, Request $request): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
try {
|
||||
|
|
@ -371,15 +497,19 @@ class UserController extends AbstractController
|
|||
$orgId = $request->get('organizationId');
|
||||
$org = $this->organizationRepository->find($orgId);
|
||||
if (!$org) {
|
||||
$this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getId());
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
$user = $this->userRepository->find($id);
|
||||
if (!$user) {
|
||||
$this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getId());
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
$uo = $this->uoRepository->findOneBy(['users' => $user,
|
||||
'organization' => $org]);
|
||||
if (!$uo) {
|
||||
$this->loggerService->logEntityNotFound('UsersOrganization', ['user_id' => $user->getId(),
|
||||
'organization_id' => $org->getId()], $actingUser->getId());
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
$status = $request->get('status');
|
||||
|
|
@ -391,55 +521,115 @@ class UserController extends AbstractController
|
|||
$data = ['user' => $user,
|
||||
'organization' => $org];
|
||||
$this->organizationsService->notifyOrganizationAdmins($data, "USER_DEACTIVATED");
|
||||
$this->logger->notice("User Organizaton deactivated " . $user->getUserIdentifier());
|
||||
$this->loggerService->logOrganizationInformation($org->getId(), $actingUser->getId(), "UO link deactivated with uo id : {$uo->getId()}");
|
||||
$this->actionService->createAction("Deactivate user in organization", $actingUser, $org, $org->getName() . " for user " . $user->getUserIdentifier());
|
||||
return new JsonResponse(['status' => 'deactivated'], Response::HTTP_OK);
|
||||
}
|
||||
if($status === "activate"){
|
||||
if ($status === "activate") {
|
||||
$uo->setIsActive(true);
|
||||
$this->entityManager->persist($uo);
|
||||
$this->entityManager->flush();
|
||||
$this->loggerService->logOrganizationInformation($orgId, $actingUser->getId(), "UO link activated with uo id : {$uo->getId()}");
|
||||
$this->actionService->createAction("Activate user in organization", $actingUser, $org, $org->getName() . " for user " . $user->getUserIdentifier());
|
||||
$data = ['user' => $user,
|
||||
'organization' => $org];
|
||||
$this->organizationsService->notifyOrganizationAdmins($data, "USER_ACTIVATED");
|
||||
return new JsonResponse(['status' => 'activated'], Response::HTTP_OK);
|
||||
}
|
||||
//invalid status
|
||||
$this->loggerService->logError('Invalid status provided for activateStatusOrganization', [
|
||||
'requested_status' => $status,
|
||||
'target_user_id' => $id,
|
||||
'organization_id' => $orgId,
|
||||
]);
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
}catch (\Exception $exception){
|
||||
$this->logger->error($exception->getMessage());
|
||||
} catch (\Exception $exception) {
|
||||
$this->loggerService->logCritical($exception->getMessage());
|
||||
}
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
|
||||
//TODO : MONOLOG + remove picture from bucket
|
||||
//TODO :remove picture from bucket
|
||||
#[Route('/delete/{id}', name: 'delete', methods: ['GET', 'POST'])]
|
||||
public function delete(int $id, Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted("ROLE_SUPER_ADMIN");
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$user = $this->userRepository->find($id);
|
||||
if (!$user) {
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
$user->setIsActive(false);
|
||||
$user->setModifiedAt(new \DateTimeImmutable('now'));
|
||||
$this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, $user);
|
||||
$user->setIsDeleted(true);
|
||||
if ($this->userService->isUserConnected($user)) {
|
||||
$this->userService->revokeUserTokens($user->getUserIdentifier());
|
||||
}
|
||||
$this->entityManager->persist($user);
|
||||
$this->entityManager->flush();
|
||||
$this->actionService->createAction("Delete user", $actingUser, null, $user->getUserIdentifier());
|
||||
$data = ['user' => $user,
|
||||
'organization' => null];
|
||||
$this->organizationsService->notifyOrganizationAdmins($data, "USER_DELETED");
|
||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
||||
|
||||
return new Response('', Response::HTTP_NO_CONTENT); //204
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
|
||||
try {
|
||||
$user = $this->userRepository->find($id);
|
||||
if (!$user) {
|
||||
// Security/audit log for missing user
|
||||
$this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getId());
|
||||
$this->addFlash('error', "L'utilisateur demandé n'existe pas.");
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
|
||||
// Soft delete the user
|
||||
|
||||
$user->setIsActive(false);
|
||||
$user->setIsDeleted(true);
|
||||
$user->setModifiedAt(new \DateTimeImmutable('now'));
|
||||
// Deactivate all org links
|
||||
$this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, $user);
|
||||
$this->loggerService->logOrganizationInformation($user->getId(), $actingUser->getId(), 'All user organization links deactivated');
|
||||
|
||||
// Revoke tokens if connected
|
||||
if ($this->userService->isUserConnected($user->getUserIdentifier())) {
|
||||
$this->accessTokenService->revokeUserTokens($user->getUserIdentifier());
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
// User management log
|
||||
$this->loggerService->logUserAction($user->getId(), $actingUser->getId(), 'User deleted');
|
||||
|
||||
// Super admin log (standardized style)
|
||||
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
|
||||
$this->loggerService->logSuperAdmin(
|
||||
$user->getId(),
|
||||
$actingUser->getId(),
|
||||
'Super admin deleted user'
|
||||
);
|
||||
}
|
||||
|
||||
$this->actionService->createAction('Delete user', $actingUser, null, $user->getUserIdentifier());
|
||||
|
||||
// Notify organization admins (user may belong to multiple organizations)
|
||||
try {
|
||||
$data = [
|
||||
'user' => $user,
|
||||
'organization' => null,
|
||||
];
|
||||
$this->organizationsService->notifyOrganizationAdmins($data, 'USER_DELETED');
|
||||
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$this->loggerService->logCritical($e->getMessage(), [
|
||||
'target_user_id' => $id,
|
||||
'acting_user_id' => $actingUser?->getId(),
|
||||
]);
|
||||
}
|
||||
$this->addFlash('success', 'Utilisateur supprimé avec succès.');
|
||||
return $this->redirectToRoute('user_index');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
// Route-level error logging → error.log
|
||||
$this->loggerService->logCritical('error while deleting user', [
|
||||
'target_user_id' => $id,
|
||||
'acting_user_id' => $actingUser?->getId(),
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
if ($e instanceof NotFoundHttpException) {
|
||||
throw $e; // keep 404 semantics
|
||||
}
|
||||
$this->addFlash('error', 'Erreur lors de la suppression de l\'utilisateur\.');
|
||||
return $this->redirectToRoute('user_index');
|
||||
}
|
||||
}
|
||||
|
||||
//TODO : MONOLOG
|
||||
#[Route(path: '/application/roles/{id}', name: 'application_role', methods: ['GET', 'POST'])]
|
||||
public function applicationRole(int $id, Request $request): Response
|
||||
{
|
||||
|
|
@ -447,19 +637,29 @@ class UserController extends AbstractController
|
|||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
|
||||
if ($this->userService->hasAccessTo($actingUser, true)) {
|
||||
$uo = $this->userOrganizationService->getByIdOrFail($id);
|
||||
$uo = $this->entityManager->getRepository(UsersOrganizations::class)->find($id);
|
||||
if (!$uo) {
|
||||
$this->loggerService->logEntityNotFound('UsersOrganization', ['id' => $id], $actingUser->getId());
|
||||
$this->addFlash('error', "La liaison utilisateur-organisation n'existe pas.");
|
||||
throw new NotFoundHttpException("UserOrganization not found");
|
||||
}
|
||||
$application = $this->entityManager->getRepository(Apps::class)->find($request->get('appId'));
|
||||
if (!$application) {
|
||||
$this->loggerService->logEntityNotFound('Application', ['id' => $request->get('appId')], $actingUser->getId());
|
||||
$this->addFlash('error', "L'application demandée n'existe pas.");
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
|
||||
$selectedRolesIds = $request->get('roles', []);
|
||||
$roleUser = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'USER']);
|
||||
if (!$roleUser) {
|
||||
throw $this->createNotFoundException('Default role not found');
|
||||
$this->loggerService->logEntityNotFound('Role', ['name' => 'USER'], $actingUser->getId());
|
||||
$this->addFlash('error', "Le role de l'utilisateur n'existe pas.");
|
||||
throw $this->createNotFoundException('User role not found');
|
||||
}
|
||||
|
||||
if (!empty($selectedRolesIds)) {
|
||||
// Si le role User n'est pas sélectionné, on désactive tous les liens (affiché comme 'accès' dans l'UI)
|
||||
if (!in_array((string)$roleUser->getId(), $selectedRolesIds, true)) {
|
||||
$this->userOrganizationAppService->deactivateAllUserOrganizationsAppLinks($uo, $application);
|
||||
} else {
|
||||
|
|
@ -476,6 +676,7 @@ class UserController extends AbstractController
|
|||
}
|
||||
|
||||
$user = $uo->getUsers();
|
||||
$this->addFlash('success', 'Rôles mis à jour avec succès.');
|
||||
return $this->redirectToRoute('user_show', [
|
||||
'user' => $user,
|
||||
'id' => $user->getId(),
|
||||
|
|
@ -542,7 +743,6 @@ class UserController extends AbstractController
|
|||
'statut' => $user->isActive(),
|
||||
];
|
||||
}, $rows);
|
||||
|
||||
$lastPage = (int)ceil($total / $size);
|
||||
|
||||
return $this->json([
|
||||
|
|
@ -555,6 +755,7 @@ class UserController extends AbstractController
|
|||
#[Route(path: '/', name: 'index', methods: ['GET'])]
|
||||
public function index(): Response
|
||||
{
|
||||
$this->isGranted('ROLE_SUPER_ADMIN');
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
if ($this->userService->hasAccessTo($actingUser, true) && $this->isGranted("ROLE_ADMIN")) {
|
||||
$totalUsers = $this->userRepository->count(['isDeleted' => false, 'isActive' => true]);
|
||||
|
|
@ -562,6 +763,9 @@ class UserController extends AbstractController
|
|||
'users' => $totalUsers
|
||||
]);
|
||||
}
|
||||
|
||||
//shouldn't be reached normally
|
||||
$this->loggerService->logAccessDenied($actingUser->getId());
|
||||
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||
}
|
||||
|
||||
|
|
@ -715,27 +919,38 @@ class UserController extends AbstractController
|
|||
$orgId = $request->get('organizationId');
|
||||
$org = $this->organizationRepository->find($orgId);
|
||||
if (!$org) {
|
||||
$this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getId());
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
$user = $this->userRepository->find($userId);
|
||||
if (!$user) {
|
||||
$this->loggerService->logEntityNotFound('User', ['id' => $user->getId()], $actingUser->getId());
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
$uo = $this->uoRepository->findOneBy(['users' => $user,
|
||||
'organization' => $org,
|
||||
'statut' => "INVITED"]);
|
||||
if (!$uo) {
|
||||
$this->loggerService->logEntityNotFound('UsersOrganization', [
|
||||
'user_id' => $user->getId(),
|
||||
'organization_id' => $orgId], $actingUser->getId());
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
$uo->setModifiedAt(new \DateTimeImmutable());
|
||||
try {
|
||||
$data = ['user' => $uo->getUsers(), 'organization' => $uo->getOrganization()];
|
||||
$this->emailService->sendPasswordSetupEmail($user, $orgId);
|
||||
$token = $this->userService->generatePasswordToken($user, $org->getId());
|
||||
$this->emailService->sendPasswordSetupEmail($user, $token);
|
||||
$this->logger->info("Invitation email resent to user " . $user->getUserIdentifier() . " for organization " . $org->getName());
|
||||
$this->organizationsService->notifyOrganizationAdmins($data, 'USER_INVITED');
|
||||
return $this->json(['message' => 'Invitation envoyée avec success.'], Response::HTTP_OK);
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error("Error resending invitation email to user " . $user->getUserIdentifier() . " for organization " . $org->getName() . ": " . $e->getMessage());
|
||||
$this->loggerService->logCritical('Error while resending invitation', [
|
||||
'target_user_id' => $user->getId(),
|
||||
'organization_id' => $orgId,
|
||||
'acting_user_id' => $actingUser->getId(),
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
return $this->json(['message' => 'Erreur lors de l\'envoie du mail.'], Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
|
@ -749,29 +964,46 @@ class UserController extends AbstractController
|
|||
$userId = $request->get('id');
|
||||
|
||||
if (!$token || !$userId) {
|
||||
$this->loggerService->logEntityNotFound('Token or UserId missing in accept invitation', [
|
||||
'token' => $token,
|
||||
'user_id' => $userId
|
||||
],
|
||||
null);
|
||||
throw $this->createNotFoundException('Invalid invitation link.');
|
||||
}
|
||||
$user = $this->userRepository->find($userId);
|
||||
if (!$user) {
|
||||
$this->loggerService->logEntityNotFound('User not found in accept invitation', [
|
||||
'user_id' => $userId
|
||||
],null);
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
if (!$this->userService->isPasswordTokenValid($user, $token)) {
|
||||
$this->loggerService->logError('Token or UserId mismatch in accept invitation', [
|
||||
'token' => $token,
|
||||
'user_id' => $userId
|
||||
]);
|
||||
throw $this->createNotFoundException('Invalid or expired invitation token.');
|
||||
}
|
||||
$orgId = $this->userService->getOrgFromToken($token);
|
||||
$uo = $this->uoRepository->findOneBy(['users' => $user, 'organization' => $orgId]);
|
||||
if (!$uo || $uo->getStatut() !== 'INVITED') {
|
||||
$this->logger->warning("User " . $user->getUserIdentifier() . " tried to accept an invitation but no pending invitation was found for organization ID " . $orgId);
|
||||
throw $this->createNotFoundException('No pending invitation found for this user and organization.');
|
||||
if ($orgId) {
|
||||
$uo = $this->uoRepository->findOneBy(['users' => $user, 'organization' => $orgId]);
|
||||
if (!$uo || $uo->getStatut() !== 'INVITED') {
|
||||
$this->loggerService->logEntityNotFound('UsersOrganization not found or not in INVITED status in accept invitation', [
|
||||
'user_id' => $user->getId(),
|
||||
'organization_id' => $orgId
|
||||
], null);
|
||||
throw $this->createNotFoundException('No pending invitation found for this user and organization.');
|
||||
}
|
||||
$uo->setModifiedAt(new \DateTimeImmutable());
|
||||
$uo->setStatut("ACCEPTED");
|
||||
$uo->setIsActive(true);
|
||||
$this->entityManager->persist($uo);
|
||||
$this->entityManager->flush();
|
||||
$this->loggerService->logUserAction($user->getId(), null, "User accepted invitation for organization id : {$orgId}");
|
||||
$this->loggerService->logOrganizationInformation($orgId, $user->getId(), "User accepted invitation with uo id : {$uo->getId()}");
|
||||
}
|
||||
$uo->setModifiedAt(new \DateTimeImmutable());
|
||||
$uo->setStatut("ACCEPTED");
|
||||
$uo->setIsActive(true);
|
||||
$this->entityManager->persist($uo);
|
||||
$this->entityManager->flush();
|
||||
$this->logger->info("User " . $user->getUserIdentifier() . " accepted invitation for organization ID " . $orgId);
|
||||
|
||||
return $this->render('user/show.html.twig', ['user' => $user, 'orgId' => $orgId]);
|
||||
return $this->render('security/login.html.twig');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ class Apps
|
|||
{
|
||||
$this->userOrganizatonApps = new ArrayCollection();
|
||||
$this->organization = new ArrayCollection();
|
||||
$this->setIsActive(true);
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
|
|
|
|||
|
|
@ -4,10 +4,13 @@ namespace App\Entity;
|
|||
|
||||
use App\Repository\OrganizationsRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: OrganizationsRepository::class)]
|
||||
#[ORM\UniqueConstraint(name: 'UNIQ_ORGANIZATION_EMAIL', fields: ['email'])]
|
||||
#[UniqueEntity(fields: ['email'], message: 'Une organisation avec cet email existe déjà.')]
|
||||
class Organizations
|
||||
{
|
||||
#[ORM\Id]
|
||||
|
|
@ -24,7 +27,7 @@ class Organizations
|
|||
#[ORM\Column(length: 255)]
|
||||
private ?string $address = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $logo_url = null;
|
||||
|
||||
#[ORM\Column(options: ['default' => 'CURRENT_TIMESTAMP'])]
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ use Doctrine\DBAL\Types\Types;
|
|||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
|
||||
#[ORM\Entity(repositoryClass: UserRepository::class)]
|
||||
#[ORM\Table(name: '`user`')]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
->add('email', EmailType::class, ['required' => true, 'label' => 'Email*'])
|
||||
->add('name', TextType::class, ['required' => true, 'label' => 'Nom de l\'organisation*'])
|
||||
->add('address', TextType::class, ['required' => false, 'label' => 'Adresse'])
|
||||
->add('number', TextType::class, ['required' => false, 'label' => 'Numéro de téléphone'])
|
||||
->add('address', TextType::class, ['required' => true, 'label' => 'Adresse'])
|
||||
->add('number', TextType::class, ['required' => true, 'label' => 'Numéro de téléphone'])
|
||||
->add('logoUrl', FileType::class, [
|
||||
'required' => false,
|
||||
'label' => 'Logo',
|
||||
|
|
|
|||
|
|
@ -11,17 +11,38 @@ class AccessTokenService
|
|||
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
public function __construct(EntityManagerInterface $entityManager,
|
||||
private readonly LoggerService $loggerService)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
public function revokeTokens(String $userIdentifier): void {
|
||||
$accessTokens = $this->entityManager->getRepository(AccessToken::class)->findBy(['userIdentifier' => $userIdentifier, 'revoked' => false]);
|
||||
foreach($accessTokens as $accessToken) {
|
||||
$accessToken->revoke();
|
||||
$this->entityManager->persist($accessToken);
|
||||
$this->entityManager->flush();
|
||||
public function revokeUserTokens(string $userIdentifier): void
|
||||
{
|
||||
$tokens = $this->entityManager->getRepository(AccessToken::class)->findBy([
|
||||
'userIdentifier' => $userIdentifier,
|
||||
'revoked' => false
|
||||
]);
|
||||
foreach ($tokens as $token) {
|
||||
try{
|
||||
$token->revoke();
|
||||
$this->loggerService->logTokenRevocation(
|
||||
'Access token revoked for user',
|
||||
[
|
||||
'user_identifier' => $userIdentifier,
|
||||
'token_id' => $token->getIdentifier(),
|
||||
]
|
||||
);
|
||||
}catch (\Exception $e){
|
||||
$this->loggerService->logError(
|
||||
'Error revoking access token: ' . $e->getMessage(),
|
||||
[
|
||||
'user_identifier' => $userIdentifier,
|
||||
'token_id' => $token->getIdentifier(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,11 +40,11 @@ readonly class ActionService
|
|||
{
|
||||
return array_map(function (Actions $activity) {
|
||||
return [
|
||||
'date' => $activity->getDate(),
|
||||
'date' => $activity->getDate()->format('d/m/Y H:i'),
|
||||
'actionType' => $activity->getActionType(),
|
||||
'users' => $activity->getUsers(),
|
||||
'organization' => $activity->getOrganization(),
|
||||
'description' => $activity->getDescription(),
|
||||
'userName' => $activity->getUsers()->getName(),
|
||||
// 'organization' => $activity->getOrganization(),
|
||||
// 'description' => $activity->getDescription(),
|
||||
'color' => $this->getActivityColor($activity->getDate())
|
||||
];
|
||||
}, $activities);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Service\LoggerService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use App\Entity\Cgu;
|
||||
|
|
@ -9,7 +10,7 @@ use App\Entity\CguUser;
|
|||
|
||||
class CguUserService
|
||||
{
|
||||
public function __construct(private EntityManagerInterface $entityManager)
|
||||
public function __construct(private EntityManagerInterface $entityManager, private readonly LoggerService $loggerService)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -40,11 +41,20 @@ class CguUserService
|
|||
|
||||
$cguUser = $this->entityManager->getRepository(CguUser::class)->findOneBy(['users' => $user, 'cgu' => $latestCgu]);
|
||||
if (!$cguUser) {
|
||||
// Create a new CguUser relation if it doesn't exist
|
||||
$cguUser = new CguUser();
|
||||
$cguUser->setUsers($user);
|
||||
$cguUser->setCgu($latestCgu);
|
||||
$this->entityManager->persist($cguUser);
|
||||
try{
|
||||
// Create a new CguUser relation if it doesn't exist
|
||||
$cguUser = new CguUser();
|
||||
$cguUser->setUsers($user);
|
||||
$cguUser->setCgu($latestCgu);
|
||||
$this->entityManager->persist($cguUser);
|
||||
}catch (\Exception $e){
|
||||
$this->loggerService->logError('CguUserService', [
|
||||
'acceptLatestCgu' => 'Failed to create CguUser relation',
|
||||
'exception' => $e,
|
||||
'targer_user_id' => $user->getId(),]);
|
||||
throw new \RuntimeException('Failed to create CguUser relation: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$cguUser->setIsAccepted(true);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ namespace App\Service;
|
|||
|
||||
use App\Entity\Organizations;
|
||||
use App\Entity\User;
|
||||
use App\Service\LoggerService;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||
|
|
@ -14,15 +15,12 @@ class EmailService
|
|||
{
|
||||
public function __construct(
|
||||
private readonly MailerInterface $mailer,
|
||||
private readonly UserService $userService,
|
||||
private readonly LoggerInterface $logger,
|
||||
private UrlGeneratorInterface $urlGenerator
|
||||
private UrlGeneratorInterface $urlGenerator, private readonly LoggerService $loggerService
|
||||
) {}
|
||||
|
||||
public function sendPasswordSetupEmail(User $user, int $orgId): void
|
||||
public function sendPasswordSetupEmail(User $user, string $token): void
|
||||
{
|
||||
$token = $this->userService->generatePasswordToken($user, $orgId);
|
||||
|
||||
// Generate absolute URL for the password setup route
|
||||
$link = $this->urlGenerator->generate(
|
||||
'password_setup',
|
||||
|
|
@ -46,15 +44,16 @@ class EmailService
|
|||
]);
|
||||
|
||||
try {
|
||||
$orgId = $this->getOrgFromToken($token);
|
||||
$this->mailer->send($email);
|
||||
$this->loggerService->logEmailSent($user->getId(), $orgId, 'Password setup email sent.');
|
||||
} catch (\Symfony\Component\Mailer\Exception\TransportExceptionInterface $e) {
|
||||
$this->logger->error('Failed to send password setup email: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function sendExistingUserNotificationEmail(User $existingUser, Organizations $org): void
|
||||
public function sendExistingUserNotificationEmail(User $existingUser, Organizations $org, $token): void
|
||||
{
|
||||
$token = $this->userService->generatePasswordToken($existingUser, $org->getId());
|
||||
$link = $this->urlGenerator->generate('user_accept',[
|
||||
'id' => $existingUser->getId(),
|
||||
'token' => $token
|
||||
|
|
@ -73,10 +72,26 @@ class EmailService
|
|||
]);
|
||||
|
||||
try{
|
||||
$orgId = $org->getId();
|
||||
$this->loggerService->logEmailSent($existingUser->getId(), $orgId, 'Existing user notification email sent.');
|
||||
$this->mailer->send($email);
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
$this->logger->error('Failed to send existing user notification email: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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(
|
||||
recipient: $recipient,
|
||||
type: self::TYPE_USER_DEACTIVATED,
|
||||
title: 'Membre retiré',
|
||||
title: 'Membre désactivé',
|
||||
message: sprintf('%s %s a été désactivé de %s', $removedUser->getName(), $removedUser->getSurname(), $organization->getName()),
|
||||
data: [
|
||||
'userId' => $removedUser->getId(),
|
||||
|
|
|
|||
|
|
@ -6,8 +6,11 @@ use App\Entity\Apps;
|
|||
use App\Entity\Organizations;
|
||||
use App\Entity\Roles;
|
||||
use App\Entity\UserOrganizatonApp;
|
||||
use App\Entity\UsersOrganizations;
|
||||
use App\Repository\UsersOrganizationsRepository;
|
||||
use App\Service\LoggerService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\File\Exception\FileException;
|
||||
|
||||
class OrganizationsService
|
||||
|
|
@ -15,10 +18,11 @@ class OrganizationsService
|
|||
private string $logoDirectory;
|
||||
|
||||
public function __construct(
|
||||
string $logoDirectory, private readonly AwsService $awsService,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
string $logoDirectory, private readonly AwsService $awsService,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly UsersOrganizationsRepository $uoRepository,
|
||||
private readonly NotificationService $notificationService
|
||||
private readonly NotificationService $notificationService,
|
||||
private readonly LoggerInterface $emailNotificationLogger, private readonly LoggerService $loggerService,
|
||||
)
|
||||
{
|
||||
$this->logoDirectory = $logoDirectory;
|
||||
|
|
@ -32,8 +36,18 @@ class OrganizationsService
|
|||
|
||||
try {
|
||||
$this->awsService->PutDocObj($_ENV['S3_PORTAL_BUCKET'], $logoFile, $customFilename, $extension, 'logo/');
|
||||
$this->loggerService->logAWSAction('Upload organization logo', [
|
||||
'organization_id' => $organization->getId(),
|
||||
'filename' => $customFilename,
|
||||
'bucket' => $_ENV['S3_PORTAL_BUCKET'],
|
||||
]);
|
||||
$organization->setLogoUrl('logo/' . $customFilename);
|
||||
} catch (FileException $e) {
|
||||
$this->loggerService->logError('Failed to upload organization logo to S3', [
|
||||
'organization_id' => $organization->getId(),
|
||||
'error' => $e->getMessage(),
|
||||
'bucket' => $_ENV['S3_PORTAL_BUCKET'],
|
||||
]);
|
||||
throw new FileException('Failed to upload logo to S3: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
|
@ -85,6 +99,10 @@ class OrganizationsService
|
|||
$newUser,
|
||||
$data['organization']
|
||||
);
|
||||
$this->loggerService->logAdminNotified([
|
||||
'admin_user_id' =>$adminUO->getUsers()->getId(),
|
||||
'target_user_id' => $newUser->getId(),
|
||||
'organization_id' => $data['organization']->getId(),'case' =>$type]);
|
||||
}
|
||||
break;
|
||||
case 'USER_INVITED':
|
||||
|
|
@ -95,7 +113,12 @@ class OrganizationsService
|
|||
$invitedUser,
|
||||
$data['organization']
|
||||
);
|
||||
$this->loggerService->logAdminNotified([
|
||||
'admin_user_id' =>$adminUO->getUsers()->getId(),
|
||||
'target_user_id' => $invitedUser->getId(),
|
||||
'organization_id' => $data['organization']->getId(),'case' =>$type]);
|
||||
}
|
||||
|
||||
break;
|
||||
case 'USER_DEACTIVATED':
|
||||
if ($uoa && $adminUO->getUsers()->getId() !== $data['user']->getId() ) {
|
||||
|
|
@ -105,7 +128,12 @@ class OrganizationsService
|
|||
$removedUser,
|
||||
$data['organization']
|
||||
);
|
||||
$this->loggerService->logAdminNotified([
|
||||
'admin_user_id' =>$adminUO->getUsers()->getId(),
|
||||
'target_user_id' => $removedUser->getId(),
|
||||
'organization_id' => $data['organization']->getId(),'case' =>$type]);
|
||||
}
|
||||
|
||||
break;
|
||||
case 'USER_DELETED':
|
||||
if ($uoa && $adminUO->getUsers()->getId() !== $data['user']->getId() ) {
|
||||
|
|
@ -115,6 +143,10 @@ class OrganizationsService
|
|||
$removedUser,
|
||||
$data['organization']
|
||||
);
|
||||
$this->loggerService->logAdminNotified([
|
||||
'admin_user_id' =>$adminUO->getUsers()->getId(),
|
||||
'target_user_id' => $removedUser->getId(),
|
||||
'organization_id' => $data['organization']->getId(),'case' =>$type]);
|
||||
}
|
||||
break;
|
||||
case 'USER_ACTIVATED':
|
||||
|
|
@ -125,6 +157,10 @@ class OrganizationsService
|
|||
$activatedUser,
|
||||
$data['organization']
|
||||
);
|
||||
$this->loggerService->logAdminNotified([
|
||||
'admin_user_id' =>$adminUO->getUsers()->getId(),
|
||||
'target_user_id' => $activatedUser->getId(),
|
||||
'organization_id' => $data['organization']->getId(),'case' =>$type]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,13 +8,15 @@ use App\Entity\User;
|
|||
use App\Entity\UserOrganizatonApp;
|
||||
use App\Entity\UsersOrganizations;
|
||||
use App\Service\ActionService;
|
||||
use App\Service\LoggerService;
|
||||
use App\Service\UserService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
|
||||
class UserOrganizationAppService
|
||||
{
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager, private readonly ActionService $actionService, private readonly Security $security, private readonly UserService $userService)
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager, private readonly ActionService $actionService, private readonly Security $security, private readonly UserService $userService, private readonly LoggerInterface $logger, private readonly LoggerService $loggerService)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -79,10 +81,20 @@ class UserOrganizationAppService
|
|||
$uoas = $this->entityManager->getRepository(UserOrganizatonApp::class)->findBy(['userOrganization' => $userOrganization, 'isActive' => true]);
|
||||
}
|
||||
foreach ($uoas as $uoa) {
|
||||
$uoa->setIsActive(false);
|
||||
$this->actionService->createAction("Deactivate UOA link", $userOrganization->getUsers(),
|
||||
$userOrganization->getOrganization(), "App: " . $uoa->getApplication()->getName() . ", Role: " . $uoa->getRole()->getName());
|
||||
$this->entityManager->persist($uoa);
|
||||
try{
|
||||
$uoa->setIsActive(false);
|
||||
$this->actionService->createAction("Deactivate UOA link", $userOrganization->getUsers(),
|
||||
$userOrganization->getOrganization(), "App: " . $uoa->getApplication()->getName() . ", Role: " . $uoa->getRole()->getName());
|
||||
$this->entityManager->persist($uoa);
|
||||
$this->loggerService->logUOALinkDeactivated($uoa->getId(), $uoa->getApplication()->getId(), $uoa->getRole()->getId());
|
||||
}catch (\Exception $exception){
|
||||
$this->loggerService->logCritical("Error deactivating UOA link", [
|
||||
'uoa_id' => $uoa->getId(),
|
||||
'app_id' => $uoa->getApplication()->getId(),
|
||||
'role_id' => $uoa->getRole()->getId(),
|
||||
'exception_message' => $exception->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -128,6 +140,11 @@ class UserOrganizationAppService
|
|||
if (!$uoa->isActive()) {
|
||||
$uoa->setIsActive(true);
|
||||
$this->entityManager->persist($uoa);
|
||||
$this->loggerService->logOrganizationInformation(
|
||||
$uo->getOrganization()->getId(),
|
||||
$actingUser->getId(),
|
||||
"Re-activated role '$roleName' for user '{$uo->getUsers()->getId()}' in application '{$application->getName()} with UOA ID {$uoa->getId()}'"
|
||||
);
|
||||
$this->actionService->createAction(
|
||||
"Re-activate user role for application",
|
||||
$actingUser,
|
||||
|
|
@ -148,7 +165,11 @@ class UserOrganizationAppService
|
|||
if ($uoa->isActive()) {
|
||||
$uoa->setIsActive(false);
|
||||
$this->entityManager->persist($uoa);
|
||||
|
||||
$this->loggerService->logOrganizationInformation(
|
||||
$uo->getOrganization()->getId(),
|
||||
$actingUser->getId(),
|
||||
"Deactivated role '$roleName' for user '{$uo->getUsers()->getId()}' in application '{$application->getName()}' with UOA ID {$uoa->getId()}'"
|
||||
);
|
||||
$this->actionService->createAction(
|
||||
"Deactivate user role for application",
|
||||
$actingUser,
|
||||
|
|
@ -185,6 +206,11 @@ class UserOrganizationAppService
|
|||
$this->ensureAdminRoleForSuperAdmin($newUoa);
|
||||
}
|
||||
$this->entityManager->persist($newUoa);
|
||||
$this->loggerService->logOrganizationInformation(
|
||||
$uo->getOrganization()->getId(),
|
||||
$actingUser->getId(),
|
||||
"Created new role '{$role->getName()}' for user '{$uo->getUsers()->getId()}' in application '{$application->getName()}' with UOA ID {$newUoa->getId()}'"
|
||||
);
|
||||
$this->actionService->createAction("New user role for application",
|
||||
$actingUser,
|
||||
$uo->getOrganization(),
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ use App\Entity\Organizations;
|
|||
use App\Entity\User;
|
||||
use App\Entity\UsersOrganizations;
|
||||
use App\Service\ActionService;
|
||||
use App\Service\LoggerService;
|
||||
use \App\Service\UserOrganizationAppService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
|
@ -19,7 +20,7 @@ readonly class UserOrganizationService
|
|||
{
|
||||
|
||||
public function __construct(
|
||||
private userOrganizationAppService $userOrganizationAppService, private EntityManagerInterface $entityManager, private ActionService $actionService,
|
||||
private userOrganizationAppService $userOrganizationAppService, private EntityManagerInterface $entityManager, private ActionService $actionService, private LoggerService $loggerService,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -41,22 +42,19 @@ readonly class UserOrganizationService
|
|||
$uos = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['organization' => $organizations, 'isActive' => true]);
|
||||
}
|
||||
//deactivate all UO links
|
||||
foreach ($uos as $uo) {
|
||||
$this->userOrganizationAppService->deactivateAllUserOrganizationsAppLinks($uo);
|
||||
$uo->setIsActive(false);
|
||||
$this->entityManager->persist($uo);
|
||||
$this->actionService->createAction("Deactivate UO link", $actingUser, $uo->getOrganization(), $uo->getOrganization()->getName() );
|
||||
if (!empty($uos)) {
|
||||
foreach ($uos as $uo) {
|
||||
$this->userOrganizationAppService->deactivateAllUserOrganizationsAppLinks($uo);
|
||||
$this->loggerService->logOrganizationInformation($uo->getOrganization()->getId(), $actingUser->getId(),
|
||||
'Uo link deactivated');
|
||||
$uo->setIsActive(false);
|
||||
$this->entityManager->persist($uo);
|
||||
$this->actionService->createAction("Deactivate UO link", $actingUser, $uo->getOrganization(), $uo->getOrganization()->getName() );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function getByIdOrFail(int $id): UsersOrganizations
|
||||
{
|
||||
$uo = $this->entityManager->getRepository(UsersOrganizations::class)->find($id);
|
||||
if (!$uo) {
|
||||
throw new NotFoundHttpException("UserOrganization not found");
|
||||
}
|
||||
return $uo;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ use App\Entity\Roles;
|
|||
use App\Entity\User;
|
||||
use App\Entity\UserOrganizatonApp;
|
||||
use App\Entity\UsersOrganizations;
|
||||
use App\Service\AwsService;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
|
@ -16,18 +15,25 @@ use Doctrine\ORM\EntityNotFoundException;
|
|||
use Exception;
|
||||
use League\Bundle\OAuth2ServerBundle\Model\AccessToken;
|
||||
use Random\RandomException;
|
||||
use SebastianBergmann\CodeCoverage\Util\DirectoryCouldNotBeCreatedException;
|
||||
use RuntimeException;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpFoundation\File\Exception\FileException;
|
||||
use App\Event\UserCreatedEvent;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
class UserService
|
||||
{
|
||||
|
||||
public const NOT_FOUND = 'Entity not found';
|
||||
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager,
|
||||
private readonly Security $security,
|
||||
private readonly AwsService $awsService
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager,
|
||||
private readonly Security $security,
|
||||
private readonly AwsService $awsService,
|
||||
private readonly LoggerService $loggerService,
|
||||
private readonly ActionService $actionService,
|
||||
private readonly EmailService $emailService,
|
||||
private readonly OrganizationsService $organizationsService,
|
||||
private readonly EventDispatcherInterface $eventDispatcher
|
||||
)
|
||||
{
|
||||
|
||||
|
|
@ -39,16 +45,7 @@ class UserService
|
|||
*/
|
||||
public function generateRandomPassword(): string
|
||||
{
|
||||
$length = 50; // Length of the password
|
||||
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+';
|
||||
$charactersLength = strlen($characters);
|
||||
$randomPassword = '';
|
||||
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$randomPassword .= $characters[random_int(0, $charactersLength - 1)];
|
||||
}
|
||||
|
||||
return $randomPassword;
|
||||
return bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -88,20 +85,20 @@ class UserService
|
|||
*/
|
||||
public function hasAccessTo(User $user, bool $skipSelfCheck = false): bool
|
||||
{
|
||||
if ($this->security->isGranted('ROLE_SUPER_ADMIN')) {
|
||||
return true;
|
||||
}
|
||||
if (!$skipSelfCheck && $user->getUserIdentifier() === $this->security->getUser()->getUserIdentifier()) {
|
||||
return true;
|
||||
}
|
||||
$userOrganizations = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['users' => $user]);
|
||||
if ($userOrganizations) {
|
||||
foreach ($userOrganizations as $uo) {
|
||||
if ($this->isAdminOfOrganization($uo->getOrganization())) {
|
||||
if ($this->isAdminOfOrganization($uo->getOrganization()) && $uo->getStatut() === "ACCEPTED" && $uo->isActive()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($this->security->isGranted('ROLE_SUPER_ADMIN')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
|
@ -112,7 +109,7 @@ class UserService
|
|||
* entity role 'ROLE_ADMIN' in the UsersOrganizationsApp entity
|
||||
* (if he is admin for any application of the organization).
|
||||
*
|
||||
* @param UsersOrganizations $usersOrganizations
|
||||
* @param Organizations $organizations
|
||||
* @return bool
|
||||
* @throws Exception
|
||||
*/
|
||||
|
|
@ -144,6 +141,7 @@ class UserService
|
|||
{
|
||||
$user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $userIdentifier]);
|
||||
if (!$user) {
|
||||
$this->loggerService->logEntityNotFound('User', ['user_identifier' => $userIdentifier], null);
|
||||
throw new EntityNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
return $user;
|
||||
|
|
@ -175,18 +173,20 @@ class UserService
|
|||
return ['none' => $group];
|
||||
}
|
||||
|
||||
//TODO: reset function
|
||||
public function handleProfilePicture(User $user, $picture): void
|
||||
{
|
||||
// Get file extension
|
||||
$extension = $picture->guessExtension();
|
||||
|
||||
// Create custom filename: userNameUserSurname_ddmmyyhhmmss
|
||||
// Create custom filename: userNameUserSurname_dmyHis
|
||||
$customFilename = $user->getName() . $user->getSurname() . '_' . date('dmyHis') . '.' . $extension;
|
||||
// $customFilename = $user->getName() . $user->getSurname() . "." .$extension;
|
||||
try {
|
||||
$this->awsService->PutDocObj($_ENV['S3_PORTAL_BUCKET'], $picture, $customFilename, $extension, 'profile/');
|
||||
|
||||
$this->loggerService->logAWSAction(
|
||||
'Profile picture uploaded to S3', [
|
||||
'user_id' => $user->getId(),
|
||||
'filename' => $customFilename,
|
||||
]);
|
||||
$user->setPictureUrl('profile/' . $customFilename);
|
||||
} catch (FileException $e) {
|
||||
// Handle upload error
|
||||
|
|
@ -242,12 +242,18 @@ class UserService
|
|||
if ($roleFormatted === 'ROLE_SUPER_ADMIN' && !in_array('ROLE_ADMIN', $user->getRoles(), true)) {
|
||||
$user->setRoles(array_merge($user->getRoles(), ['ROLE_ADMIN']));
|
||||
}
|
||||
$this->loggerService->logRoleAssignment(
|
||||
'Role assigned to user',
|
||||
[
|
||||
'user_id' => $user->getId(),
|
||||
'role' => $roleFormatted,
|
||||
]
|
||||
);
|
||||
} else {
|
||||
// Remove the role if present and not used elsewhere
|
||||
if (in_array($roleFormatted, $user->getRoles(), true)) {
|
||||
$uos = $this->entityManager->getRepository(UsersOrganizations::class)
|
||||
->findBy(['users' => $user, 'isActive' => true]);
|
||||
|
||||
$hasRole = false;
|
||||
foreach ($uos as $uo) {
|
||||
$uoa = $this->entityManager->getRepository(UserOrganizatonApp::class)
|
||||
|
|
@ -257,7 +263,6 @@ class UserService
|
|||
'role' => $this->entityManager->getRepository(Roles::class)
|
||||
->findOneBy(['name' => $role]),
|
||||
]);
|
||||
|
||||
if ($uoa) {
|
||||
$hasRole = true;
|
||||
break;
|
||||
|
|
@ -287,17 +292,6 @@ class UserService
|
|||
return 'ROLE_' . $role;
|
||||
}
|
||||
|
||||
public function revokeUserTokens(string $userIdentifier)
|
||||
{
|
||||
$tokens = $this->entityManager->getRepository(AccessToken::class)->findBy([
|
||||
'userIdentifier' => $userIdentifier,
|
||||
'revoked' => false
|
||||
]);
|
||||
|
||||
foreach ($tokens as $token) {
|
||||
$token->revoke();
|
||||
}
|
||||
}
|
||||
|
||||
public function formatStatutForOrganizations(array $rows): array
|
||||
{
|
||||
|
|
@ -330,7 +324,7 @@ class UserService
|
|||
return $formatted;
|
||||
}
|
||||
|
||||
public function generatePasswordToken(User $user, int $orgId): string
|
||||
public function generatePasswordToken(User $user, int $orgId = null): string
|
||||
{
|
||||
$orgString = "o" . $orgId . "@";
|
||||
$token = $orgString . bin2hex(random_bytes(32));
|
||||
|
|
@ -413,15 +407,17 @@ class UserService
|
|||
return $rolesArray;
|
||||
}
|
||||
|
||||
public function canEditRolesCheck(User $actingUser, User $user, $org, bool $isAdmin): bool
|
||||
public function canEditRolesCheck(User $actingUser, User $user, bool $isAdmin, UsersOrganizations $uo = null, $org = null): bool
|
||||
{
|
||||
$userRoles = $user->getRoles();
|
||||
$actingUserRoles = $actingUser->getRoles();
|
||||
// if acting user is admin, he can´t edit super admin roles
|
||||
|
||||
if (in_array('ROLE_SUPER_ADMIN', $userRoles, true) && !in_array('ROLE_SUPER_ADMIN', $actingUserRoles, true)) {
|
||||
if (!in_array('ROLE_SUPER_ADMIN', $actingUserRoles, true) && in_array('ROLE_SUPER_ADMIN', $userRoles, true)) {
|
||||
return false;
|
||||
}
|
||||
if ($uo && $this->isAdminOfOrganization($uo->getOrganization())) {
|
||||
return true;
|
||||
}
|
||||
return $isAdmin && !empty($org);
|
||||
|
||||
}
|
||||
|
|
@ -434,7 +430,7 @@ class UserService
|
|||
* @param Organizations $organization
|
||||
* @return void
|
||||
*/
|
||||
public function handleExistingUser(User $user, Organizations $organization): void
|
||||
public function handleExistingUser(User $user, Organizations $organization): int
|
||||
{
|
||||
if (!$user->isActive()) {
|
||||
$user->setIsActive(true);
|
||||
|
|
@ -448,6 +444,8 @@ class UserService
|
|||
$uo->setModifiedAt(new \DateTimeImmutable('now'));
|
||||
$this->entityManager->persist($uo);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $uo->getId();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -460,7 +458,7 @@ class UserService
|
|||
* @param User $user
|
||||
* @return void
|
||||
*/
|
||||
public function formatNewUserData(User $user, $picture, bool $setPassword = false): void
|
||||
public function formatUserData(User $user, $picture, bool $setPassword = false): void
|
||||
{
|
||||
// capitalize name and surname
|
||||
$user->setName(ucfirst(strtolower($user->getName())));
|
||||
|
|
@ -470,13 +468,163 @@ class UserService
|
|||
$user->setName(trim($user->getName()));
|
||||
$user->setSurname(trim($user->getSurname()));
|
||||
$user->setEmail(trim($user->getEmail()));
|
||||
if($setPassword) {
|
||||
if ($setPassword) {
|
||||
//FOR SETTING A DEFAULT RANDOM PASSWORD OF 50 CHARACTERS until user set his own password
|
||||
$user->setPassword($this->generateRandomPassword());
|
||||
try {
|
||||
$user->setPassword(bin2hex(random_bytes(50)));
|
||||
} catch (RandomException $e) {
|
||||
$this->loggerService->logError('Error generating random password: ' . $e->getMessage(), [
|
||||
'target_user_id' => $user->getId(),
|
||||
]);
|
||||
throw new RuntimeException('Error generating random password: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
if($picture) {
|
||||
if ($picture) {
|
||||
$this->handleProfilePicture($user, $picture);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle existing user being added to an organization
|
||||
*/
|
||||
public function addExistingUserToOrganization(
|
||||
User $existingUser,
|
||||
Organizations $org,
|
||||
User $actingUser,
|
||||
): int
|
||||
{
|
||||
try {
|
||||
$uoId = $this->handleExistingUser($existingUser, $org);
|
||||
|
||||
$this->loggerService->logExistingUserAddedToOrg(
|
||||
$existingUser->getId(),
|
||||
$org->getId(),
|
||||
$actingUser->getId(),
|
||||
$uoId,
|
||||
);
|
||||
$this->actionService->createAction(
|
||||
"Add existing user to organization",
|
||||
$actingUser,
|
||||
$org,
|
||||
"Added user {$existingUser->getUserIdentifier()} to {$org->getName()}"
|
||||
);
|
||||
$this->sendExistingUserNotifications($existingUser, $org, $actingUser);
|
||||
|
||||
return $uoId;
|
||||
} catch (\Exception $e) {
|
||||
$this->loggerService->logError('Error linking existing user to organization: ' . $e->getMessage(), [
|
||||
'target_user_id' => $existingUser->getId(),
|
||||
'organization_id' => $org->getId(),
|
||||
'acting_user_id' => $actingUser->getId(),
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a brand-new user
|
||||
*/
|
||||
public function createNewUser(User $user, User $actingUser, $picture): void
|
||||
{
|
||||
try {
|
||||
$this->formatUserData($user, $picture, true);
|
||||
|
||||
// Generate token here if it's part of the user persistence flow
|
||||
$token = $this->generatePasswordToken($user);
|
||||
|
||||
$this->entityManager->persist($user);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->eventDispatcher->dispatch(new UserCreatedEvent($user, $actingUser));
|
||||
|
||||
} catch (\Exception $e) {
|
||||
// Error logging remains here because the event won't fire if exception occurs
|
||||
$this->loggerService->logError('Error creating new user: ' . $e->getMessage(), [
|
||||
'target_user_email' => $user->getEmail(),
|
||||
'acting_user_id' => $actingUser->getId(),
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Link newly created user to an organization
|
||||
*/
|
||||
public function linkUserToOrganization(
|
||||
User $user,
|
||||
Organizations $org,
|
||||
User $actingUser,
|
||||
): UsersOrganizations
|
||||
{
|
||||
try {
|
||||
$uo = new UsersOrganizations();
|
||||
$uo->setUsers($user);
|
||||
$uo->setOrganization($org);
|
||||
$uo->setStatut("INVITED");
|
||||
$uo->setIsActive(false);
|
||||
$uo->setModifiedAt(new \DateTimeImmutable('now'));
|
||||
$this->entityManager->persist($uo);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->loggerService->logUserOrganizationLinkCreated(
|
||||
$user->getId(),
|
||||
$org->getId(),
|
||||
$actingUser->getId(),
|
||||
$uo->getId(),
|
||||
|
||||
);
|
||||
|
||||
$this->actionService->createAction(
|
||||
"Link user to organization",
|
||||
$actingUser,
|
||||
$org,
|
||||
"Added {$user->getUserIdentifier()} to {$org->getName()}"
|
||||
);
|
||||
|
||||
$this->sendNewUserNotifications($user, $org, $actingUser);
|
||||
|
||||
return $uo;
|
||||
} catch (\Exception $e) {
|
||||
$this->loggerService->logError('Error linking user to organization: ' . $e->getMessage(), [
|
||||
'target_user_id' => $user->getId(),
|
||||
'organization_id' => $org->getId(),
|
||||
'acting_user_id' => $actingUser->getId(),
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
// Private helpers for email notifications
|
||||
private function sendExistingUserNotifications(User $user, Organizations $org, User $actingUser): void
|
||||
{
|
||||
try {
|
||||
$token = $this->generatePasswordToken($user, $org->getId());
|
||||
$this->emailService->sendExistingUserNotificationEmail($user, $org, $token);
|
||||
$this->loggerService->logExistingUserNotificationSent($user->getId(), $org->getId());
|
||||
$this->organizationsService->notifyOrganizationAdmins(['user' => $user, 'acting_user_id' => $actingUser->getId(),
|
||||
'organization' => $org], 'USER_INVITED');
|
||||
} catch (\Exception $e) {
|
||||
$this->loggerService->logError("Error sending existing user notification: " . $e->getMessage(), [
|
||||
'target_user_id' => $user->getId(),
|
||||
'organization_id' => $org->getId(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function sendNewUserNotifications(User $user, Organizations $org, User $actingUser): void
|
||||
{
|
||||
try {
|
||||
$token = $this->generatePasswordToken($user, $org->getId());
|
||||
$this->emailService->sendPasswordSetupEmail($user, $token);
|
||||
$this->organizationsService->notifyOrganizationAdmins(['user' => $user, 'acting_user_id' => $actingUser->getId(),
|
||||
'organization' => $org], 'USER_INVITED');
|
||||
} catch (\Exception $e) {
|
||||
$this->loggerService->logError("Error sending password setup email: " . $e->getMessage(), [
|
||||
'target_user_id' => $user->getId(),
|
||||
'organization_id' => $org->getId(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
12
symfony.lock
12
symfony.lock
|
|
@ -11,6 +11,18 @@
|
|||
"config/packages/aws.yaml"
|
||||
]
|
||||
},
|
||||
"dama/doctrine-test-bundle": {
|
||||
"version": "8.3",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes-contrib",
|
||||
"branch": "main",
|
||||
"version": "8.3",
|
||||
"ref": "dfc51177476fb39d014ed89944cde53dc3326d23"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/dama_doctrine_test_bundle.yaml"
|
||||
]
|
||||
},
|
||||
"doctrine/deprecations": {
|
||||
"version": "1.1",
|
||||
"recipe": {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
{% if application.hasAccess %}
|
||||
{% if is_granted("ROLE_SUPER_ADMIN") %}
|
||||
<form method="POST"
|
||||
action="{{ path('application_remove', {'id': application.entity.id}) }}"
|
||||
action="{{ path('application_revoke', {'id': application.entity.id}) }}"
|
||||
data-controller="application"
|
||||
data-application-application-value="{{ application.entity.name }}"
|
||||
data-application-organization-value="{{ organization.name|capitalize }}"
|
||||
|
|
|
|||
|
|
@ -8,10 +8,22 @@
|
|||
<div class="w-100 h-100 p-5 m-auto">
|
||||
<div class="row m-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>
|
||||
<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>
|
||||
|
||||
{% 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 %}
|
||||
<div class="col-6 mb-3">
|
||||
{% 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-header border-0">
|
||||
<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 class="card-body">
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,13 @@
|
|||
|
||||
{% block body %}
|
||||
<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-header d-flex justify-content-between align-items-center border-0">
|
||||
<div class="card-title">
|
||||
|
|
@ -11,30 +18,31 @@
|
|||
</div>
|
||||
|
||||
{% 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 %}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card-body ">
|
||||
{% if organizationsData|length == 0 %}
|
||||
<div class="card-body">
|
||||
{% 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">
|
||||
<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>
|
||||
|
||||
{% else %}
|
||||
|
||||
<div id="tabulator-org" data-controller="organization"
|
||||
data-organization-data-value="{{ organizationsData|json_encode(constant("JSON_UNESCAPED_UNICODE"))|e("html_attr") }}"
|
||||
data-organization-aws-value="{{ aws_url }}"></div>
|
||||
|
||||
<div id="tabulator-org"
|
||||
data-controller="organization"
|
||||
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 %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -6,13 +6,20 @@
|
|||
<div class="w-100 h-100 p-5 m-auto">
|
||||
<div class="card no-header-bg p-3 m-3">
|
||||
<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">
|
||||
<h1>Ajouter une organisation</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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_widget(form) }}
|
||||
<button type="submit" class="btn btn-primary">Enregistrer</button>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,13 @@
|
|||
|
||||
{% block body %}
|
||||
<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="d-flex ">
|
||||
{% if organization.logoUrl %}
|
||||
|
|
@ -39,9 +46,9 @@
|
|||
</div>
|
||||
|
||||
</div>
|
||||
{# single row so that activity and users tabs are next to each other#}
|
||||
{# single row so that activity and users tabs are next to each other #}
|
||||
<div class="row">
|
||||
{# User tables #}
|
||||
{# User tables #}
|
||||
<div class="col-9">
|
||||
<div class="row mb-3 d-flex gap-2 ">
|
||||
<div class="col mb-3 card no-header-bg">
|
||||
|
|
@ -49,7 +56,8 @@
|
|||
<h2>
|
||||
Nouveaux utilisateurs
|
||||
</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 class="card-body">
|
||||
<div id="tabulator-userListSmall" data-controller="user"
|
||||
|
|
@ -93,7 +101,7 @@
|
|||
</div>
|
||||
|
||||
{# APPLICATION ROW #}
|
||||
{# TODO: Weird gap not going away#}
|
||||
{# TODO: Weird gap not going away #}
|
||||
<div class="row mb-3 ">
|
||||
{% for application in applications %}
|
||||
<div class="col-6 mb-3">
|
||||
|
|
@ -104,17 +112,37 @@
|
|||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{# Activities col#}
|
||||
{# Activities col #}
|
||||
<div class="col-3 m-auto">
|
||||
{% include 'organization/activity.html.twig' with {
|
||||
title: 'Activités récentes',
|
||||
empty_message: 'Aucune activité récente.'
|
||||
} %}
|
||||
<div class="card border-0"
|
||||
data-controller="organization"
|
||||
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 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>
|
||||
|
||||
{# 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>
|
||||
{# Ne pas enlever le 2ème /div#}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@
|
|||
<div class="card-title">
|
||||
<h2>Modifier l'utilisateur</h2>
|
||||
</div>
|
||||
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
||||
<a href="{{ path('user_delete', {'id': user.id}) }}" class="btn btn-danger m-3">Supprimer</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
|
|
|
|||
|
|
@ -5,11 +5,18 @@
|
|||
{% block body %}
|
||||
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
||||
<div class="w-100 h-100 p-5 m-auto">
|
||||
{% for type, messages in app.flashes %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ type }}">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
<div class="card p-3 m-3 border-0">
|
||||
<div class="card-header border-0">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 ">
|
||||
<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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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="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") %}
|
||||
<div class="card-header border-0 d-flex justify-content-between align-items-center ">
|
||||
|
|
@ -14,13 +21,6 @@
|
|||
{% if is_granted("ROLE_SUPER_ADMIN") %}
|
||||
<a href="{{ path('user_delete', {'id': user.id}) }}"
|
||||
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 %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,29 +13,12 @@
|
|||
|
||||
<div class="d-flex gap-2">
|
||||
{% 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}) }}"
|
||||
class="btn btn-primary">Modifier</a>
|
||||
{% endif %}
|
||||
{% elseif user.id == app.user.id or is_granted("ROLE_SUPER_ADMIN") %}
|
||||
<a href="{{ path('user_edit', {'id': user.id}) }}"
|
||||
class="btn btn-primary">Modifier</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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