Compare commits

...

16 Commits

Author SHA1 Message Date
Charles f2166b604e ignore uploads 2025-08-06 12:20:55 +02:00
Charles 8d92d3f9fc update organisation 2025-08-06 12:20:05 +02:00
Charles 5ceed1f2f2 Visual 2025-08-06 12:06:50 +02:00
Charles 450543fab7 logo 2025-08-06 11:54:24 +02:00
Charles d543e69863 Add role protection 2025-08-06 11:52:55 +02:00
Charles 7021b28163 Création d'organisation 2025-08-06 11:15:53 +02:00
Charles c55e9fa039 Correction 2025-08-06 08:52:58 +02:00
Charles bdf9f0478e update userOrg update info 2025-08-05 15:09:51 +02:00
Charles 1053a2ab22 Add user to organization 2025-08-05 11:16:45 +02:00
Charles 6efbeb0fa2 Display applications 2025-08-05 10:11:05 +02:00
Charles 1ee9a0110b AWS 2025-08-05 09:13:04 +02:00
Charles cbdb47fb17 set action log on user entity 2025-08-04 13:57:13 +02:00
Charles e6c8d5a462 update reamdme 2025-08-04 12:16:02 +02:00
Charles 7e272b2b2f Add actions display 2025-08-04 12:01:40 +02:00
Charles 6670fbc8b8 Ajout organistion Id au Actions 2025-08-04 10:57:05 +02:00
Charles 1e8d5e1eaf Ajout organistion Id au Actions 2025-08-04 10:56:53 +02:00
38 changed files with 2205 additions and 502 deletions

5
.env
View File

@ -62,3 +62,8 @@ MERCURE_PUBLIC_URL=https://example.com/.well-known/mercure
# The secret used to sign the JWTs
MERCURE_JWT_SECRET="!ChangeThisMercureHubJWTSecretKey!"
###< symfony/mercure-bundle ###
###> aws/aws-sdk-php-symfony ###
AWS_KEY=not-a-real-key
AWS_SECRET=@@not-a-real-secret
###< aws/aws-sdk-php-symfony ###

1
.gitignore vendored
View File

@ -5,6 +5,7 @@
/.env.*.local
/config/secrets/prod/prod.decrypt.private.php
/public/bundles/
/public/uploads/
/var/
/vendor/
###< symfony/framework-bundle ###

View File

@ -8,6 +8,16 @@
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mercure" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mercure-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/firebase/php-jwt" />
<excludeFolder url="file://$MODULE_DIR$/vendor/aws/aws-crt-php" />
<excludeFolder url="file://$MODULE_DIR$/vendor/aws/aws-sdk-php" />
<excludeFolder url="file://$MODULE_DIR$/vendor/aws/aws-sdk-php-symfony" />
<excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/guzzle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/promises" />
<excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/psr7" />
<excludeFolder url="file://$MODULE_DIR$/vendor/knplabs/knp-time-bundle" />
<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" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />

View File

@ -83,7 +83,6 @@
<path value="$PROJECT_DIR$/vendor/lcobucci/jwt" />
<path value="$PROJECT_DIR$/vendor/lcobucci/clock" />
<path value="$PROJECT_DIR$/vendor/twig/twig" />
<path value="$PROJECT_DIR$/vendor/twig/extra-bundle" />
<path value="$PROJECT_DIR$/vendor/composer" />
<path value="$PROJECT_DIR$/vendor/theseer/tokenizer" />
<path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" />
@ -163,6 +162,17 @@
<path value="$PROJECT_DIR$/vendor/symfony/mercure" />
<path value="$PROJECT_DIR$/vendor/symfony/mercure-bundle" />
<path value="$PROJECT_DIR$/vendor/firebase/php-jwt" />
<path value="$PROJECT_DIR$/vendor/doctrine/cache" />
<path value="$PROJECT_DIR$/vendor/knplabs/knp-time-bundle" />
<path value="$PROJECT_DIR$/vendor/aws/aws-sdk-php" />
<path value="$PROJECT_DIR$/vendor/aws/aws-crt-php" />
<path value="$PROJECT_DIR$/vendor/ralouphie/getallheaders" />
<path value="$PROJECT_DIR$/vendor/aws/aws-sdk-php-symfony" />
<path value="$PROJECT_DIR$/vendor/guzzlehttp/psr7" />
<path value="$PROJECT_DIR$/vendor/guzzlehttp/promises" />
<path value="$PROJECT_DIR$/vendor/guzzlehttp/guzzle" />
<path value="$PROJECT_DIR$/vendor/mtdowling/jmespath.php" />
<path value="$PROJECT_DIR$/vendor/psr/http-client" />
</include_path>
</component>
<component name="PhpProjectSharedConfiguration" php_language_level="8.2" />

View File

@ -6,20 +6,20 @@
- Stimulus
- Turbo
- Bootstrap 5.3
- Symfony UX toogle password (https://ux.symfony.com/toggle-password)
- Les icones sont gérées via symfony UX (https://ux.symfony.com/icons)
- Les icones sont prises en prioritées dans la bibliothèque bootstrap
- Les icones n'éxistants pas dans cette bibliothèques seront prises en priorité dans fontawesome regular (pour une cohérence visuelle)
- Sinon privilégier la bibliothèque ayant le visuel le plus proche
### Version 0.1 : (17/03/2025)
- Contient la logique de login mot de passe avec une entité user (email et password seuelement)
- Une base de template twig public est gérée pour les page n'ayant pas besoin de menu
- La page de login est designé
- Une base de template est gérée pour toutes les pages de l'application aya,t besoin de l'entête et du menu général
- Une ébauche de page d'accueil est en cours
### Installation
#### Database
```bash
php bin/console doctrine:database:create
php bin/console doctrine:schema:update --force
```
#### SQL
```bash
insert into public.roles (id, name, created_at)
values (3, 'USER', '2025-05-21 13:22:52'),
(2, 'ADMIN', '2025-05-21 13:22:52'),
(1, 'SUPER ADMIN', '2025-05-21 13:22:52');
```
#### Choices.js
```bash
php bin/console importmap:require choices.js

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -6,6 +6,8 @@
--delete : #E42E31;
--disable : #A3A3A3;
--check : #80F20E;
--secondary : #cc664c;
--secondary-dark : #a5543d;
}
html {
@ -113,4 +115,17 @@ body {
}
.color-primary-dark{
color: var(--primary-blue-dark);
}
.btn-secondary{
background: var(--secondary);
color : #FFFFFF;
border: var(--secondary);
border-radius: 1rem;
}
.btn-secondary:hover{
background: var(--secondary-dark);
color : #FFFFFF;
border: var(--secondary);
}

View File

@ -8,11 +8,13 @@
"ext-ctype": "*",
"ext-iconv": "*",
"ext-openssl": "*",
"aws/aws-sdk-php-symfony": "^2.8",
"doctrine/dbal": "^3",
"doctrine/doctrine-bundle": "^2.14",
"doctrine/doctrine-migrations-bundle": "^3.4",
"doctrine/orm": "^3.3",
"firebase/php-jwt": "^6.11",
"knplabs/knp-time-bundle": "^2.4",
"league/oauth2-server-bundle": "^0.11.0",
"nelmio/cors-bundle": "^2.5",
"phpdocumentor/reflection-docblock": "^5.6",
@ -49,7 +51,6 @@
"symfony/validator": "7.2.*",
"symfony/web-link": "7.2.*",
"symfony/yaml": "7.2.*",
"twig/extra-bundle": "^2.12|^3.0",
"twig/twig": "^2.12|^3.0"
},
"config": {

1649
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,6 @@ return [
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
Symfony\UX\Turbo\TurboBundle::class => ['all' => true],
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
@ -18,4 +17,6 @@ return [
League\Bundle\OAuth2ServerBundle\LeagueOAuth2ServerBundle::class => ['all' => true],
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
Symfony\Bundle\MercureBundle\MercureBundle::class => ['all' => true],
Knp\Bundle\TimeBundle\KnpTimeBundle::class => ['all' => true],
Aws\Symfony\AwsBundle::class => ['all' => true],
];

11
config/packages/aws.yaml Normal file
View File

@ -0,0 +1,11 @@
aws:
version: latest
region: "%env(AWS_REGION)%"
credentials:
key: "%env(AWS_KEY)%"
secret: "%env(AWS_SECRET)%"
S3:
region: "%env(AWS_REGION)%"
endpoint: "%env(AWS_ENDPOINT)%"
use_path_style_endpoint: true
signature_version: 'v4'

View File

@ -4,6 +4,9 @@
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
parameters:
aws_url: '%env(AWS_ENDPOINT)%'
aws_public_url: '%env(AWS_ENDPOINT)%'
logos_directory: '%kernel.project_dir%/public/uploads/logos'
services:
# default configuration for services in *this* file
@ -22,6 +25,9 @@ services:
App\EventSubscriber\:
resource: '../src/EventSubscriber/'
tags: ['kernel.event_subscriber']
App\Service\AwsService:
arguments:
$awsPublicUrl: '%aws_public_url%'
App\EventSubscriber\ScopeResolveListener:
tags:
- { name: kernel.event_listener, event: league.oauth2_server.event.scope_resolve, method: onScopeResolve }

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250804084150 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 actions ADD organization_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE actions ADD CONSTRAINT FK_548F1EF32C8A3DE FOREIGN KEY (organization_id) REFERENCES organizations (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('CREATE INDEX IDX_548F1EF32C8A3DE ON actions (organization_id)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE actions DROP CONSTRAINT FK_548F1EF32C8A3DE');
$this->addSql('DROP INDEX IDX_548F1EF32C8A3DE');
$this->addSql('ALTER TABLE actions DROP organization_id');
}
}

View File

@ -0,0 +1,34 @@
<?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 Version20250804085615 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('DROP INDEX uniq_548f1ef67b3b43d');
$this->addSql('CREATE INDEX IDX_548F1EF67B3B43D ON actions (users_id)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('DROP INDEX IDX_548F1EF67B3B43D');
$this->addSql('CREATE UNIQUE INDEX uniq_548f1ef67b3b43d ON actions (users_id)');
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250804101742 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 actions ADD description VARCHAR(255) DEFAULT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE actions DROP description');
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250804121445 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 apps ADD description_small VARCHAR(255) DEFAULT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE apps DROP description_small');
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -5,34 +5,36 @@ namespace App\Controller;
use App\Entity\Apps;
use App\Entity\Roles;
use App\Entity\UsersOrganizations;
use App\Form\OrganizationForm;
use App\Service\ActionService;
use App\Service\OrganizationsService;
use App\Service\UserOrganizationService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
use App\Entity\Organizations;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\Service\Attribute\Required;
#[Route(path: '/organization', name: 'organization_')]
class OrganizationController extends AbstractController
{
private const NOT_FOUND = 'Entity not found';
private const ACCESS_DENIED = 'Access denied';
public function __construct(private readonly EntityManagerInterface $entityManager,
private readonly OrganizationsService $organizationsService,
private readonly UserOrganizationService $usersOrganizationService)
public function __construct(private readonly EntityManagerInterface $entityManager,
private readonly OrganizationsService $organizationsService,
private readonly UserOrganizationService $usersOrganizationService)
{
}
#[Route(path: '/' , name: 'index', methods: ['GET'])]
public function index():Response
#[Route('/', name: 'index', methods: ['GET'])]
public function index(): Response
{
if($this->isGranted('ROLE_SUPER_ADMIN'))
{
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
$organizations = $this->entityManager->getRepository(Organizations::class)->findBy(['isActive' => true]);
} else{
} else {
$user = $this->getUser();
if (!$user) {
return $this->redirectToRoute('app_login');
@ -40,7 +42,7 @@ class OrganizationController extends AbstractController
$userIdentifier = $user->getUserIdentifier();
$organizations = $this->entityManager->getRepository(UsersOrganizations::class)->findOrganizationsByUserEmailAndRoleName($userIdentifier, 'ADMIN');
if(!$organizations) {
if (!$organizations) {
// if user is not admin in any organization, throw access denied
throw $this->createNotFoundException(self::ACCESS_DENIED);
}
@ -51,23 +53,52 @@ class OrganizationController extends AbstractController
]);
}
#[Route(path: '/{id}', name: 'show', methods: ['GET'])]
public function show(int $id): Response
#[Route('/new', name: 'new', methods: ['GET', 'POST'])]
public function new(Request $request): Response
{
if (!$this->isGranted('ROLE_SUPER_ADMIN')) {
throw $this->createNotFoundException(self::ACCESS_DENIED);
}
$form = $this->createForm(OrganizationForm::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$organization = $form->getData();
// dd($form);
$logoFile = $form->get('logoUrl')->getData();
if ($logoFile) {
$currentDate = (new \DateTime())->format('Y-m-d');
$organizationName = preg_replace('/[^a-zA-Z0-9]/', '_', $organization->getName());
$extension = $logoFile->guessExtension();
$newFilename = $currentDate . '_' . $organizationName . $extension;
// Move the file to the directory where logos are stored
$logoFile->move(
$this->getParameter('logos_directory'),
$newFilename
);
// Update the 'logoUrl' property to store the file name
$organization->setLogoUrl($newFilename);
}
$this->entityManager->persist($organization);
$this->entityManager->flush();
$this->addFlash('success', 'Organization created successfully');
return $this->redirectToRoute('organization_index');
}
return $this->render('organization/new.html.twig', [
'form' => $form->createView(),
]);
}
#[Route('/{id}', name: 'show', requirements: ['id' => '\d+'], methods: ['GET'])]
public function show(int $id, ActionService $actionService): Response
{
if ($this->isGranted('ROLE_ADMIN')) {
$user = $this->getUser();
if (!$user) {
return $this->redirectToRoute('app_login');
}
$roleAdmin = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN']);
$uo = $this->entityManager->getRepository(UsersOrganizations::class)->findOneBy([
'users' => $user,
'organization' => $id,
'role' => $roleAdmin
]);
if (!$uo) {
throw $this->createNotFoundException(self::ACCESS_DENIED);
}
//Don't care about the null pointer because if no UO found, it won't pass the previous check
$organization = $this->entityManager->getRepository(Organizations::class)->find($id);
$newUsers = $this->entityManager->getRepository(UsersOrganizations::class)->getLastNewActiveUsersByOrganization($organization);
@ -78,7 +109,21 @@ class OrganizationController extends AbstractController
// get all applications
$applications = $this->organizationsService->getApplicationsWithAccessStatus($organization);
}else{
$actions = $organization->getActions()->toArray();
usort($actions, static function ($a, $b) {
return $b->getDate() <=> $a->getDate();
});
//get the last 10 activities
$actions = array_slice($actions, 0, 10);
$activities = array_map(static function ($activity) use ($actionService) {
return [
'date' => $activity->getDate(), // or however you access the date
'actionType' => $activity->getActionType(),
'users' => $activity->getUsers(),
'color' => $actionService->getActivityColor($activity->getDate())
];
}, $actions);
} else {
throw $this->createNotFoundException(self::ACCESS_DENIED);
}
@ -86,9 +131,52 @@ class OrganizationController extends AbstractController
'organization' => $organization,
'adminUsers' => $adminUsers,
'newUsers' => $newUsers,
'org' => $org[0],
'org' => !empty($org) ? $org[0] : null,
'applications' => $applications,
'activities' => $activities
]);
}
#[Route('/edit/{id}', name: 'edit', requirements: ['id' => '\d+'], methods: ['GET', 'POST'])]
public function edit(Request $request): Response
{
$id = $request->attributes->get('id');
if (!$this->isGranted('ROLE_SUPER_ADMIN')) {
throw $this->createNotFoundException(self::ACCESS_DENIED);
}
$organization = $this->entityManager->getRepository(Organizations::class)->find($id);
if (!$organization) {
throw $this->createNotFoundException(self::NOT_FOUND);
}
$form = $this->createForm(OrganizationForm::class, $organization);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$logoFile = $form->get('logoUrl')->getData();
if ($logoFile) {
$currentDate = (new \DateTime())->format('Y-m-d');
$organizationName = preg_replace('/[^a-zA-Z0-9]/', '_', $organization->getName());
$extension = $logoFile->guessExtension();
$newFilename = $currentDate . '_' . $organizationName . '.' . $extension;
// Move the file to the directory where logos are stored
$logoFile->move(
$this->getParameter('logos_directory'),
$newFilename
);
// Update the 'logoUrl' property to store the file name
$organization->setLogoUrl($newFilename);
}
$this->entityManager->persist($organization);
$this->entityManager->flush();
$this->addFlash('success', 'Organization updated successfully');
return $this->redirectToRoute('organization_index');
}
return $this->render('organization/edit.html.twig', [
'form' => $form->createView(),
'organization' => $organization,
]);
}
}

View File

@ -2,7 +2,9 @@
namespace App\Controller;
use App\Entity\Actions;
use App\Entity\Apps;
use App\Entity\Organizations;
use App\Entity\Roles;
use App\Entity\User;
use App\Form\UserForm;
@ -61,7 +63,7 @@ class UserController extends AbstractController
* GET /user/{id} - Show specific user (show/member)
*/
#[Route('/{id}', name: 'show', requirements: ['id' => '\d+'], methods: ['GET'])]
public function show(int $id, EntityManagerInterface $entityManager): Response
public function show(int $id, EntityManagerInterface $entityManager, Request $request): Response
{
if (!$this->isGranted('ROLE_SUPER_ADMIN')) {
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
@ -71,8 +73,11 @@ class UserController extends AbstractController
if (!$user) {
throw $this->createNotFoundException(self::NOT_FOUND);
}
$userOrganizations = $this->userOrganizationService->getUserOrganizations($user);
if($request->query->has('organizationId')) {
$userOrganizations = $this->userOrganizationService->getUserOrganizations($user, $request->query->get('organizationId'));
}else{
$userOrganizations = $this->userOrganizationService->getUserOrganizations($user);
}
return $this->render('user/show.html.twig', [
'user' => $user,
@ -87,6 +92,7 @@ class UserController extends AbstractController
public function new(Request $request): Response
{
$form = $this->createForm(UserForm::class);
$organizationId = $request->query->get('organizationId');
$form->handleRequest($request);
@ -95,12 +101,38 @@ class UserController extends AbstractController
$data = $form->getData();
// Handle user creation logic here
//FOR DEV PURPOSES ONLY
$data->setPictureUrl("");
$data->setPassword($this->userService->generateRandomPassword());
//FOR DEV PURPOSES ONLY
$orgId = $request->get('organization_id');
if ($orgId) {
$organization = $this->entityManager->getRepository(Organizations::class)->find($orgId);
$roleUser = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'USER']);
if (!$organization || !$roleUser) {
throw $this->createNotFoundException(self::NOT_FOUND);
}
$uo = new UsersOrganizations();
$uo->setOrganization($organization);
$uo->setRole($roleUser);
$uo->setUsers($data);
//log the action
$action = new Actions();
$action->setActionType('Création utilisateur dans une organisation');
$action->setUsers($this->getUser());
$action->setOrganization($organization);
$this->entityManager->persist($uo);
}else{
$action = new Actions();
$action->setActionType('Création utilisateur');
$action->setUsers($this->getUser());
}
$this->entityManager->persist($data);
$this->entityManager->persist($action);
$this->entityManager->flush();
// Redirect to user index
@ -109,13 +141,14 @@ class UserController extends AbstractController
return $this->render('user/new.html.twig', [
'form' => $form->createView(),
'organizationId' => $organizationId,
]);
}
/**
* GET /user/{id}/edit - Show form to edit user
*/
#[Route('/{id}/edit', name: 'edit', requirements: ['id' => '\d+'], methods: ['GET', 'PUT', 'POST'])]
#[Route('/edit/{id}', name: 'edit', requirements: ['id' => '\d+'], methods: ['GET', 'PUT', 'POST'])]
public function edit(int $id, EntityManagerInterface $entityManager, Request $request): Response
{
//Handle access control
@ -137,6 +170,11 @@ class UserController extends AbstractController
if ($form->isSubmitted() && $form->isValid()) {
//Persist changes to the user entity
$entityManager->persist($user);
//Log the action
$action = new Actions();
$action->setActionType('Modification utilisateur');
$action->setUsers($this->getUser());
$entityManager->persist($action);
$entityManager->flush();
//Redirect to user profile after successful edit
@ -171,6 +209,11 @@ class UserController extends AbstractController
// Handle user deletion logic
$user->setIsDeleted(true);
$entityManager->persist($user);
// Log the action
$action = new Actions();
$action->setActionType('Suppression utilisateur');
$action->setUsers($this->getUser());
$entityManager->persist($action);
$entityManager->flush();
return $this->redirectToRoute('user_index');
@ -193,6 +236,11 @@ class UserController extends AbstractController
// Handle user deletion logic
$entityManager->remove($user);
// Log the action
$action = new Actions();
$action->setActionType('Suppression définitive utilisateur');
$action->setUsers($this->getUser());
$entityManager->persist($action);
$entityManager->flush();
return $this->redirectToRoute('user_index');
@ -215,6 +263,11 @@ class UserController extends AbstractController
}
$user->setIsActive(false);
$entityManager->persist($user);
// Log the action
$action = new Actions();
$action->setActionType('Désactivation utilisateur');
$action->setUsers($this->getUser());
$entityManager->persist($action);
$entityManager->flush();
return $this->redirectToRoute('user_index');
}
@ -260,7 +313,7 @@ class UserController extends AbstractController
// Fetch all roles and apps
$roles = $entityManager->getRepository(Roles::class)->findAll();
$apps = $entityManager->getRepository(Apps::class)->findAll();
$apps = $organization->getApps() ?? throw $this->createNotFoundException(self::NOT_FOUND);
if (!$roles) {
throw $this->createNotFoundException(self::NOT_FOUND);
}

View File

@ -13,7 +13,7 @@ class Actions
#[ORM\Column]
private ?int $id = null;
#[ORM\OneToOne(cascade: ['persist', 'remove'])]
#[ORM\ManyToOne(cascade: ['persist', 'remove'])]
private ?user $users = null;
#[ORM\Column(length: 255)]
@ -22,6 +22,17 @@ class Actions
#[ORM\Column(options: ['default' => 'CURRENT_TIMESTAMP'])]
private ?\DateTimeImmutable $date = null;
#[ORM\ManyToOne(inversedBy: 'actions')]
private ?Organizations $Organization = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $description = null;
public function __construct()
{
$this->date = new \DateTimeImmutable();
}
public function getId(): ?int
{
return $this->id;
@ -62,4 +73,28 @@ class Actions
return $this;
}
public function getOrganization(): ?Organizations
{
return $this->Organization;
}
public function setOrganization(?Organizations $Organization): static
{
$this->Organization = $Organization;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): static
{
$this->description = $description;
return $this;
}
}

View File

@ -39,6 +39,9 @@ class Apps
#[ORM\ManyToMany(targetEntity: organizations::class, inversedBy: 'apps')]
private Collection $organization;
#[ORM\Column(length: 255, nullable: true)]
private ?string $descriptionSmall = null;
public function __construct()
{
$this->organization = new ArrayCollection();
@ -144,4 +147,16 @@ class Apps
return $this;
}
public function getDescriptionSmall(): ?string
{
return $this->descriptionSmall;
}
public function setDescriptionSmall(?string $descriptionSmall): static
{
$this->descriptionSmall = $descriptionSmall;
return $this;
}
}

View File

@ -31,10 +31,10 @@ class Organizations
private ?\DateTimeImmutable $createdAt = null;
#[ORM\Column(options: ['default' => false])]
private ?bool $isDeleted = null;
private ?bool $isDeleted = false;
#[ORM\Column(options: ['default' => true])]
private ?bool $isActive = null;
private ?bool $isActive = true;
/**
* @var Collection<int, Apps>
@ -45,9 +45,17 @@ class Organizations
#[ORM\Column(length: 255, nullable: true)]
private ?string $name = null;
/**
* @var Collection<int, Actions>
*/
#[ORM\OneToMany(targetEntity: Actions::class, mappedBy: 'Organization')]
private Collection $actions;
public function __construct()
{
$this->apps = new ArrayCollection();
$this->actions = new ArrayCollection();
$this->createdAt = new \DateTimeImmutable();
}
public function getId(): ?int
@ -177,4 +185,34 @@ class Organizations
return $this;
}
/**
* @return Collection<int, Actions>
*/
public function getActions(): Collection
{
return $this->actions;
}
public function addAction(Actions $action): static
{
if (!$this->actions->contains($action)) {
$this->actions->add($action);
$action->setOrganization($this);
}
return $this;
}
public function removeAction(Actions $action): static
{
if ($this->actions->removeElement($action)) {
// set the owning side to null (unless already changed)
if ($action->getOrganization() === $this) {
$action->setOrganization(null);
}
}
return $this;
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Form;
use App\Entity\Organizations;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class OrganizationForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$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('logoUrl', FileType::class, [
'required' => false,
'label' => 'Logo',
'mapped' => false, // Important if the entity property is not directly mapped
'attr' => ['accept' => 'image/*'],
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Organizations::class,
]);
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Service;
class ActionService
{
public function getActivityColor(\DateTimeImmutable $activityTime): string
{
$now = new \DateTimeImmutable();
$diffInSeconds = $now->getTimestamp() - $activityTime->getTimestamp();
if ($diffInSeconds < 15 * 60) { // less than 15 minutes
return '#086572';
}
if ($diffInSeconds < 60 * 60) { // less than 1 hour
return '#247208';
}
return '#C76633';
}
}

206
src/Service/AwsService.php Normal file
View File

@ -0,0 +1,206 @@
<?php
namespace App\Service;
use Aws\S3\S3Client;
class AwsService
{
public function __construct(
private S3Client $s3Client,
private string $awsPublicUrl
) {
}
/**
* Function to generate UUID Version 4
*
* @return string
*/
public function generateUUIDv4():string {
$uuid = uuid_create(4);
$isValid = uuid_is_valid($uuid);
if( $isValid == true){
$retour = $uuid;
}else{
$retour = 'une erreur est survenue !';
}
return $retour;
}
/**
* Get public url for files download or visualisation
*
* @param string $bucket nom du conteneur S3
* @return string
*/
public function getPublicUrl(string $bucket): string{
$publicUrl = substr_replace($this->awsPublicUrl, $bucket.'.', 8, 0);
$publicUrl .= '/';
return $publicUrl;
}
/**
* CREATE bucket S3 for new project
*
* @return string|array
*/
public function createBucket(): string|array{
$bucket = $this->generateUUIDv4();
$result = $this->s3Client->createBucket([
'Bucket' => $bucket,
'ObjectOwnership' => 'BucketOwnerPreferred'
]);
if ( $result['@metadata']['statusCode'] == 200){
return $bucket;
}else{
return $result['@metadata'];
}
}
/**
* DELETE bucket S3
*
* @param string $bucket nom du conteneur S3
* @return string|array
*/
public function DeleteBucket(string $bucket): string|array{
$result = $this->s3Client->deleteBucket([
'Bucket' => $bucket,
]);
if ( $result['@metadata']['statusCode'] == 200){
return $bucket;
}else{
return $result['@metadata'];
}
}
/**
* Get list files infos in the Bucket S3
* If prefix NULL get ALL FILES else get FILES in this prefix
*
* @param string $bucket nom du conteneur S3
* @param string|null $prefix arborescence dans le bucket
* @return array|null
*/
public function getListObject(string $bucket, string|null $prefix = null):array|null{
$results = $this->s3Client->listObjectsV2([
'Bucket' => $bucket,
'Prefix' => $prefix
]);
if( isset($results['Contents']) ){
$return = $results['Contents'];
}
return $return;
}
/**
* PUT file Object in bucket S3
*
* @param string $bucket nom du conteneur S3
* @param object $file fichier à déposer dans le bucket
* @param string $filename nom du fichier enregistré dans la bdd métier
* @param string $mimeType type du fichier
* @param string|null $prefix arborescence dans le bucket
* @return bool
*/
public function PutDocObj(string $bucket, object $file, string $filename, $mimeType, string|null $prefix = null): int{
$body = fopen( $file, 'r');
$hashRaw = hash_file('sha256', $file, true);
$hash = base64_encode($hashRaw);
rewind($body);
$doc = $this->s3Client->putObject([
'Bucket' => $bucket,
'ChecksumAlgorithm' => 'SHA256',
'ChecksumSHA256' => $hash,
'Key' => $prefix.$filename,
'Body' => $body,
'ACL' => 'public-read',
'ContentType' => $mimeType // pour rendre l'image publique si besoin
]);
return $doc['@metadata']['statusCode'];
}
/**
* DELETE file Object in bucket S3
*
* @param string $bucket nom du conteneur S3
* @param string $filename nom du fichier
* @param string|null $prefix arborescence dans le bucket
* @return bool
*/
public function DeleteDocObj(string $bucket, string $filename, string|null $prefix = null): int{
$doc = $this->s3Client->deleteObject([
'Bucket' => $bucket,
'Key' => $prefix.$filename,
]);
return $doc['@metadata']['statusCode'];
}
/**
* RENAME file Object in bucket S3
*
* @param string $bucket nom du conteneur S3
* @param string $filename nom du fichier
* @param string $newFilename
* @param string|null $prefix arborescence dans le bucket
* @return bool
*/
public function renameDocObj(string $bucket, string $filename, string $newFilename, string|null $prefix = null): int{
$doc = $this->s3Client->copyObject([
'Bucket' => $bucket,
'CopySource' => $prefix.$filename,
'Key' => $prefix.$newFilename,
]);
$this->DeleteDocObj($bucket, $filename, $prefix);
return $doc['@metadata']['statusCode'];
}
/**
* MOVE file Object in bucket S3
*
* @param string $bucket nom du conteneur S3
* @param string $filename nom du fichier
* @param string|null $prefix arborescence dans le bucket
* @param string|null $newPrefix nouvel emplacement dans le bucket
* @return bool
*/
public function moveDocObj(string $bucket, string $filename, string|null $prefix = null, string|null $newPrefix = null): int{
$doc = $this->s3Client->copyObject([
'Bucket' => $bucket,
'CopySource' => $prefix.$filename,
'Key' => $newPrefix.$filename,
]);
$this->DeleteDocObj($bucket, $filename, $prefix);
return $doc['@metadata']['statusCode'];
}
}

View File

@ -2,6 +2,7 @@
namespace App\Service;
use App\Entity\Actions;
use App\Entity\Apps;
use App\Entity\Organizations;
use App\Entity\Roles;
@ -333,6 +334,12 @@ readonly class UserOrganizationService
]);
foreach ($userOrganizations as $uo) {
$uo->setIsActive(false);
//Log action
$action = new Actions();
$action->setActionType("Désactivation role" );
$action->setDescription("Désactivation du rôle " . $uo->getRole()->getName() . " pour l'utilisateur " . $user->getUserIdentifier() . " dans l'organisation " . $organization->getName());
$action->setOrganization($organization);
$action->setUsers($user);
$this->entityManager->persist($uo);
}
$this->entityManager->flush();
@ -401,12 +408,12 @@ readonly class UserOrganizationService
*/
public function findActiveUsersByOrganizations(array $organizations): array
{
if (empty($organizations)) {
return [];
}
$userOrgs = $this->entityManager->getRepository(UsersOrganizations::class)->getAllActiveUserOrganizationLinks($organizations);
$usersByOrg = [];
foreach ($userOrgs as $uo) {
$org = $uo->getOrganization();

View File

@ -1,4 +1,16 @@
{
"aws/aws-sdk-php-symfony": {
"version": "2.8",
"recipe": {
"repo": "github.com/symfony/recipes-contrib",
"branch": "main",
"version": "1.3",
"ref": "d1753f9e2a669c464b2b0618af9b0123426b67b4"
},
"files": [
"config/packages/aws.yaml"
]
},
"doctrine/deprecations": {
"version": "1.1",
"recipe": {
@ -35,6 +47,9 @@
"migrations/.gitignore"
]
},
"knplabs/knp-time-bundle": {
"version": "v2.4.0"
},
"league/oauth2-server-bundle": {
"version": "0.11",
"recipe": {
@ -381,8 +396,5 @@
"files": [
"config/packages/messenger.yaml"
]
},
"twig/extra-bundle": {
"version": "v3.20.0"
}
}

View File

@ -0,0 +1,25 @@
{% block body %}
<div class="card">
<div class="card-header">
<div class="card-title">
<h3><img width=10% src="{{ asset(application.application.logoUrl) }}" alt="Logo application">
{{ application.application.name }}</h3>
</div>
</div>
<div class="card-body d-flex flex-column align-items-center">
<p class="card-text">{{ application.application.descriptionSmall }}</p>
{% if application.has_access %}
<div >
<a href="http://{{ application.application.subDomain }}.solutions-easy.moi" class="btn btn-primary me-2">Y
accéder</a>
<a href="#" class="btn btn-secondary">Gérer l'application</a>
</div>
{% else %}<a href="#" class="btn btn-primary">Demander l'accès</a>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -2,36 +2,24 @@
<div class="card border-0">
<div class="card-header d-flex justify-content-between align-items-center">
<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 %}#}
{% 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">
{# {% for activity in activities %}#}
<li class="list-group-item">
<p> 5 mins ago</p>
</li>
<li class="list-group-item">
<p> 5 mins ago</p>
</li>
<li class="list-group-item">
<p> 5 mins ago</p>
</li>
<li class="list-group-item">
<p> 5 mins ago</p>
</li>
<li class="list-group-item">
<p> 5 mins ago</p>
</li>
<li class="list-group-item">
<p> 5 mins ago</p>
</li>
{# {% endfor %}#}
{% 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 %}#}
{% endif %}
</div>
{% endblock %}

View File

@ -0,0 +1,21 @@
{% extends 'base.html.twig' %}
{% block body %}
<div class=" col-md-10 m-auto p-5">
<div class="card">
<div class="card-title shadow-sm p-3 d-flex justify-content-between align-items-center">
<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">
{{ form_start(form, {'action': path('organization_edit', {'id': organization.id}), 'method': 'PUT'}) }}
{{ form_widget(form) }}
<button type="submit" class="btn btn-primary">Enregistrer</button>
{{ form_end(form) }}
</div>
</div>
</div>
{% endblock %}

View File

@ -6,11 +6,16 @@
<div class="w-100 h-100 p-5 m-auto" data-controller="organization">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>Gestion des organisations</h1>
{# <a href="{{ path('organization_new') }}" class="btn btn-primary">Ajouter une organisation</a>#}
{% if is_granted("ROLE_SUPER_ADMIN") %}
<a href="{{ path('organization_new') }}" class="btn btn-primary">Ajouter une organisation</a>
{% endif %}
</div>
{% if organizations|length == 0 %}
<tr>
<td colspan="4" class="text-center">Aucune organisation trouvée.</td>
<td colspan="4" class="text-center">
<a href="{{ path('organization_new') }}" class="btn btn-primary">Créer une organisation</a>
</td>
</tr>
{% else %}
<table class="table align-middle shadow">
@ -27,7 +32,7 @@
<tr>
<td>
{% if organization.logoUrl %}
<img src="{{ asset(organization.logoUrl) }}" alt="Organization logo" class="rounded-circle" style="width:40px; height:40px;">
<img src="{{ asset('uploads/logos/' ~ organization.logoUrl) }}" alt="Organization logo" class="rounded-circle" style="width:40px; height:40px;">
{% endif %}
</td>
<td>{{ organization.name }}</td>

View File

@ -0,0 +1,21 @@
{% extends 'base.html.twig' %}
{% block title %}Ajouter une organisation{% endblock %}
{% block body %}
<div class=" col-md-10 m-auto p-5">
<div class="card">
<div class="card-title shadow-sm p-3 d-flex justify-content-between align-items-center">
<h1>Ajouter une organisation</h1>
</div>
<div class="card-body">
<form method="post" action="{{ path('organization_new') }}" enctype="multipart/form-data">
{{ form_start(form) }}
{{ form_widget(form) }}
<button type="submit" class="btn btn-primary">Enregistrer</button>
{{ form_end(form) }}
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -3,12 +3,18 @@
{% block body %}
<div class="col-md-12 m-auto p-5">
<div class="col d-flex justify-content-between align-items-center ">
<h1 class="mb-4">{{ organization.name|title }} - Dashboard</h1>
{% if is_granted("ROLE_SUER_ADMIN") %}
{# <a href="{{ path('user_deactivate', {'id': user.id}) }}" class="btn btn-danger">Désactiver</a> #}
<h1 class="mb-4">
{% if organization.logoUrl %}
<img src="{{ asset('uploads/logos/' ~ organization.logoUrl) }}" alt="Organization logo"
class="rounded-circle" style="width:40px; height:40px;">
{% endif %}
{{ organization.name|title }} - Dashboard</h1>
{% if is_granted("ROLE_SUPER_ADMIN") %}
<a href="{{ path('organization_edit', {'id': organization.id}) }}" class="btn btn-primary">Gérer mon
organisation</a>
{% endif %}
</div>
{# USER ROW#}
{# USER ROW #}
<div class="row">
<div class="col-9">
<div class="row mb-4">
@ -16,7 +22,8 @@
{% include 'user/userListSmall.html.twig' with {
title: 'Nouveaux utilisateurs',
users: newUsers,
empty_message: 'Aucun nouvel utilisateur trouvé.'
empty_message: 'Aucun nouveaux utilisateurs trouvé.',
organizationId: organization.id
} %}
</div>
<div class="col mb-3 mb-sm-0">
@ -30,8 +37,20 @@
<div class="m-auto">
{% include 'user/userList.html.twig' with {
title: 'Mes utilisateurs',
organizationId: organization.id,
empty_message: 'Aucun utilisateurs trouvé.'
} %}
</div>
{# APPLICATION ROW #}
<div class="row ">
{% for application in applications %}
<div class="col-6 mb-3">
{% include 'applications/appSmall.html.twig' with {
application: application
} %}
</div>
{% endfor %}
</div>
</div>
<div class="col-3 m-auto">
@ -43,11 +62,9 @@
</div>
{# APPLICATION ROW#}
<div class="row">
</div>
</div>
{% endblock %}

View File

@ -12,6 +12,11 @@
<form method="post" action="{{ path('user_new') }}" enctype="multipart/form-data">
{{ form_start(form) }}
{{ form_widget(form) }}
{% if organizationId is defined %}
<div class="form-group">
<input hidden type="text" value="{{ organizationId }}" name="organization_id">
</div>
{% endif %}
<button type="submit" class="btn btn-primary">Enregistrer</button>
{{ form_end(form) }}
</form>

View File

@ -0,0 +1,17 @@
{% 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 %}

View File

@ -22,38 +22,50 @@
</tr>
</thead>
<tbody>
{% if org.users|length == 0 %}
{% if org|length == 0 %}
<tr>
<td colspan="6" class="text-center">Aucun utilisateur trouvé.</td>
</tr>
{% elseif org.users|length == 0 %}
<tr>
<td colspan="6" class="text-center">Aucun utilisateur trouvé.</td>
</tr>
{% else %}
{% for user in org.users %}
<tr>
<td>
{% if user.users.pictureUrl %}
<img src="{{ asset(user.users.pictureUrl) }}" alt="User profile pic"
class="rounded-circle"
style="width:40px; height:40px;">
{% endif %}
</td>
<td>{{ user.users.surname }}</td>
<td>{{ user.users.name }}</td>
<td>{{ user.users.email }}</td>
<td>
{% if user.is_connected %}
<span class="badge bg-success">Actif</span>
{% else %}
<span class="badge bg-secondary">Inactif</span>
{% endif %}
</td>
<td>
{% if organizationId is defined %}
<a href="{{ path('user_show', {'id': user.users.id, 'organizationId': organizationId}) }}"
class="p-3 align-middle color-primary">
{{ ux_icon('fa6-regular:eye', {height: '30px', width: '30px'}) }}
</a>
{% else %}
<a href="{{ path('user_show', {'id': user.users.id}) }}"
class="p-3 align-middle color-primary">
{{ ux_icon('fa6-regular:eye', {height: '30px', width: '30px'}) }}
</a>
{% endif %}
</td>
</tr>
{% endfor %}
{% endif %}
{% for user in org.users %}
<tr>
<td>
{% if user.users.pictureUrl %}
<img src="{{ asset(user.users.pictureUrl) }}" alt="User profile pic"
class="rounded-circle"
style="width:40px; height:40px;">
{% endif %}
</td>
<td>{{ user.users.surname }}</td>
<td>{{ user.users.name }}</td>
<td>{{ user.users.email }}</td>
<td>
{% if user.is_connected %}
<span class="badge bg-success">Actif</span>
{% else %}
<span class="badge bg-secondary">Inactif</span>
{% endif %}
</td>
<td>
<a href="{{ path('user_show', {'id': user.users.id}) }}" class="p-3 align-middle">
<i class="icon-grid menu-icon color-primary">
{{ ux_icon('fa6-regular:eye', {height: '30px', width: '30px'}) }}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@ -3,6 +3,9 @@
<div class="card border-0">
<div class="card-title p-3 d-flex justify-content-between align-items-center ">
<h3>{{ title }}</h3>
{% if organizationId is defined %}
<a href="{{ path('user_new', {'organizationId': organizationId}) }}" class="btn btn-primary">Ajouter un utilisateur</a>
{% endif %}
</div>
<div class="card-body">
<table class="table align-middle table-borderless">
@ -29,10 +32,18 @@
</td>
<td>{{ user.email }}</td>
<td>
<a href="{{ path('user_show', {'id': user.id}) }}"
{% if organizationId is defined %}
<a href="{{ path('user_show', {'id': user.id, 'organizationId': organizationId}) }}"
class="p-3 align-middle color-primary">
{{ ux_icon('fa6-regular:eye', {height: '30px', width: '30px'}) }}
</a>
{% else %}
<a href="{{ path('user_show', {'id': user.id}) }}"
class="p-3 align-middle color-primary">
{{ ux_icon('fa6-regular:eye', {height: '30px', width: '30px'}) }}
</a>
{% endif %}
</td>
</tr>
{% endfor %}