Compare commits

...

67 Commits

Author SHA1 Message Date
Charles-Edouard 993188ac4f Delete public/uploads/logos/6893102127c028.11727111.jpg 2025-08-06 12:22:00 +02:00
Charles-Edouard 18dc5f8492 Delete public/uploads/logos/2025-08-06_fsadf_logofile.jpg 2025-08-06 12:21:53 +02:00
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
Charles 2e99457e16 get applications access per organization 2025-07-29 16:33:16 +02:00
Charles cde6c529a9 update access logic 2025-07-29 16:07:51 +02:00
Charles a3f993b858 organizations user information 2025-07-29 15:55:55 +02:00
Charles d2c20b9423 access token on log in 2025-07-29 14:38:19 +02:00
Charles 89ed7049b9 correction 2025-07-29 14:37:55 +02:00
Charles 16dd919a5d activity template 2025-07-28 16:23:47 +02:00
Charles 301f7bb445 statut template 2025-07-28 16:05:20 +02:00
Charles 05d8ca0499 organization dashboard User management 2025-07-28 15:19:59 +02:00
Charles e17e8e0eb2 refactor 2025-07-28 12:13:04 +02:00
Charles e6391279fe refactor 2025-07-28 11:45:59 +02:00
Charles cf16ec09a1 refactor 2025-07-28 11:40:45 +02:00
Charles 943752a002 Display all organizations 2025-07-28 11:38:16 +02:00
Charles a7e7298310 refactor 2025-07-28 11:27:18 +02:00
Charles 3337b8c001 refactor 2025-07-28 11:22:57 +02:00
Charles 6446eb2ce1 Handle permission 2025-07-28 11:20:31 +02:00
Charles a10b499522 refactor 2025-07-28 11:02:26 +02:00
Charles e77e92d39f refactor 2025-07-28 11:02:19 +02:00
Charles 00e3003257 changed user index logic 2025-07-28 10:26:13 +02:00
Charles f1b953d005 helper function 2025-07-28 10:23:07 +02:00
Charles 4aadaa351a Comments 2025-07-28 09:53:26 +02:00
Charles fcb69f987f refactor 2025-07-25 14:32:02 +02:00
Charles d7677db885 refactor 2025-07-25 12:03:54 +02:00
Charles 8eb5cf433d remove app from user organization 2025-07-25 11:50:06 +02:00
Charles fed351b433 remove user from organization 2025-07-25 11:03:21 +02:00
Charles 0a602fb52e Deactivate roles 2025-07-25 10:54:01 +02:00
Charles e87fdd32e4 refactor + default value 2025-07-25 10:38:03 +02:00
Charles cfe89f58db edit user roles and app per organization 2025-07-25 10:30:00 +02:00
Charles 1d2debf364 Roles adjustment 2025-07-17 15:55:09 +02:00
Charles 3271da59fa delete and set delete user 2025-07-17 14:10:55 +02:00
Charles d8df0bc1f4 front modifications 2025-07-17 14:10:20 +02:00
Charles c3d3218bff update userForm 2025-07-17 11:45:02 +02:00
Charles f24fb0180d Edit user 2025-07-17 11:33:34 +02:00
Charles e360019e58 Type refacto 2025-07-17 11:32:28 +02:00
Charles 8e38fe47db Type refacto 2025-07-17 11:26:05 +02:00
Charles b54fe41795 const execption 2025-07-17 10:39:32 +02:00
Charles 798ca4ba07 unique email address on creation 2025-07-17 10:39:17 +02:00
Charles d43b516826 Create user 2025-07-17 09:16:09 +02:00
Charles c99b575814 Deactivate user 2025-07-16 15:59:56 +02:00
Charles 65ff838dd9 handle error 2025-07-16 15:50:45 +02:00
Charles 4a2f9d9547 Read User information 2025-07-16 15:12:45 +02:00
Charles 10a8eb2255 Correction 2025-07-16 15:04:35 +02:00
Charles 8f232498b8 Add phone number 2025-07-16 15:03:41 +02:00
Charles bbe50dbfd9 Last connection on user entity 2025-07-10 09:26:54 +02:00
Charles 436284c9c7 add name to Organizations entity 2025-07-09 14:11:31 +02:00
Charles 228ef8cbe9 refactor + update DB 2025-07-09 09:30:34 +02:00
Charles c81142e4a5 refactor 2025-07-09 09:21:47 +02:00
Charles 6a9b7568af refactor 2025-07-09 09:16:05 +02:00
Charles 57e115a6a8 update to Template 2025-07-09 09:14:29 +02:00
Charles 79c5596766 refactor 2025-07-04 15:55:35 +02:00
113 changed files with 5392 additions and 805 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 ###

5
.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 ###
@ -23,3 +24,7 @@
/public/assets/
/assets/vendor/
###< symfony/asset-mapper ###
###> IntelliJ IDEA ###
.idea/
*.iml

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" />
@ -141,7 +140,6 @@
<path value="$PROJECT_DIR$/vendor/defuse/php-encryption" />
<path value="$PROJECT_DIR$/vendor/symfony/ux-turbo" />
<path value="$PROJECT_DIR$/vendor/symfony/stimulus-bundle" />
<path value="$PROJECT_DIR$/vendor/doctrine/cache" />
<path value="$PROJECT_DIR$/vendor/symfony/type-info" />
<path value="$PROJECT_DIR$/vendor/doctrine/event-manager" />
<path value="$PROJECT_DIR$/vendor/symfony/console" />
@ -164,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,18 +6,29 @@
- 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
php bin/console importmap:require choices.js/public/assets/styles/choices.min.css
```
### Notes
- certaines abbreviations sont utilisées afin de simplifier le code et d'éviter les répétitions ou noms trop longs :
- `uo` pour `User Organization`
- `uoId` pour `User Organization Id`
- `oa` pour `Organization Application`
- `at` pour `Access Token`

View File

@ -8,7 +8,12 @@ import './bootstrap.js';
import 'bootstrap/dist/css/bootstrap.min.css';
import './styles/app.css';
import './styles/navbar.css';
import './styles/sidebar.css';
import 'bootstrap';
console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉');
import './js/template.js';
import './js/off_canvas.js';
import './js/hoverable-collapse.js';
import './js/cookies.js';
import 'choices.js';
import 'choices.js/public/assets/styles/choices.min.css';

View File

@ -0,0 +1,77 @@
import {Controller} from '@hotwired/stimulus';
import Choices from 'choices.js';
/*
* The following line makes this controller "lazy": it won't be downloaded until needed
* See https://symfony.com/bundles/StimulusBundle/current/index.html#lazy-stimulus-controllers
*/
/* stimulusFetch: 'lazy' */
export default class extends Controller {
static values = {
rolesArray: Array,
selectedRoleIds: Array,
applicationsArray: Array,
selectedApplicationIds: Array
}
// {value: 'choice1', label: 'Choice 1'},
// {value: 'choice2', label: 'Choice 2'},
// {value: 'choice3', label: 'Choice 3'},
roleSelect() {
const element = document.getElementById('roles');
if (element) {
const choicesData = this.rolesArrayValue.map(role => ({
value: role.id,
label: role.name,
selected: this.selectedRoleIdsValue.includes(role.id)
}));
const choices = new Choices(element, {
choices: choicesData,
removeItemButton: true,
placeholder: true,
placeholderValue: 'Ajouter un ou plusieurs rôles'
});
}
}
appSelect() {
const element = document.getElementById('applications');
if (element) {
const choicesData = this.applicationsArrayValue.map(app => ({
value: app.id,
label: app.name,
customProperties: {icon: app.icon},
selected: this.selectedApplicationIdsValue.includes(app.id)
}));
const choices = new Choices(element, {
choices: choicesData,
removeItemButton: true,
placeholder: true,
placeholderValue: 'Ajouter une ou plusieurs applications',
});
}
}
connect() {
this.roleSelect();
this.appSelect();
// Set choices after initialization
// choices.setValue(choicesData);
}
// Add custom controller actions here
// fooBar() { this.fooTarget.classList.toggle(this.bazClass) }
disconnect() {
// Called anytime its element is disconnected from the DOM
// (on page change, when it's removed from or moved in the DOM, etc.)
// Here you should remove all event listeners added in "connect()"
// this.fooTarget.removeEventListener('click', this._fooBar)
}
}

View File

@ -0,0 +1 @@
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 16 16"><g fill="currentColor"><path d="M4 2.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5zm3 0a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5zm3.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zM4 5.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5zM7.5 5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zm2.5.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5zM4.5 8a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zm2.5.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5zm3.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5z"/><path d="M2 1a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1zm11 0H3v14h3v-2.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5V15h3z"/></g></svg>

After

Width:  |  Height:  |  Size: 965 B

View File

@ -0,0 +1 @@
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 16 16"><g fill="currentColor"><path d="M14.763.075A.5.5 0 0 1 15 .5v15a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5V14h-1v1.5a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5V10a.5.5 0 0 1 .342-.474L6 7.64V4.5a.5.5 0 0 1 .276-.447l8-4a.5.5 0 0 1 .487.022M6 8.694L1 10.36V15h5zM7 15h2v-1.5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5V15h2V1.309l-7 3.5z"/><path d="M2 11h1v1H2zm2 0h1v1H4zm-2 2h1v1H2zm2 0h1v1H4zm4-4h1v1H8zm2 0h1v1h-1zm-2 2h1v1H8zm2 0h1v1h-1zm2-2h1v1h-1zm0 2h1v1h-1zM8 7h1v1H8zm2 0h1v1h-1zm2 0h1v1h-1zM8 5h1v1H8zm2 0h1v1h-1zm2 0h1v1h-1zm0-2h1v1h-1z"/></g></svg>

After

Width:  |  Height:  |  Size: 598 B

View File

@ -0,0 +1 @@
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708"/></svg>

After

Width:  |  Height:  |  Size: 236 B

View File

@ -0,0 +1 @@
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8L4.646 2.354a.5.5 0 0 1 0-.708"/></svg>

After

Width:  |  Height:  |  Size: 236 B

View File

@ -0,0 +1 @@
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M256 464a208 208 0 1 1 0-416a208 208 0 1 1 0 416m0-464a256 256 0 1 0 0 512a256 256 0 1 0 0-512m120.9 294.6c4.5-4.2 7.1-10.1 7.1-16.3c0-12.3-10-22.3-22.3-22.3H304v-96c0-17.7-14.3-32-32-32h-32c-17.7 0-32 14.3-32 32v96h-57.7c-12.3 0-22.3 10-22.3 22.3c0 6.2 2.6 12.1 7.1 16.3l107.1 99.9c3.8 3.5 8.7 5.5 13.8 5.5s10.1-2 13.8-5.5z"/></svg>

After

Width:  |  Height:  |  Size: 425 B

View File

@ -0,0 +1 @@
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M256 48a208 208 0 1 1 0 416a208 208 0 1 1 0-416m0 464a256 256 0 1 0 0-512a256 256 0 1 0 0 512M151.2 217.4c-4.6 4.2-7.2 10.1-7.2 16.4c0 12.3 10 22.3 22.3 22.3H208v96c0 17.7 14.3 32 32 32h32c17.7 0 32-14.3 32-32v-96h41.7c12.3 0 22.3-10 22.3-22.3c0-6.2-2.6-12.1-7.2-16.4l-91-84c-3.8-3.5-8.7-5.4-13.9-5.4s-10.1 1.9-13.9 5.4l-91 84z"/></svg>

After

Width:  |  Height:  |  Size: 428 B

View File

@ -0,0 +1 @@
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M406.5 399.6c-19.1-46.7-65-79.6-118.5-79.6h-64c-53.5 0-99.4 32.9-118.5 79.6C69.9 362.2 48 311.7 48 256c0-114.9 93.1-208 208-208s208 93.1 208 208c0 55.7-21.9 106.2-57.5 143.6m-40.1 32.7c-32 20.1-69.8 31.7-110.4 31.7s-78.4-11.6-110.5-31.7c7.3-36.7 39.7-64.3 78.5-64.3h64c38.8 0 71.2 27.6 78.5 64.3zM256 512a256 256 0 1 0 0-512a256 256 0 1 0 0 512m0-272a40 40 0 1 1 0-80a40 40 0 1 1 0 80m-88-40a88 88 0 1 0 176 0a88 88 0 1 0-176 0"/></svg>

After

Width:  |  Height:  |  Size: 528 B

View File

@ -0,0 +1 @@
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="currentColor" d="M288 80c-65.2 0-118.8 29.6-159.9 67.7C89.6 183.5 63 226 49.4 256C63 286 89.6 328.5 128 364.3c41.2 38.1 94.8 67.7 160 67.7s118.8-29.6 159.9-67.7C486.4 328.5 513 286 526.6 256c-13.6-30-40.2-72.5-78.6-108.3C406.8 109.6 353.2 80 288 80M95.4 112.6C142.5 68.8 207.2 32 288 32s145.5 36.8 192.6 80.6c46.8 43.5 78.1 95.4 93 131.1c3.3 7.9 3.3 16.7 0 24.6c-14.9 35.7-46.2 87.7-93 131.1C433.5 443.2 368.8 480 288 480s-145.5-36.8-192.6-80.6C48.6 356 17.3 304 2.5 268.3c-3.3-7.9-3.3-16.7 0-24.6C17.3 208 48.6 156 95.4 112.6M288 336c44.2 0 80-35.8 80-80s-35.8-80-80-80h-2c1.3 5.1 2 10.5 2 16c0 35.3-28.7 64-64 64c-5.5 0-10.9-.7-16-2v2c0 44.2 35.8 80 80 80m0-208a128 128 0 1 1 0 256a128 128 0 1 1 0-256"/></svg>

After

Width:  |  Height:  |  Size: 787 B

View File

@ -0,0 +1 @@
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" fill-rule="evenodd" d="m243.07 65.728l34.263 14.684v46.42l-41.306-17.703l-107.695 61.54l116.919 66.811L256 243.623v146.285l106.667-60.952v-94.288h42.666v119.048l-10.749 6.143l-149.333 85.333l-10.584 6.048l-10.585-6.048l-149.333-85.333L64 353.716V158.289l10.749-6.142l149.333-85.333l9.224-5.271zm-29.737 324.18V268.383l-106.666-60.952v121.525zm106.666-283.24h55.163l-91.581 91.582l30.17 30.17l91.581-91.582v55.163h42.667v-128h-128z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 548 B

View File

@ -0,0 +1 @@
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M9 17.192V6.808L17.154 12zm1-1.842L15.289 12L10 8.65z"/></svg>

After

Width:  |  Height:  |  Size: 152 B

View File

@ -0,0 +1 @@
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M10 16h4q.425 0 .713-.288T15 15v-2h-2v1h-2v-4h2v1h2V9q0-.425-.288-.712T14 8h-4q-.425 0-.712.288T9 9v6q0 .425.288.713T10 16m2 6q-2.075 0-3.9-.788t-3.175-2.137T2.788 15.9T2 12t.788-3.9t2.137-3.175T8.1 2.788T12 2t3.9.788t3.175 2.137T21.213 8.1T22 12t-.788 3.9t-2.137 3.175t-3.175 2.138T12 22m0-2q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8"/></svg>

After

Width:  |  Height:  |  Size: 473 B

View File

@ -0,0 +1 @@
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M13 8V4q0-.425.288-.712T14 3h6q.425 0 .713.288T21 4v4q0 .425-.288.713T20 9h-6q-.425 0-.712-.288T13 8M3 12V4q0-.425.288-.712T4 3h6q.425 0 .713.288T11 4v8q0 .425-.288.713T10 13H4q-.425 0-.712-.288T3 12m10 8v-8q0-.425.288-.712T14 11h6q.425 0 .713.288T21 12v8q0 .425-.288.713T20 21h-6q-.425 0-.712-.288T13 20M3 20v-4q0-.425.288-.712T4 15h6q.425 0 .713.288T11 16v4q0 .425-.288.713T10 21H4q-.425 0-.712-.288T3 20m2-9h4V5H5zm10 8h4v-6h-4zm0-12h4V5h-4zM5 19h4v-2H5zm4-2"/></svg>

After

Width:  |  Height:  |  Size: 560 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

BIN
assets/img/logo-access.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

BIN
assets/img/logo-check.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

BIN
assets/img/logo-exploit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
assets/img/sudalys_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 888 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 888 B

39
assets/js/cookies.js Normal file
View File

@ -0,0 +1,39 @@
var application
function getApplication(){
const body = document.getElementsByTagName('body')[0];
application = body.getAttribute('data-application');
}
getApplication();
// Support pour différents systèmes de navigation
if (typeof Turbo !== 'undefined') {
document.addEventListener('turbo:load', getApplication);
document.addEventListener('turbo:render', getApplication);
}
// Support pour les applications SPA qui utilisent l'historique de navigation
window.addEventListener('popstate', getApplication);
window.setCookie = function (cname, cvalue, exdays) {
const d = new Date();
d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
let expires = "expires=" + d.toUTCString();
document.cookie = application + "-" + cname + "=" + cvalue + ";" + expires + ";path=/;SameSite=Strict";
}
window.getCookie = function (cname) {
let name = application + "-" + cname + "=";
let decodedCookie = decodeURIComponent(document.cookie);
let ca = decodedCookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') {
c = c.substring(1);
}
if (c.indexOf(name) === 0) {
return c.substring(name.length, c.length);
}
}
return "";
}

View File

@ -0,0 +1,39 @@
//Open submenu on hover in compact sidebar mode and horizontal menu mode
function initSubMenu(){
var sidebar = document.querySelectorAll('.sidebar .nav-item')
sidebar.forEach(element => {
element.addEventListener('mouseenter', eventMenu)
element.addEventListener('mouseleave', eventMenu)
})
}
function eventMenu(e){
var body = document.querySelector('body');
var sidebarIconOnly = body.classList.contains("sidebar-icon-only");
var sidebarFixed = body.classList.contains("sidebar-fixed");
if (!('ontouchstart' in document.documentElement)) {
if (sidebarIconOnly) {
var menuItem = this;
if (e.type === 'mouseenter') {
menuItem.classList.add('hover-open')
} else {
menuItem.classList.remove('hover-open')
}
}
}
}
// Exécuter l'initialisation au chargement de la page
document.addEventListener('DOMContentLoaded', initSubMenu);
// Support pour différents systèmes de navigation
if (typeof Turbo !== 'undefined') {
document.addEventListener('turbo:load', initSubMenu);
document.addEventListener('turbo:render', initSubMenu);
}
// Support pour les applications SPA qui utilisent l'historique de navigation
window.addEventListener('popstate', initSubMenu);

0
assets/js/off_canvas.js Normal file
View File

65
assets/js/template.js Normal file
View File

@ -0,0 +1,65 @@
// Utilisation des fonctions de cookies globales définies dans app.js
// Fonction d'initialisation du template
function initTemplate() {
// Appliquer l'état du menu depuis le cookie
applyMenuState();
// Initialiser le bouton de minimisation du menu
initMinimizeButton();
}
// Fonction pour appliquer l'état du menu depuis le cookie
function applyMenuState() {
var body = document.querySelector('body');
var menuState = getCookie('sidebar_state');
// Si le cookie existe, appliquer l'état enregistré
if (menuState === 'collapsed') {
if (!body.classList.contains('sidebar-icon-only')) {
body.classList.add('sidebar-icon-only');
}
} else if (menuState === 'expanded') {
if (body.classList.contains('sidebar-icon-only')) {
body.classList.remove('sidebar-icon-only');
}
}
}
// Fonction pour initialiser le bouton de minimisation du menu
function initMinimizeButton() {
// Supprimer l'ancien gestionnaire d'événements s'il existe
document.querySelectorAll('[data-toggle="minimize"]').forEach(function(button) {
// Créer une copie du bouton pour supprimer tous les écouteurs d'événements
var newButton = button.cloneNode(true);
button.parentNode.replaceChild(newButton, button);
// Ajouter le nouvel écouteur d'événements
newButton.addEventListener("click", function() {
var body = document.querySelector('body');
if ((body.classList.contains('sidebar-toggle-display')) || (body.classList.contains('sidebar-absolute'))) {
body.classList.toggle('sidebar-hidden');
// Enregistrer l'état dans un cookie
var newState = body.classList.contains('sidebar-hidden') ? 'collapsed' : 'expanded';
setCookie('sidebar_state', newState, 365); // Valable 1 an
} else {
body.classList.toggle('sidebar-icon-only');
// Enregistrer l'état dans un cookie
var newState = body.classList.contains('sidebar-icon-only') ? 'collapsed' : 'expanded';
setCookie('sidebar_state', newState, 365); // Valable 1 an
}
});
});
}
// Exécuter l'initialisation au chargement de la page
document.addEventListener('DOMContentLoaded', initTemplate);
// Support pour différents systèmes de navigation
if (typeof Turbo !== 'undefined') {
document.addEventListener('turbo:load', initTemplate);
document.addEventListener('turbo:render', initTemplate);
}
// Support pour les applications SPA qui utilisent l'historique de navigation
window.addEventListener('popstate', initTemplate);

View File

@ -1,3 +1,15 @@
/*variable*/
:root{
--primary-blue-light : #086572;
--primary-blue-dark : #094754;
--black-font: #1D1E1C;
--delete : #E42E31;
--disable : #A3A3A3;
--check : #80F20E;
--secondary : #cc664c;
--secondary-dark : #a5543d;
}
html {
margin: 0;
padding: 0;
@ -31,3 +43,89 @@ body {
overflow: hidden;
}
.page-body-wrapper {
min-height: calc(100vh - 60px);
display: -webkit-flex;
display: flex;
-webkit-flex-direction: row;
flex-direction: row;
padding-left: 0;
padding-right: 0;
padding-top: 60px;
&.full-page-wrapper {
width: 100%;
min-height: 100vh;
padding-top: 0;
}
}
.main-panel {
transition: width 0.25s ease, margin 0.25s ease;
width: calc(100% - 235px);
min-height: calc(100vh - 60px);
display: -webkit-flex;
display: flex;
-webkit-flex-direction: column;
flex-direction: column;
}
.content-wrapper {
background: #EEF0FD;
width: 100%;
-webkit-flex-grow: 1;
flex-grow: 1;
display: flex;
}
.footer {
background: #fff;
padding: 10px 2.45rem;
transition: all 0.25s ease;
-moz-transition: all 0.25s ease;
-webkit-transition: all 0.25s ease;
-ms-transition: all 0.25s ease;
font-size: calc(0.875rem - 0.05rem);
font-family: "Nunito", sans-serif;
font-weight: 400;
border-top: 1px solid rgba(0, 0, 0, 0.06);
}
.btn-primary{
background: var(--primary-blue-light);
color : #FFFFFF;
border: var(--primary-blue-dark);
border-radius: 1rem;
}
.btn-primary:hover{
background: var(--primary-blue-dark);
color : #FFFFFF;
border: var(--primary-blue-light);
}
.btn-danger{
background: var(--delete);
color : #FFFFFF;
border: var(--delete);
border-radius: 1rem;
}
.color-primary{
color: var(--primary-blue-light);
}
.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

@ -69,8 +69,11 @@
border-radius: 0;
}
.navbar-nav-right{
flex-direction: row;
.navbar .navbar-menu-wrapper .navbar-toggler:active,
.navbar .navbar-menu-wrapper .navbar-toggler:focus {
outline: none !important;
box-shadow: none !important;
outline: 0;
}
.navbar .navbar-menu-wrapper .navbar-toggler:not(.navbar-toggler-right) {
@ -82,12 +85,24 @@
transition: transform 0.3s linear;
}
.sidebar-icon-only .navbar .navbar-menu-wrapper .navbar-toggler:not(.navbar-toggler-right) {
transform: rotate(180deg);
}
.navbar-nav-right{
flex-direction: row;
}
.navbar .navbar-menu-wrapper .navbar-nav .nav-item.nav-search .input-group{
align-items: center;
}
.navbar .navbar-menu-wrapper .navbar-nav .nav-item.nav-search .input-group .form-control{
margin-left: 0.7rem;
}
#navbar-search-icon > #search{
vertical-align: middle;
vertical-align: baseline;
}
.navbar .navbar-menu-wrapper .navbar-nav.navbar-nav-right {
@ -245,3 +260,15 @@
width:auto;
max-height:40px;
}
.navbar .navbar-menu-wrapper .navbar-nav .nav-item.nav-search .input-group .form-control{
background: transparent;
border: 0;
color: #000;
padding: 0;
}
#change-project{
padding: 2px 5px;
font-size: 16px;
}

290
assets/styles/sidebar.css Normal file
View File

@ -0,0 +1,290 @@
.sidebar {
min-height: calc(100vh - 60px);
background: #fff;
font-weight: 500;
padding: 0;
width: 235px;
z-index: 11;
transition: width 0.25s ease, background 0.25s ease;
-webkit-transition: width 0.25s ease, background 0.25s ease;
-moz-transition: width 0.25s ease, background 0.25s ease;
-ms-transition: width 0.25s ease, background 0.25s ease;
}
.sidebar .nav {
overflow: hidden;
flex-wrap: nowrap;
flex-direction: column;
margin-bottom: 60px;
}
.sidebar .nav:not(.sub-menu) {
padding-top: 1.45rem;
padding-left: 1rem;
padding-right: 1rem;
padding-bottom: 0.5rem;
}
.sidebar .nav .nav-item {
-webkit-transition-duration: 0.25s;
-moz-transition-duration: 0.25s;
-o-transition-duration: 0.25s;
transition-duration: 0.25s;
transition-property: background;
-webkit-transition-property: background;
}
.sidebar .nav .nav-item .collapse {
z-index: 999;
}
.sidebar .nav .nav-item.active {
border-radius: 8px;
box-shadow: 0px 0px 5px 0px rgba(197, 197, 197, 0.75);
}
.sidebar .nav:not(.sub-menu) > .nav-item {
border-radius: 8px;
margin-top: 0.2rem;
}
.sidebar > .nav:not(.sub-menu) > .nav-item:hover {
border-radius: 8px;
color: #494949;
}
.sidebar .nav .nav-item .nav-link {
display: -webkit-flex;
display: flex;
-webkit-align-items: center;
align-items: center;
white-space: nowrap;
padding: 0.8125rem 1.937rem 0.8125rem 1rem;
color: #848484;
border-radius: 8px;
-webkit-transition-duration: 0.45s;
-moz-transition-duration: 0.45s;
-o-transition-duration: 0.45s;
transition-duration: 0.45s;
transition-property: color;
-webkit-transition-property: color;
height: 50px;
}
.sidebar .nav .nav-item.active > .nav-link {
color:lightgrey;
position: relative;
}
.sidebar .nav:not(.sub-menu) > .nav-item > .nav-link {
margin: 0;
}
.sidebar .nav .nav-item .nav-link i.menu-icon {
font-size: 1rem;
line-height: 1;
margin-right: 1rem;
color: #838383;
}
.sidebar .nav .nav-item .nav-link i.menu-arrow {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
margin-left: auto;
margin-right: 0;
color: #686868;
transform: rotate(0deg);
-webkit-transition: transform 0.3s linear;
-moz-transition: transform 0.3s linear;
-ms-transition: transform 0.3s linear;
-o-transition: transform 0.3s linear;
transition: transform 0.3s linear;
}
.sidebar .nav .nav-item .nav-link .menu-title {
display: inline-block;
font-size: 0.875rem;
line-height: 1;
vertical-align: middle;
}
.sidebar .nav .nav-item.active > .nav-link i, .sidebar .nav .nav-item.active > .nav-link .menu-title, .sidebar .nav .nav-item.active > .nav-link .menu-arrow {
color: #494949;
}
.sidebar .nav:not(.sub-menu) > .nav-item > .nav-link[aria-expanded=true] {
border-radius: 8px 8px 0 0;
color: #494949;
}
.sidebar .nav:not(.sub-menu) > .nav-item > .nav-link[aria-expanded=true] i.menu-arrow {
transform: rotate(90deg);
}
.sidebar .nav.sub-menu {
margin-bottom: 0;
margin-top: 0;
list-style: none;
padding: 0.25rem 1.5rem 0 2rem;
padding-bottom: 12px;
}
.sidebar .nav.sub-menu .nav-item {
position: relative;
padding: 0;
display: flex;
align-items: center;
color: #969696;
justify-content: space-between;
list-style: none;
}
.sidebar .nav.sub-menu .nav-item svg {
position: absolute;
color: #b2b2b2;
}
.sidebar .nav.sub-menu .nav-item .nav-link {
padding: 0.7rem 1rem;
position: relative;
font-size: 0.875rem;
line-height: 1;
height: auto;
border-top: 0;
font-weight: 400;
}
.sidebar .nav:not(.sub-menu) .nav-link:hover{
color: #494949;
}
@media (min-width: 992px) {
.sidebar-icon-only .sidebar {
width: 70px;
}
.sidebar-icon-only .sidebar .nav {
overflow: visible;
padding-left: 0;
padding-right: 0;
}
.sidebar-icon-only .navbar .navbar-brand-wrapper {
width: 70px;
}
.sidebar-icon-only .navbar .navbar-brand-wrapper .brand-logo {
display: none;
}
.sidebar-icon-only .navbar .navbar-brand-wrapper .brand-logo-mini {
display: inline-block;
}
.sidebar-icon-only .sidebar .nav .nav-item .nav-link {
display: block;
padding-left: 0.5rem;
padding-right: 0.5rem;
text-align: center;
position: static;
}
.sidebar-icon-only .sidebar .nav .nav-item {
border-radius: 0px;
position: relative;
}
.sidebar-icon-only .sidebar .nav .nav-item .collapse {
display: none;
}
.sidebar-icon-only .sidebar .nav:not(.sub-menu) .nav-item.active {
border-radius: 0;
box-shadow: 4px 0px 7px 0px rgba(182, 185, 189, 0.25);
}
.sidebar-icon-only .sidebar .nav .nav-item .nav-link i.menu-icon {
margin-right: 0;
margin-left: 0;
margin-bottom: 0;
}
.sidebar-icon-only .sidebar .nav .nav-item .nav-link i.menu-arrow {
display: none;
}
.sidebar-icon-only .sidebar .nav.sub-menu {
padding: 0 0 0 1.5rem;
}
.sidebar-icon-only .sidebar .nav.sub-menu .nav-item .nav-link {
text-align: left;
padding-left: 20px;
}
.sidebar-icon-only .main-panel {
width: calc(100% - 70px);
}
.sidebar-icon-only .navbar .navbar-menu-wrapper {
width: calc(100% - 70px);
}
.sidebar-icon-only .sidebar .nav .nav-item .nav-link .menu-title, .sidebar-icon-only .sidebar .nav .nav-item .nav-link .badge, .sidebar-icon-only .sidebar .nav .nav-item .nav-link .menu-sub-title {
display: none;
}
.sidebar-icon-only .sidebar .nav .nav-item .nav-link .menu-title {
border-radius: 0 5px 5px 0px;
}
.sidebar .nav:not(.sub-menu) > .nav-item:hover {
border-radius: 8px;
box-shadow: 0px 0px 5px 0px rgba(197, 197, 197, 0.75);
}
.sidebar-icon-only .nav:not(.sub-menu) > .nav-item:hover,
.sidebar-icon-only .nav:not(.sub-menu) > .nav-item:hover .nav-link{
border-radius: 0;
}
.sidebar-icon-only .sidebar .nav .nav-item:hover .nav-link[aria-expanded] .menu-title {
border-radius: 0 5px 0 0;
}
}
.sidebar-icon-only .sidebar .nav .nav-item.hover-open .nav-link .menu-title {
display: -webkit-flex;
display: flex;
-webkit-align-items: center;
align-items: center;
background: #ffffff;
padding: 0.5rem 1.4rem;
left: 70px;
position: absolute;
text-align: left;
top: 0;
bottom: 0;
width: 190px;
z-index: 10000;
line-height: 1.8;
-webkit-box-shadow: 4px 0px 7px 0px rgba(182, 185, 189, 0.25);
box-shadow: 4px 0px 7px 0px rgba(182, 185, 189, 0.25);
}
.sidebar-icon-only .sidebar .nav .nav-item.hover-open .nav-link .menu-title:after {
display: none;
}
.sidebar-icon-only .sidebar .nav .nav-item.hover-open .collapse,
.sidebar-icon-only .sidebar .nav .nav-item.hover-open .collapsing {
display: block;
background: #fff;
border-radius: 0 0 5px 0;
position: absolute;
left: 70px;
width: 190px;
-webkit-box-shadow: 4px 4px 7px 0px rgba(182, 185, 189, 0.25);
box-shadow: 4px 4px 7px 0px rgba(182, 185, 189, 0.25);
}

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": {

1976
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

@ -11,8 +11,9 @@ security:
property: email
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_ALLOWED_TO_SWITCH, ROLE_ADMIN]
firewalls:
dev:

View File

@ -2,6 +2,10 @@ twig:
file_name_pattern: '*.twig'
form_themes: ['bootstrap_5_layout.html.twig']
globals:
application: '%env(APPLICATION)%'
version: '0.4'
when@test:
twig:
strict_variables: true

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

@ -35,4 +35,11 @@ return [
'version' => '5.3.5',
'type' => 'css',
],
'choices.js' => [
'version' => '11.1.0',
],
'choices.js/public/assets/styles/choices.min.css' => [
'version' => '11.1.0',
'type' => 'css',
],
];

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250709072959 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 SEQUENCE subscriptions_id_seq CASCADE');
$this->addSql('ALTER TABLE subscriptions DROP CONSTRAINT fk_4778a0167b3b43d');
$this->addSql('DROP TABLE subscriptions');
}
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('CREATE SEQUENCE subscriptions_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE subscriptions (id SERIAL NOT NULL, users_id INT NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX idx_4778a0167b3b43d ON subscriptions (users_id)');
$this->addSql('ALTER TABLE subscriptions ADD CONSTRAINT fk_4778a0167b3b43d FOREIGN KEY (users_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
}
}

View File

@ -0,0 +1,35 @@
<?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 Version20250709073312 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE subscriptions (id SERIAL NOT NULL, users_id INT NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_4778A0167B3B43D ON subscriptions (users_id)');
$this->addSql('ALTER TABLE subscriptions ADD CONSTRAINT FK_4778A0167B3B43D FOREIGN KEY (users_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE subscriptions DROP CONSTRAINT FK_4778A0167B3B43D');
$this->addSql('DROP TABLE subscriptions');
}
}

View File

@ -0,0 +1,35 @@
<?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 Version20250709073752 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE user_tab (id SERIAL NOT NULL, users_id INT NOT NULL, ip_address VARCHAR(255) NOT NULL, tab_id VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_98F5228767B3B43D ON user_tab (users_id)');
$this->addSql('ALTER TABLE user_tab ADD CONSTRAINT FK_98F5228767B3B43D FOREIGN KEY (users_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE user_tab DROP CONSTRAINT FK_98F5228767B3B43D');
$this->addSql('DROP TABLE user_tab');
}
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250709115309 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 SEQUENCE subscriptions_id_seq CASCADE');
$this->addSql('ALTER TABLE subscriptions DROP CONSTRAINT fk_4778a0167b3b43d');
$this->addSql('DROP TABLE subscriptions');
}
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('CREATE SEQUENCE subscriptions_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE subscriptions (id SERIAL NOT NULL, users_id INT NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX idx_4778a0167b3b43d ON subscriptions (users_id)');
$this->addSql('ALTER TABLE subscriptions ADD CONSTRAINT fk_4778a0167b3b43d FOREIGN KEY (users_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
}
}

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 Version20250709120951 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE organizations ADD name VARCHAR(255) 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 DROP name');
}
}

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 Version20250709121023 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE organizations ADD name VARCHAR(255) DEFAULT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE organizations DROP name');
}
}

View File

@ -0,0 +1,33 @@
<?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 Version20250709141934 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 "user" ADD last_connection DATE DEFAULT NULL');
$this->addSql('COMMENT ON COLUMN "user".last_connection IS \'(DC2Type:date_immutable)\'');
}
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 "user" DROP last_connection');
}
}

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 Version20250710070735 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 "user" ALTER last_connection TYPE DATE');
$this->addSql('COMMENT ON COLUMN "user".last_connection IS 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 "user" ALTER last_connection TYPE DATE');
$this->addSql('COMMENT ON COLUMN "user".last_connection IS \'(DC2Type:date_immutable)\'');
}
}

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 Version20250710071344 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 "user" ALTER last_connection TYPE TIMESTAMP(0) WITHOUT TIME ZONE');
}
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 "user" ALTER last_connection TYPE DATE');
}
}

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 Version20250710090534 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 "user" ADD phone_number VARCHAR(20) 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 "user" DROP phone_number');
}
}

View File

@ -0,0 +1,33 @@
<?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 Version20250716083017 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE users_organizations ADD created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP');
$this->addSql('COMMENT ON COLUMN users_organizations.created_at IS \'(DC2Type:datetime_immutable)\'');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE users_organizations DROP created_at');
}
}

View File

@ -0,0 +1,31 @@
<?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 Version20250716130850 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
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
}
}

View File

@ -0,0 +1,52 @@
<?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 Version20250724133531 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE user_organization_app (id SERIAL NOT NULL, applications_id INT DEFAULT NULL, organization_id INT DEFAULT NULL, users_id INT DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_BEF66DF129A0022 ON user_organization_app (applications_id)');
$this->addSql('CREATE INDEX IDX_BEF66DF132C8A3DE ON user_organization_app (organization_id)');
$this->addSql('CREATE INDEX IDX_BEF66DF167B3B43D ON user_organization_app (users_id)');
$this->addSql('CREATE TABLE user_organization_roles (id SERIAL NOT NULL, role_id INT DEFAULT NULL, users_id INT DEFAULT NULL, organization_id INT DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_94FD2EFBD60322AC ON user_organization_roles (role_id)');
$this->addSql('CREATE INDEX IDX_94FD2EFB67B3B43D ON user_organization_roles (users_id)');
$this->addSql('CREATE INDEX IDX_94FD2EFB32C8A3DE ON user_organization_roles (organization_id)');
$this->addSql('ALTER TABLE user_organization_app ADD CONSTRAINT FK_BEF66DF129A0022 FOREIGN KEY (applications_id) REFERENCES apps (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE user_organization_app ADD CONSTRAINT FK_BEF66DF132C8A3DE FOREIGN KEY (organization_id) REFERENCES organizations (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE user_organization_app ADD CONSTRAINT FK_BEF66DF167B3B43D FOREIGN KEY (users_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE user_organization_roles ADD CONSTRAINT FK_94FD2EFBD60322AC FOREIGN KEY (role_id) REFERENCES roles (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE user_organization_roles ADD CONSTRAINT FK_94FD2EFB67B3B43D FOREIGN KEY (users_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE user_organization_roles ADD CONSTRAINT FK_94FD2EFB32C8A3DE FOREIGN KEY (organization_id) REFERENCES organizations (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE user_organization_app DROP CONSTRAINT FK_BEF66DF129A0022');
$this->addSql('ALTER TABLE user_organization_app DROP CONSTRAINT FK_BEF66DF132C8A3DE');
$this->addSql('ALTER TABLE user_organization_app DROP CONSTRAINT FK_BEF66DF167B3B43D');
$this->addSql('ALTER TABLE user_organization_roles DROP CONSTRAINT FK_94FD2EFBD60322AC');
$this->addSql('ALTER TABLE user_organization_roles DROP CONSTRAINT FK_94FD2EFB67B3B43D');
$this->addSql('ALTER TABLE user_organization_roles DROP CONSTRAINT FK_94FD2EFB32C8A3DE');
$this->addSql('DROP TABLE user_organization_app');
$this->addSql('DROP TABLE user_organization_roles');
}
}

View File

@ -0,0 +1,56 @@
<?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 Version20250725065027 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 SEQUENCE user_organization_roles_id_seq CASCADE');
$this->addSql('DROP SEQUENCE user_organization_app_id_seq CASCADE');
$this->addSql('ALTER TABLE user_organization_app DROP CONSTRAINT fk_bef66df129a0022');
$this->addSql('ALTER TABLE user_organization_app DROP CONSTRAINT fk_bef66df132c8a3de');
$this->addSql('ALTER TABLE user_organization_app DROP CONSTRAINT fk_bef66df167b3b43d');
$this->addSql('ALTER TABLE user_organization_roles DROP CONSTRAINT fk_94fd2efb32c8a3de');
$this->addSql('ALTER TABLE user_organization_roles DROP CONSTRAINT fk_94fd2efb67b3b43d');
$this->addSql('ALTER TABLE user_organization_roles DROP CONSTRAINT fk_94fd2efbd60322ac');
$this->addSql('DROP TABLE user_organization_app');
$this->addSql('DROP TABLE user_organization_roles');
}
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('CREATE SEQUENCE user_organization_roles_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE SEQUENCE user_organization_app_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE user_organization_app (id SERIAL NOT NULL, applications_id INT DEFAULT NULL, organization_id INT DEFAULT NULL, users_id INT DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX idx_bef66df129a0022 ON user_organization_app (applications_id)');
$this->addSql('CREATE INDEX idx_bef66df132c8a3de ON user_organization_app (organization_id)');
$this->addSql('CREATE INDEX idx_bef66df167b3b43d ON user_organization_app (users_id)');
$this->addSql('CREATE TABLE user_organization_roles (id SERIAL NOT NULL, role_id INT DEFAULT NULL, users_id INT DEFAULT NULL, organization_id INT DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX idx_94fd2efb32c8a3de ON user_organization_roles (organization_id)');
$this->addSql('CREATE INDEX idx_94fd2efb67b3b43d ON user_organization_roles (users_id)');
$this->addSql('CREATE INDEX idx_94fd2efbd60322ac ON user_organization_roles (role_id)');
$this->addSql('ALTER TABLE user_organization_app ADD CONSTRAINT fk_bef66df129a0022 FOREIGN KEY (applications_id) REFERENCES apps (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE user_organization_app ADD CONSTRAINT fk_bef66df132c8a3de FOREIGN KEY (organization_id) REFERENCES organizations (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE user_organization_app ADD CONSTRAINT fk_bef66df167b3b43d FOREIGN KEY (users_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE user_organization_roles ADD CONSTRAINT fk_94fd2efb32c8a3de FOREIGN KEY (organization_id) REFERENCES organizations (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE user_organization_roles ADD CONSTRAINT fk_94fd2efb67b3b43d FOREIGN KEY (users_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE user_organization_roles ADD CONSTRAINT fk_94fd2efbd60322ac FOREIGN KEY (role_id) REFERENCES roles (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
}
}

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');
}
}

View File

@ -14,7 +14,6 @@ final class IndexController extends AbstractController
#[Route('/', name: 'app_index')]
public function index(Request $request, LoggerInterface $logger): Response
{
$logger->info("SESSION ID: " . $request->getSession()->getId());
return $this->render('index/index.html.twig', [
'controller_name' => 'IndexController',
]);

View File

@ -0,0 +1,182 @@
<?php
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)
{
}
#[Route('/', name: 'index', methods: ['GET'])]
public function index(): Response
{
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
$organizations = $this->entityManager->getRepository(Organizations::class)->findBy(['isActive' => true]);
} else {
$user = $this->getUser();
if (!$user) {
return $this->redirectToRoute('app_login');
}
$userIdentifier = $user->getUserIdentifier();
$organizations = $this->entityManager->getRepository(UsersOrganizations::class)->findOrganizationsByUserEmailAndRoleName($userIdentifier, 'ADMIN');
if (!$organizations) {
// if user is not admin in any organization, throw access denied
throw $this->createNotFoundException(self::ACCESS_DENIED);
}
}
return $this->render('organization/index.html.twig', [
'organizations' => $organizations,
]);
}
#[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');
}
//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);
$adminUsers = $this->entityManager->getRepository(UsersOrganizations::class)->getAdminUsersByOrganization($organization);
// reusing the method to avoid code duplication even though it returns an array of UsersOrganizations
$org = $this->usersOrganizationService->findActiveUsersByOrganizations([$organization]);
// get all applications
$applications = $this->organizationsService->getApplicationsWithAccessStatus($organization);
$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);
}
return $this->render('organization/show.html.twig', [
'organization' => $organization,
'adminUsers' => $adminUsers,
'newUsers' => $newUsers,
'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

@ -0,0 +1,375 @@
<?php
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;
use App\Entity\UsersOrganizations;
use App\Service\UserOrganizationService;
use App\Service\UserService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Asset\Packages;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
#[Route(path: '/user', name: 'user_')]
class UserController extends AbstractController
{
private const NOT_FOUND = 'Entity not found';
private const ACCESS_DENIED = 'Access denied';
public function __construct(
private readonly UserOrganizationService $userOrganizationService,
private readonly EntityManagerInterface $entityManager,
private readonly UserService $userService)
{
}
/**
* GET /user - List all users (index/collection)
*/
#[Route('/', name: 'index', methods: ['GET'])]
public function index(EntityManagerInterface $entityManager): Response
{
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
$usersByOrganization = $this->userOrganizationService->getActiveUsersGroupedByOrganization();
// dd($usersByOrganization);
} else{
$user = $this->getUser();
if (!$user) {
return $this->redirectToRoute('app_login');
}
$userIdentifier = $user->getUserIdentifier();
$organizations = $this->entityManager->getRepository(UsersOrganizations::class)->findOrganizationsByUserEmailAndRoleName($userIdentifier, 'ADMIN');
if(!$organizations) {
// if user is not admin in any organization, throw access denied
throw $this->createNotFoundException(self::ACCESS_DENIED);
}
$usersByOrganization = $this->userOrganizationService->findActiveUsersByOrganizations($organizations);
}
return $this->render('user/index.html.twig', [
'usersByOrganization' => $usersByOrganization,
'controller_name' => 'IndexController',
]);
}
/**
* GET /user/{id} - Show specific user (show/member)
*/
#[Route('/{id}', name: 'show', requirements: ['id' => '\d+'], methods: ['GET'])]
public function show(int $id, EntityManagerInterface $entityManager, Request $request): Response
{
if (!$this->isGranted('ROLE_SUPER_ADMIN')) {
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
}
$user = $entityManager->getRepository(User::class)->find($id);
if (!$user) {
throw $this->createNotFoundException(self::NOT_FOUND);
}
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,
'userOrganizations' => $userOrganizations,
]);
}
/**
* GET /user/new - Show form to create new user and handle submission
*/
#[Route('/new', name: 'new', methods: ['GET', 'POST'])]
public function new(Request $request): Response
{
$form = $this->createForm(UserForm::class);
$organizationId = $request->query->get('organizationId');
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
//Data is a User object. App\Form\NewUserForm is a form type that maps to User entity
$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
return $this->redirectToRoute('user_index');
}
return $this->render('user/new.html.twig', [
'form' => $form->createView(),
'organizationId' => $organizationId,
]);
}
/**
* GET /user/{id}/edit - Show form to edit user
*/
#[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
if (!$this->isGranted('ROLE_SUPER_ADMIN')) {
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
}
//Fetch user by ID and handle not found case
$user = $entityManager->getRepository(User::class)->find($id);
if (!$user) {
throw $this->createNotFoundException(self::NOT_FOUND);
}
//Create form for editing user
$form = $this->createForm(UserForm::class, $user);
//Handle form submission
$form->handleRequest($request);
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
return $this->redirectToRoute('user_show', ['id' => $user->getId()]);
}
return $this->render('user/edit.html.twig', [
'form' => $form->createView(),
'user' => $user,
]);
}
/**
* DELETE /user/{id} - Delete user
*/
#[Route('/{id}', name: 'setDelete', requirements: ['id' => '\d+'], methods: ['POST'])]
public function setDelete(int $id, EntityManagerInterface $entityManager): Response
{
//This method is used to set a user as deleted without actually removing them from the database.
//Handle access control
if (!$this->isGranted('ROLE_SUPER_ADMIN')) {
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
}
//Fetch user by ID and handle not found case
$user = $entityManager->getRepository(User::class)->find($id);
if (!$user) {
throw $this->createNotFoundException(self::NOT_FOUND);
}
// 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');
}
/**
* DELETE /user/{id} - Delete user
*/
#[Route('/{id}', name: 'delete', requirements: ['id' => '\d+'], methods: ['DELETE'])]
public function delete(int $id, EntityManagerInterface $entityManager): Response
{
if (!$this->isGranted('ROLE_SUPER_ADMIN')) {
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
}
$user = $entityManager->getRepository(User::class)->find($id);
if (!$user) {
throw $this->createNotFoundException(self::NOT_FOUND);
}
// 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');
}
/**
* GET /user/deactivate/{id} - Deactivate user
* This method is used to deactivate a user without deleting them.
* The user will still exist in the database but will not be active.
*/
#[Route('/deactivate/{id}', name: 'deactivate', methods: ['GET'])]
public function deactivate(Request $request, EntityManagerInterface $entityManager): Response
{
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
$userId = $request->attributes->get('id');
$user = $entityManager->getRepository(User::class)->find($userId);
if (!$user) {
throw $this->createNotFoundException(self::NOT_FOUND);
}
$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');
}
return new Response('Unauthorized', Response::HTTP_UNAUTHORIZED);
}
/**
* Update organization user /userOrganizationEdit/{id} - Update organization user
* The id parameter is the ID of the UsersOrganizations entity.
*/
#[Route('/userOrganizationEdit/{id}', name: 'organization_edit', requirements: ['id' => '\d+'], methods: ['GET', 'POST'])]
public function userOrganizationEdit(int $id, Request $request, EntityManagerInterface $entityManager, Packages $packages): Response
{
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
// get the UsersOrganizations entity by ID and handle not found case same for user
$userOrganization = $entityManager->getRepository(UsersOrganizations::class)->find($id);
if (!$userOrganization) {
throw $this->createNotFoundException(self::NOT_FOUND);
}
$user = $userOrganization->getUsers() ?? throw $this->createNotFoundException(self::NOT_FOUND);
$organization = $userOrganization->getOrganization() ?? throw $this->createNotFoundException(self::NOT_FOUND);
//Handle the POST
if ($request->isMethod('POST')) {
// Get the selected roles and apps from the request
$selectedRoles = $request->request->all('roles');
$selectedApps = $request->request->all('applications');
// order in important here. apps MUST be before roles
$this->userOrganizationService->setUserOrganizationsApps($user, $organization,$selectedApps);
$this->userOrganizationService->setUserOrganizations($user, $organization, $selectedRoles);
// Redirect to the user profile after successful update
return $this->redirectToRoute('user_show', ['id' => $user->getId()]);
}
//Overwrite the userOrganization with the userOrganizationsService for data consistency
// NULL pointer won't occur here because a valid UsersOrganizations entity was fetched above
$userOrganization = $this->userOrganizationService->getUserOrganizations($userOrganization->getUsers(), $userOrganization->getOrganization()->getId());
// Fetch all roles and apps
$roles = $entityManager->getRepository(Roles::class)->findAll();
$apps = $organization->getApps() ?? throw $this->createNotFoundException(self::NOT_FOUND);
if (!$roles) {
throw $this->createNotFoundException(self::NOT_FOUND);
}
if (!$apps) {
throw $this->createNotFoundException(self::NOT_FOUND);
}
// Map roles and apps to arrays for rendering
$rolesArray = array_map(static function ($role) {
return [
'id' => $role->getId(),
'name' => $role->getName()
];
}, $roles);
$appsArray = [];
foreach ($apps as $app) {
$appsArray[] = [
'id' => $app->getId(),
'name' => $app->getName(),
'icon' => $packages->getUrl($app->getLogoUrl()),
];
}
// Map selected roles and apps to their IDs for the form
$selectedRoles = array_map(static function ($role) {
return $role->getId();
}, $userOrganization[0]["roles"]);
$selectedApps = array_map(static function ($app) {
return $app->getId();
}, $userOrganization[0]["apps"]);
return $this->render('user/organization/edit.html.twig', [
'userOrganization' => $userOrganization,
'user' => $user,
'rolesArray' => $rolesArray,
'selectedRoleIds' => $selectedRoles,
'appsArray' => $appsArray,
'selectedAppIds' => $selectedApps,]);
}
/**
* GET /user/deactivateOrganization/{id} - Deactivate user
* This method is used to deactivate a user without deleting them in an organization.
* The user will still exist in the database but will not be active.
*/
#[Route('/organizationDeactivate/{id}', name: 'organization_deactivate', requirements: ['id' => '\d+'], methods: ['GET'])]
public function deactivateUserOrganization(int $id, Request $request, EntityManagerInterface $entityManager): Response
{
$this->denyAccessUnlessGranted('ROLE_ADMIN');
$userOrganization = $entityManager->getRepository(UsersOrganizations::class)->find($id) ?? throw $this->createNotFoundException(self::NOT_FOUND);
$user = $userOrganization->getUsers() ?? throw $this->createNotFoundException(self::NOT_FOUND);
$organization = $userOrganization->getOrganization() ?? throw $this->createNotFoundException(self::NOT_FOUND);
$this->userOrganizationService->deactivateAllUserRoles($user, $organization);
return $this->redirectToRoute('user_show', ['id' => $user->getId()]);
}
}

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>
@ -42,9 +42,20 @@ class Organizations
#[ORM\ManyToMany(targetEntity: Apps::class, mappedBy: 'organization')]
private Collection $apps;
#[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
@ -162,4 +173,46 @@ class Organizations
return $this;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
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

@ -1,36 +0,0 @@
<?php
namespace App\Entity;
use App\Repository\SubscriptionsRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: SubscriptionsRepository::class)]
class Subscriptions
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'subscriptions')]
#[ORM\JoinColumn(nullable: false)]
private ?User $users = null;
public function getId(): ?int
{
return $this->id;
}
public function getUsers(): ?User
{
return $this->users;
}
public function setUsers(?User $users): static
{
$this->users = $users;
return $this;
}
}

View File

@ -5,10 +5,13 @@ namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
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;
#[UniqueEntity(fields: ['email'], message: 'This email address is already in use.')]
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: '`user`')]
#[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_EMAIL', fields: ['email'])]
@ -44,7 +47,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
private ?\DateTimeImmutable $createdAt = null;
#[ORM\Column(length: 255)]
private ?string $picture_url = null;
private ?string $pictureUrl = null;
#[ORM\Column(options: ['default' => 'CURRENT_TIMESTAMP'])]
private ?\DateTimeImmutable $modifiedAt = null;
@ -55,17 +58,21 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
#[ORM\Column(options: ['default' => false])]
private ?bool $isDeleted = null;
/**
* @var Collection<int, Subscriptions>
*/
#[ORM\OneToMany(targetEntity: Subscriptions::class, mappedBy: 'users')]
private Collection $subscriptions;
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
private ?\DateTime $lastConnection = null;
#[ORM\Column(length: 20, nullable: true)]
private ?string $phoneNumber = null;
public function __construct()
{
$this->subscriptions = new ArrayCollection();
$this->createdAt = new \DateTimeImmutable();
$this->modifiedAt = new \DateTimeImmutable();
$this->isActive = true;
$this->isDeleted = false;
}
public function getId(): ?int
{
return $this->id;
@ -179,12 +186,12 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
public function getPictureUrl(): ?string
{
return $this->picture_url;
return $this->pictureUrl;
}
public function setPictureUrl(string $picture_url): static
public function setPictureUrl(string $pictureUrl): static
{
$this->picture_url = $picture_url;
$this->pictureUrl = $pictureUrl;
return $this;
}
@ -233,33 +240,29 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return (string) $this->getId();
}
/**
* @return Collection<int, Subscriptions>
*/
public function getSubscriptions(): Collection
public function getLastConnection(): ?\DateTime
{
return $this->subscriptions;
return $this->lastConnection;
}
public function addSubscription(Subscriptions $subscription): static
public function setLastConnection(?\DateTime $lastConnection): static
{
if (!$this->subscriptions->contains($subscription)) {
$this->subscriptions->add($subscription);
$subscription->setUsers($this);
}
$this->lastConnection = $lastConnection;
return $this;
}
public function removeSubscription(Subscriptions $subscription): static
public function getPhoneNumber(): ?string
{
if ($this->subscriptions->removeElement($subscription)) {
// set the owning side to null (unless already changed)
if ($subscription->getUsers() === $this) {
$subscription->setUsers(null);
}
}
return $this->phoneNumber;
}
public function setPhoneNumber(?string $phoneNumber): static
{
$this->phoneNumber = $phoneNumber;
return $this;
}
}

View File

@ -21,24 +21,29 @@ class UsersOrganizations
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
private ?organizations $organization = null;
private ?Organizations $organization = null;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
private ?roles $role = null;
private ?Roles $role = null;
#[ORM\Column(options: ['default' => true])]
private ?bool $isActive = null;
/**
* @var Collection<int, apps>
* @var Collection<int, Apps>
*/
#[ORM\ManyToMany(targetEntity: apps::class)]
#[ORM\ManyToMany(targetEntity: Apps::class)]
private Collection $apps;
#[ORM\Column(nullable:true, options: ['default' => 'CURRENT_TIMESTAMP'])]
private ?\DateTimeImmutable $createdAt = null;
public function __construct()
{
$this->apps = new ArrayCollection();
$this->isActive = true; // Default value for isActive
$this->createdAt = new \DateTimeImmutable(); // Set createdAt to current
}
public function getId(): ?int
@ -117,4 +122,16 @@ class UsersOrganizations
return $this;
}
public function getCreatedAt(): ?\DateTimeImmutable
{
return $this->createdAt;
}
public function setCreatedAt(?\DateTimeImmutable $createdAt): static
{
$this->createdAt = $createdAt;
return $this;
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\EventSubscriber;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use League\Bundle\OAuth2ServerBundle\Model\AccessToken;
use League\Bundle\OAuth2ServerBundle\Model\Client;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
class LoginSubscriber implements EventSubscriberInterface
{
private EntityManagerInterface $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
public static function getSubscribedEvents()
{
return [
LoginSuccessEvent::class => 'onLoginSuccess',
];
}
public function onLoginSuccess(LoginSuccessEvent $event): void
{
$user = $event->getUser();
if($user) {
$user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $user->getUserIdentifier()]);
$user->setLastConnection(new \DateTime('now', new \DateTimeZone('Europe/Paris')));
$easySolution = $this->entityManager->getRepository(Client::class)->findOneBy(['name' => 'EasySolution']);
if($easySolution) {
$accessToken = new AccessToken(
identifier: bin2hex(random_bytes(40)), // Generate unique identifier
expiry: new \DateTimeImmutable('+1 hour', new \DateTimeZone('Europe/Paris')),
client: $easySolution,
userIdentifier: $user->getUserIdentifier(),
scopes: ['email profile openid apps:easySolutions'] // Empty array if no specific scopes needed
);
$this->entityManager->persist($user);
$this->entityManager->persist($accessToken);
$this->entityManager->flush();
}
}
}
}

View File

@ -2,7 +2,6 @@
namespace App\EventSubscriber;
use App\Service\ClientService;
use Doctrine\ORM\EntityManagerInterface;
use League\Bundle\OAuth2ServerBundle\Event\ScopeResolveEvent;
use League\Bundle\OAuth2ServerBundle\Repository\ScopeRepository;
@ -16,15 +15,14 @@ final class ScopeResolveListener implements EventSubscriberInterface
{
private ClientRepositoryInterface $clientRepository;
private LoggerInterface $logger;
private ClientService $clientService;
private EntityManagerInterface $entityManager;
public function __construct(ClientRepositoryInterface $clientRepository, LoggerInterface $logger, ClientService $clientService, EntityManagerInterface $entityManager)
public function __construct(ClientRepositoryInterface $clientRepository, LoggerInterface $logger, EntityManagerInterface $entityManager)
{
$this->logger = $logger;
// Inject the client repository
$this->clientRepository = $clientRepository;
$this->clientService = $clientService;
$this->entityManager = $entityManager;
}

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,
]);
}
}

29
src/Form/UserForm.php Normal file
View File

@ -0,0 +1,29 @@
<?php
namespace App\Form;
use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class UserForm 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' => 'Prénom*'])
->add('surname', TextType::class, ['required' => true, 'label' => 'Nom*'])
->add('phoneNumber', TextType::class, ['required' => false, 'label' => 'Numéro de téléphone']);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => User::class,
]);
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
class UserOrganization extends AbstractType
{
public function buildForm($builder, array $options): void
{
$builder
->add('admin' , CheckboxType::class, [
'label' => 'Admin',
'required' => false])
->add('application' , ChoiceType::class, [
'label' => 'Application',
'choices' => [
'Application 1' => 'app1',
'Application 2' => 'app2',
'Application 3' => 'app3',
]]);
}
}

View File

@ -33,28 +33,14 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader
$this->getEntityManager()->flush();
}
// /**
// * @return User[] Returns an array of User objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('u')
// ->andWhere('u.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('u.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// Rewrote this function to return less data
public function getAllActiveUsers(): array{
$queryBuilder = $this->createQueryBuilder('u')
->select('u.surname', 'u.email', 'u.id', 'u.isActive', 'u.name', 'u.pictureUrl')
->where('u.isActive = :isActive')
->orderBy('u.surname', 'ASC');
$queryBuilder->setParameter('isActive', true);
return $queryBuilder->getQuery()->getResult();
}
// public function findOneBySomeField($value): ?User
// {
// return $this->createQueryBuilder('u')
// ->andWhere('u.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View File

@ -2,7 +2,10 @@
namespace App\Repository;
use App\Entity\User;
use App\Entity\UsersOrganizations;
use App\Entity\Organizations;
use App\Entity\Roles;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
@ -16,28 +19,153 @@ class UsersOrganizationsRepository extends ServiceEntityRepository
parent::__construct($registry, UsersOrganizations::class);
}
// /**
// * @return UsersOrganizations[] Returns an array of UsersOrganizations objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('u')
// ->andWhere('u.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('u.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
/**
* Find all distinct active organizations for a given user ID.
*
* @param int $userId
* @return UsersOrganizations[]
*/
public function findAllDistinctOrganizationsByUserId(int $userId): array
{
return $this->createQueryBuilder('uo')
->select('DISTINCT uo')
->leftJoin('uo.organization', 'o')
->leftJoin('uo.role', 'r')
->addSelect('o', 'r')
->where('uo.users = :userId', 'uo.isActive = :isActive')
->setParameter('userId', $userId)
->setParameter('isActive', true)
->getQuery()
->getResult();
}
// public function findOneBySomeField($value): ?UsersOrganizations
// {
// return $this->createQueryBuilder('u')
// ->andWhere('u.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
/**
* Find all organizations where a user (by email) has a specific role.
*
* @param string $userEmail
* @param string $roleName
* @return Organizations[]
*/
public function findOrganizationsByUserEmailAndRoleName(string $userEmail, string $roleName): array
{
$results = $this->createQueryBuilder('uo')
->innerJoin('uo.users', 'u')
->innerJoin('uo.organization', 'o')
->innerJoin('uo.role', 'r')
->where('u.email = :email')
->andWhere('r.name = :roleName')
->andWhere('uo.isActive = :isActive')
->andWhere('o.isActive = :orgIsActive') // Check if organization is active
->setParameter('email', $userEmail)
->setParameter('roleName', $roleName)
->setParameter('isActive', true)
->setParameter('orgIsActive', true) // Parameter for organization active status
->getQuery()
->getResult();
return array_map(fn($uo) => $uo->getOrganization(), $results);
}
/**
* Helper: Get all active UsersOrganizations links, optionally filtered by organizations.
*
* @param Organizations[]|null $organizations
* @return UsersOrganizations[]
*/
public function getAllActiveUserOrganizationLinks(array $organizations = null): array
{
$qb = $this->createQueryBuilder('uo')
->innerJoin('uo.organization', 'o')
->innerJoin('uo.users', 'u')
->where('uo.isActive = :isActive')
->setParameter('isActive', true);
if (!empty($organizations)) {
$qb->andWhere('o IN (:organizations)')
->setParameter('organizations', $organizations);
}
return $qb->getQuery()->getResult();
}
/**
* Get the last 10 new active users for a specific organization.
* Users are ordered by creation date (most recent first).
*
* @param Organizations $organization
* @return array
*/
public function getLastNewActiveUsersByOrganization(Organizations $organization): array
{
$results = $this->createQueryBuilder('uo')
->select('u.id', 'u.surname', 'u.name', 'u.email', 'u.pictureUrl', 'u.isActive', 'uo.createdAt')
->innerJoin('uo.users', 'u')
->innerJoin('uo.organization', 'o')
->where('uo.isActive = :isActive')
->andWhere('u.isActive = :userIsActive')
->andWhere('o.isActive = :orgIsActive')
->andWhere('uo.organization = :organization')
->setParameter('isActive', true)
->setParameter('userIsActive', true)
->setParameter('orgIsActive', true)
->setParameter('organization', $organization)
->orderBy('uo.createdAt', 'DESC')
->setMaxResults(10)
->getQuery()
->getResult();
// Remove duplicates by user ID (in case user has multiple roles)
$uniqueUsers = [];
foreach ($results as $result) {
$userId = $result['id'];
if (!isset($uniqueUsers[$userId])) {
$uniqueUsers[$userId] = $result;
}
}
return array_values($uniqueUsers);
}
/**
* Get all active admin users for a specific organization.
* Returns users who have the 'ADMIN' role in the given organization.
*
* @param Organizations $organization
* @return array
*/
public function getAdminUsersByOrganization(Organizations $organization): array
{
$results = $this->createQueryBuilder('uo')
->select('u.id', 'u.surname', 'u.name', 'u.email', 'u.pictureUrl', 'u.isActive')
->innerJoin('uo.users', 'u')
->innerJoin('uo.organization', 'o')
->innerJoin('uo.role', 'r')
->where('uo.isActive = :isActive')
->andWhere('u.isActive = :userIsActive')
->andWhere('o.isActive = :orgIsActive')
->andWhere('uo.organization = :organization')
->andWhere('r.name = :roleName')
->setParameter('isActive', true)
->setParameter('userIsActive', true)
->setParameter('orgIsActive', true)
->setParameter('organization', $organization)
->setParameter('roleName', 'ADMIN')
->orderBy('u.surname', 'ASC')
->getQuery()
->getResult();
// Remove duplicates by user ID (in case user has multiple admin-related roles)
$uniqueUsers = [];
foreach ($results as $result) {
$userId = $result['id'];
if (!isset($uniqueUsers[$userId])) {
$uniqueUsers[$userId] = $result;
}
}
return array_values($uniqueUsers);
}
}

View File

@ -25,12 +25,4 @@ class AccessTokenService
}
}
public function getUserFromToken(string $token)
{
$data = json_decode(base64_decode(strtr($token, '-_', '+/')), true);
if (isset($data['user_identifier'])) {
return $data['user_identifier'];
}
return null;
}
}

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

@ -1,23 +0,0 @@
<?php
namespace App\Service;
use League\Bundle\OAuth2ServerBundle\Model\Client;
use Doctrine\ORM\EntityManagerInterface;
class ClientService{
/**
* Retrieves a Client entity by its identifier.
*
* @param string $identifier The identifier of the client.
* @param EntityManagerInterface $entityManager The entity manager to use for database operations.
* @return Client|null The Client entity or null if not found.
*/
public function getClientIdentifier(String $identifier, EntityManagerInterface $entityManager): Client
{
return $entityManager->getRepository(Client::class)->findOneBy(['identifier' => $identifier]);
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace App\Service;
use App\Entity\Apps;
use App\Entity\Organizations;
use App\Entity\Roles;
use App\Entity\UsersOrganizations;
use Doctrine\ORM\EntityManagerInterface;
class OrganizationsService
{
public function __construct(private readonly EntityManagerInterface $entityManager)
{
}
public function getAdminUsers(Organizations $organization): array{
$roleAdmin = $this->entityManager->getRepository(Roles::class)->findBy(['name' => 'ADMIN']);
return $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['organization'=>$organization,
'role' => $roleAdmin,
'isActive' => true]);
}
public function getNewUsers(Organizations $organization): array{
return $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['organization'=>$organization,
'isActive' => true]);
}
/**
* Get all applications with organization access status
*
* @param Organizations $organization
* @return array
*/
public function getApplicationsWithAccessStatus(Organizations $organization): array
{
// Get all applications
$allApplications = $this->entityManager->getRepository(Apps::class)->findAll();
// Get applications the organization has access to
$organizationApps = $organization->getApps();
// Create a lookup array for faster checking
$orgAppIds = [];
foreach ($organizationApps as $app) {
$orgAppIds[$app->getId()] = true;
}
// Build result array
$result = [];
foreach ($allApplications as $app) {
$result[] = [
'application' => $app,
'has_access' => isset($orgAppIds[$app->getId()])
];
}
return $result;
}
}

View File

@ -0,0 +1,471 @@
<?php
namespace App\Service;
use App\Entity\Actions;
use App\Entity\Apps;
use App\Entity\Organizations;
use App\Entity\Roles;
use App\Entity\User;
use App\Service\UserService;
use App\Entity\UsersOrganizations;
use Doctrine\ORM\EntityManagerInterface;
/**
* Service pour la gestion des organisations d'utilisateurs.
* Fournit des méthodes pour récupérer, modifier et désactiver les rôles et applications d'un utilisateur dans une organisation.
*/
readonly class UserOrganizationService
{
/**
* Constructeur du service UserOrganizationService.
*
* @param EntityManagerInterface $entityManager Le gestionnaire d'entités Doctrine
*/
public function __construct(private readonly EntityManagerInterface $entityManager,
private readonly UserService $userService)
{
}
/**
* Récupère toutes les organisations auxquelles appartient l'utilisateur donné,
* incluant les rôles et applications uniques de l'utilisateur dans chaque organisation.
*
* @param User $user L'utilisateur concerné
* @param int|null $organizationsId ID optionnel pour filtrer par organisation
* @return array<array{organization:object, roles:Roles[], apps:object[], uoId:int}>
*/
public function getUserOrganizations(User $user, int $organizationsId = null): array
{
$userOrganizations = $this->entityManager
->getRepository(UsersOrganizations::class)
->findAllDistinctOrganizationsByUserId($user->getId());
$organizations = [];
foreach ($userOrganizations as $uo) {
$orgId = $uo->getOrganization()->getId();
// Si $organizationsId est fourni, ignorer les autres organisations
if ($organizationsId !== null && $orgId !== $organizationsId) {
continue;
}
// Initialiser l'entrée de l'organisation si elle n'existe pas
$organizations[$orgId] = $organizations[$orgId] ?? $this->createEmptyOrganizationBucket($uo);
$organizations[$orgId]['uoId'] = $uo->getId();
// Agréger les rôles et applications
$this->addRole($organizations[$orgId]['roles'], $uo->getRole());
$this->addApps($organizations[$orgId]['apps'], $uo->getApps());
}
// Ordonner les rôles : Super Admin, Admin, puis les autres
foreach ($organizations as &$org) {
$org['roles'] = $this->sortRoles($org['roles']);
}
unset($org);
$this->normalizeAppsIndexes($organizations);
return array_values($organizations);
}
/**
* Trie les rôles pour que Super Admin et Admin soient en premier, puis les autres.
*
* @param Roles[] $roles
* @return Roles[]
*/
private function sortRoles(array $roles): array
{
usort($roles, function ($a, $b) {
$priority = [
'SUPER_ADMIN' => 0,
'ADMIN' => 1
];
$aName = strtoupper($a->getName());
$bName = strtoupper($b->getName());
$aPriority = $priority[$aName] ?? 2;
$bPriority = $priority[$bName] ?? 2;
if ($aPriority === $bPriority) {
return strcmp($aName, $bName);
}
return $aPriority <=> $bPriority;
});
return $roles;
}
/**
* Initialise la structure de données pour une organisation.
*
* @param UsersOrganizations $link Lien utilisateur-organisation
* @return array{organization:object, roles:Roles[], apps:array<int,object>}
*/
private function createEmptyOrganizationBucket(UsersOrganizations $link): array
{
return [
'organization' => $link->getOrganization(),
'roles' => [],
'apps' => [],
];
}
/**
* Ajoute un rôle à la liste si non déjà présent (par ID).
*
* @param Roles[] &$roles Liste des rôles
* @param Roles|null $role Rôle à ajouter
* @return void
*/
private function addRole(array &$roles, ?Roles $role): void
{
if ($role === null) {
return;
}
foreach ($roles as $existingRole) {
if ($existingRole->getId() === $role->getId()) {
return; // Already present
}
}
$roles[] = $role;
}
/**
* Fusionne une ou plusieurs applications dans le tableau associatif, une entrée par ID.
*
* @param array<int,object> &$apps Tableau des applications
* @param iterable $appsToAdd Applications à ajouter
* @return void
*/
private function addApps(array &$apps, iterable $appsToAdd): void
{
foreach ($appsToAdd as $app) {
$apps[$app->getId()] = $apps[$app->getId()] ?? $app;
}
}
/**
* Normalise le tableau des applications pour le rendre indexé (JSON-friendly).
*
* @param array &$organizations Tableau des organisations
* @return void
*/
private function normalizeAppsIndexes(array &$organizations): void
{
foreach ($organizations as &$org) {
$org['apps'] = array_values($org['apps']);
}
}
/**
* Définit les rôles d'un utilisateur dans une organisation, en s'assurant que le rôle USER est toujours présent.
* Désactive tous les rôles si USER n'est pas sélectionné.
*
* @param User $user L'utilisateur
* @param Organizations $organization L'organisation
* @param array $selectedRoles Tableau des IDs de rôles sélectionnés
* @return void
* @throws \RuntimeException Si le rôle USER n'est pas trouvé
*/
public function setUserOrganizations(User $user, Organizations $organization, array $selectedRoles): void
{
$repo = $this->entityManager->getRepository(UsersOrganizations::class);
$roleRepo = $this->entityManager->getRepository(Roles::class);
$userRole = $roleRepo->findOneBy(['name' => 'USER']);
if (!$userRole) {
throw new \RuntimeException('USER role not found');
}
if (!in_array($userRole->getId(), $selectedRoles)) {
$this->deactivateAllUserRoles($user, $organization);
return;
}
$currentUserOrgs = $repo->findBy([
'users' => $user,
'organization' => $organization
]);
$currentRolesMap = $this->mapUserOrgRoles($currentUserOrgs);
$selectedRoles = $this->ensureUserRolePresent($selectedRoles, $userRole->getId());
$this->addOrUpdateRoles($selectedRoles, $currentRolesMap, $roleRepo, $user, $organization);
$this->deactivateUnselectedRoles($currentRolesMap);
$this->entityManager->flush();
}
/**
* Met à jour les applications associées à l'utilisateur dans une organisation.
*
* @param User $user L'utilisateur
* @param Organizations $organization L'organisation
* @param array $selectedApps Tableau des IDs d'applications sélectionnées
* @return void
*/
public function setUserOrganizationsApps(User $user, Organizations $organization, array $selectedApps): void
{
$roleUser = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'USER']);
$uoEntity = $this->entityManager
->getRepository(UsersOrganizations::class)
->findOneBy(['users' => $user, 'organization' => $organization, 'role' => $roleUser]);
if (!$uoEntity) {
return;
}
$this->removeUnselectedApps($uoEntity, $selectedApps);
$this->addSelectedApps($uoEntity, $selectedApps);
$this->entityManager->persist($uoEntity);
$this->entityManager->flush();
}
/**
* Crée une map des rôles actuels de l'utilisateur dans l'organisation.
* @param array $currentUserOrgs
* @return array
*/
private function mapUserOrgRoles(array $currentUserOrgs): array
{
$map = [];
foreach ($currentUserOrgs as $uo) {
$map[$uo->getRole()->getId()] = $uo;
}
return $map;
}
/**
* S'assure que le rôle USER est présent dans la sélection si nécessaire.
* @param array $selectedRoles
* @param int $userRoleId
* @return array
*/
private function ensureUserRolePresent(array $selectedRoles, int $userRoleId): array
{
$hasNonUserRole = false;
foreach ($selectedRoles as $roleId) {
if ($roleId !== $userRoleId) {
$hasNonUserRole = true;
break;
}
}
if ($hasNonUserRole && !in_array($userRoleId, $selectedRoles)) {
$selectedRoles[] = $userRoleId;
}
return $selectedRoles;
}
/**
* Ajoute ou réactive les rôles sélectionnés pour l'utilisateur dans l'organisation.
* @param array $selectedRoles
* @param array $currentRolesMap
* @param $roleRepo
* @param User $user
* @param Organizations $organization
*/
private function addOrUpdateRoles(array $selectedRoles, array &$currentRolesMap, $roleRepo, User $user, Organizations $organization): void
{
foreach ($selectedRoles as $roleId) {
if (!isset($currentRolesMap[$roleId])) {
$roleEntity = $roleRepo->find($roleId);
if ($roleEntity) {
$newUserOrganization = new UsersOrganizations();
$newUserOrganization->setUsers($user);
$newUserOrganization->setRole($roleEntity);
$newUserOrganization->setOrganization($organization);
$newUserOrganization->setIsActive(true);
$this->entityManager->persist($newUserOrganization);
}
} else {
$currentRolesMap[$roleId]->setIsActive(true);
$this->entityManager->persist($currentRolesMap[$roleId]);
}
unset($currentRolesMap[$roleId]);
}
}
/**
* Désactive les rôles non sélectionnés pour l'utilisateur dans l'organisation.
* @param array $currentRolesMap
*/
private function deactivateUnselectedRoles(array $currentRolesMap): void
{
foreach ($currentRolesMap as $uo) {
$uo->setIsActive(false);
$this->entityManager->persist($uo);
}
}
/**
* Retire les applications non sélectionnées de l'utilisateur dans l'organisation.
* @param UsersOrganizations $uoEntity
* @param array $selectedApps
*/
private function removeUnselectedApps(UsersOrganizations $uoEntity, array $selectedApps): void
{
foreach ($uoEntity->getApps()->toArray() as $existingApp) {
if (!in_array($existingApp->getId(), $selectedApps)) {
$uoEntity->removeApp($existingApp);
}
}
}
/**
* Ajoute les applications sélectionnées à l'utilisateur dans l'organisation.
* @param UsersOrganizations $uoEntity
* @param array $selectedApps
*/
private function addSelectedApps(UsersOrganizations $uoEntity, array $selectedApps): void
{
foreach ($selectedApps as $appId) {
$appEntity = $this->entityManager->getRepository(Apps::class)->find($appId);
if ($appEntity && !$uoEntity->getApps()->contains($appEntity)) {
$uoEntity->addApp($appEntity);
}
}
}
/**
* Désactive tous les rôles d'un utilisateur dans une organisation.
*
* @param User $user L'utilisateur
* @param Organizations $organization L'organisation
* @return void
*/
public function deactivateAllUserRoles(User $user, Organizations $organization): void
{
$repo = $this->entityManager->getRepository(UsersOrganizations::class);
$userOrganizations = $repo->findBy([
'users' => $user,
'organization' => $organization
]);
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();
}
/**
* Get all active users grouped by organization.
* Users with no organization are grouped under 'autre'.
*
* @return array
*/
public function getActiveUsersGroupedByOrganization(): array
{
$users = $this->entityManager->getRepository(User::class)->getAllActiveUsers();
$userOrgs = $this->entityManager->getRepository(UsersOrganizations::class)->getAllActiveUserOrganizationLinks();
$userToOrgs = $this->mapUserToOrganizations($userOrgs);
$orgs = [];
foreach ($users as $user) {
$userId = $user['id'];
if (isset($userToOrgs[$userId])) {
foreach ($userToOrgs[$userId] as $orgInfo) {
$orgId = $orgInfo['organization_id'];
if (!isset($orgs[$orgId])) {
$orgs[$orgId] = [
'organization_id' => $orgId,
'organization_name' => $orgInfo['organization_name'],
'users' => [],
];
}
// $orgs[$orgId]['users'][$userId] = $user;
$orgs[$orgId]['users'][$userId] = [
'users' => $user,
'is_connected' => $this->userService->isUserConnected($user['email'])
];
}
} else {
if (!isset($orgs['autre'])) {
$orgs['autre'] = [
'organization_id' => null,
'organization_name' => 'autre',
'users' => [],
];
}
$orgs['autre']['users'][$userId] = [
'users' => $user,
'is_connected' => $this->userService->isUserConnected($user['email'])
];
}
}
// Convert users arrays to indexed arrays
foreach ($orgs as &$org) {
$org['users'] = array_values($org['users']);
}
return array_values($orgs);
}
/**
* Get all active users for each organization in the given array.
*
* @param Organizations[] $organizations
* @return array
*/
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();
$orgId = $org->getId();
if (!isset($usersByOrg[$orgId])) {
$usersByOrg[$orgId] = [
'organization_id' => $orgId,
'organization_name' => $org->getName(),
'users' => [],
];
}
$user = $uo->getUsers();
$userId = $user->getId();
// Add connection status to user data
$usersByOrg[$orgId]['users'][$userId] = [
'users' => $user,
'is_connected' => $this->userService->isUserConnected($user->getUserIdentifier())
];
}
// Convert users arrays to indexed arrays
foreach ($usersByOrg as &$org) {
$org['users'] = array_values($org['users']);
}
return array_values($usersByOrg);
}
/**
* Helper: Map userId to their organizations (id and name), avoiding duplicates.
*
* @param UsersOrganizations[] $userOrgs
* @return array
*/
private function mapUserToOrganizations(array $userOrgs): array
{
$userToOrgs = [];
foreach ($userOrgs as $uo) {
$userId = $uo->getUsers()->getId();
$org = $uo->getOrganization();
$orgId = $org->getId();
$orgName = $org->getName();
$userToOrgs[$userId][$orgId] = [
'organization_id' => $orgId,
'organization_name' => $orgName,
];
}
return $userToOrgs;
}
}

View File

@ -0,0 +1,88 @@
<?php
namespace App\Service;
use App\Entity\Organizations;
use App\Entity\Roles;
use App\Entity\User;
use App\Entity\UsersOrganizations;
use Doctrine\ORM\EntityManagerInterface;
use League\Bundle\OAuth2ServerBundle\Model\AccessToken;
class UserService
{
public function __construct(private readonly EntityManagerInterface $entityManager)
{
// Constructor logic if needed
}
/**
* Generate a random password for a new user until they set their own.
*/
public function generateRandomPassword(): string{
$length = 50; // Length of the password
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+';
$charactersLength = strlen($characters);
$randomPassword = '';
for ($i = 0; $i < $length; $i++) {
$randomPassword .= $characters[rand(0, $charactersLength - 1)];
}
return $randomPassword;
}
/**
* Check if the user is an admin in the given organization.
*
* @param int $userId
* @param int $organizationId
* @return bool
*/
public function isUserAdminInOrganization(int $userId, int $organizationId): bool
{
$user = $this->entityManager->getRepository(User::class)->find($userId);
if (!$user) {
return false;
}
$organization = $this->entityManager->getRepository(Organizations::class)->find($organizationId);
if (!$organization) {
return false;
}
$roleAdmin = $this->entityManager->getRepository(Roles::class)->findBy(['name'=> 'ADMIN']);
// Check if the user is an admin in the organization
return empty($this->entityManager->getRepository(UsersOrganizations::class)->findBy([
'userId' => $userId,
'organizationId' => $organizationId,
'roleId' => $roleAdmin[0]->getId()]));
}
/**
* Check if the user is currently connected.
* This method check if the user is currently connected to one of the applications.
*
* @param String $userIdentifier
* @return bool
*/
public function isUserConnected(string $userIdentifier): bool
{
$now = new \DateTimeImmutable('now', new \DateTimeZone('Europe/Paris'));
$tokens = $this->entityManager->getRepository(AccessToken::class)->findBy([
'userIdentifier' => $userIdentifier,
'revoked' => false
]);
foreach ($tokens as $token) {
// Assuming $token->getExpiry() returns a DateTimeInterface
if ($token->getExpiry() > $now) {
return true;
}
}
return false;
}
}

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": {
@ -291,7 +306,8 @@
"assets/bootstrap.js",
"assets/controllers.json",
"assets/controllers/csrf_protection_controller.js",
"assets/controllers/hello_controller.js"
"assets/controllers/hello_controller.js",
"assets/controllers/igg_controller.js"
]
},
"symfony/translation": {
@ -380,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

@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>
{% block title %}
{{application}}
{% endblock %}
</title>
<link rel="icon" href="{{ asset('favicon.ico') }}">
@ -16,31 +17,31 @@
{% block javascripts %}
{% block importmap %}{{ importmap('app') }}{% endblock %}
<script>
const eventSource = new EventSource("{{ mercure('http://solutions-easy.moi/connect?userId='~ app.user.userIdentifier)|raw }}");
if (!sessionStorage.getItem('tabId')) {
sessionStorage.setItem('tabId', self.crypto.randomUUID ? self.crypto.randomUUID() : Math.random().toString(36).substr(2, 9));
}
const tabId = sessionStorage.getItem('tabId');
console.log('Tab ID:', tabId);
fetch('/register-tab', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ tabId: tabId })
});
</script>
{% endblock %}
</head>
<body>
<div class="container-scroller container-fluid">
<body data-application="{{application}}">
<div class="container-scroller">
{{ include('elements/navbar.html.twig')}}
<div class="container-fluid page-body-wrapper">
{{ include('elements/menu.html.twig')}}
<div class="main-panel">
<div class="content-wrapper">
{% block body %}
{% block body %}
{% endblock %}
{% endblock %}
</div>
<footer class="footer">
<div class="d-sm-flex justify-content-center justify-content-sm-between">
<span>
<img src="{{asset('img/sudalys_icone.png')}}" alt="logo sudalys" style="vertical-align:bottom"> Sudalys <i style="vertical-align:text-bottom">{{ ux_icon('material-symbols:copyright-outline', {height: '20px', width: '20px'}) }}</i> {{ "now"|date("Y") }}
</span>
<span>
Version {{version}}
</span>
</div>
</footer>
</div>
</div>
<div>
</body>
</html>

View File

@ -0,0 +1,11 @@
{% extends 'base.html.twig' %}
{% block title %}Page Not Found (404){% endblock %}
{% block body %}
<div class="d-flex justify-content-center align-items-center vh-100">
<h1>Oops! La page n'existe pas (404)</h1>
<p>La page que vous cherchez n'existe pas.</p>
<a href="{{ path('app_index') }}"> Retour à l'accueil </a>
</div>
{% endblock %}

View File

@ -0,0 +1,52 @@
<nav class="sidebar sidebar-offcanvas" id="sidebar">
<ul class="nav">
<li class="nav-item active">
<a class="nav-link" href="#">
<i class="icon-grid menu-icon">{{ ux_icon('material-symbols:dashboard-outline-rounded', {height: '16px', width: '16px'}) }}</i>
<span class="menu-title">Dashboard</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="collapse" href="#ui-basic" aria-expanded="false" aria-controls="ui-basic">
<i class="icon-layout menu-icon">{{ ux_icon('bi:menu-up', {height: '16px', width: '16px'}) }}</i>
<span class="menu-title">Menu</span>
<i class="menu-arrow">{{ ux_icon('bi:chevron-right', {height: '16px', width: '16px'}) }}</i>
</a>
<div class="collapse" id="ui-basic">
<ul class="nav sub-menu flex-column">
<li class="nav-item">{{ ux_icon('material-symbols-light:play-arrow-outline', {height: '16px', width: '16px'}) }} <a class="nav-link" href="#">Accordions</a></li>
<li class="nav-item">{{ ux_icon('material-symbols-light:play-arrow-outline', {height: '16px', width: '16px'}) }} <a class="nav-link" href="#">Buttons</a></li>
<li class="nav-item">{{ ux_icon('material-symbols-light:play-arrow-outline', {height: '16px', width: '16px'}) }} <a class="nav-link" href="#">Badges</a></li>
<li class="nav-item">{{ ux_icon('material-symbols-light:play-arrow-outline', {height: '16px', width: '16px'}) }} <a class="nav-link" href="#">Breadcrumbs</a></li>
<li class="nav-item">{{ ux_icon('material-symbols-light:play-arrow-outline', {height: '16px', width: '16px'}) }} <a class="nav-link" href="#">Dropdowns</a></li>
</ul>
</div>
</li>
{# if user is Super Admin#}
{% if is_granted('ROLE_ADMIN') %}
<li class="nav-item">
<a class="nav-link" href="{{ path('user_index') }}">
<i class="icon-grid menu-icon">{{ ux_icon('fa6-regular:circle-user', {height: '15px', width: '15px'}) }}</i>
<span class="menu-title">Users</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ path('organization_index') }}">
<i class="icon-grid menu-icon">
{% if is_granted('ROLE_SUPER_ADMIN')%}
{{ ux_icon('bi:buildings', {height: '15px', width: '15px'}) }}
{% else %}
{{ ux_icon('bi:building', {height: '15px', width: '15px'}) }}
{% endif %}
</i>
<span class="menu-title">
{% if is_granted('ROLE_SUPER_ADMIN') %}
Organizations
{% else %}
Organization
{% endif %}</span>
</a>
</li>
{% endif %}
</ul>
</nav>

View File

@ -1,13 +1,13 @@
<nav class="navbar col-lg-12 col-12 p-0 fixed-top d-flex flex-row">
<div class="text-center navbar-brand-wrapper d-flex align-items-center justify-content-start">
<a class="navbar-brand brand-logo" href="#"><img class="me-2" src="{{ asset('img/logo.png')}}" alt="logo application"/></a>
<a class="navbar-brand brand-logo-mini" href="#"><img src="{{ asset('img/logo-mini.png')}}" alt="logo apllication"/></a>
<a class="navbar-brand brand-logo" href="/"><img class="me-2" src="{{ asset('img/logo-'~application~'.png')}}" alt="logo application"/></a>
<a class="navbar-brand brand-logo-mini" href="/"><img src="{{ asset('img/logo-'~application~'-mini.png')}}" alt="logo apllication"/></a>
</div>
<div class="navbar-menu-wrapper d-flex align-items-center justify-content-end">
<button class="navbar-toggler navbar-toggler align-self-center">
<span>
<button class="navbar-toggler navbar-toggler align-self-center" type="button" data-toggle="minimize">
<i>
{{ ux_icon('bi:list', {height: '20px', width: '20px'}) }}
</span>
</i>
</button>
<ul class="navbar-nav ms-lg-3">
<li class="nav-item nav-search d-none d-lg-block">
@ -21,6 +21,21 @@
</div>
</li>
</ul>
<ul class="navbar-nav w-auto m-auto">
<li class="nav-item nav-search d-none d-lg-block">
<div class="input-group">
<div id="navbar-search-icon" class="input-group-prepend hover-cursor">
<span id="search">
<i style="width:22px; height:22px">{{ ux_icon('ix:project-arrow-diagonal-top-right', {height: '22px', width: '22px'}) }}</i>
</span>
</div>
<select id="change-project" class="form-control">
<option>Projet 1</option>
<option>Projet 2</option>
</select>
</div>
</li>
</ul>
<ul class="navbar-nav navbar-nav-right">
<li class="nav-item d-flex">
<img id="logo_orga" class="m-auto" src="{{asset('logo_org/logo-sudalys.png')}}" alt="logo organisation">
@ -79,5 +94,10 @@
</div>
</li>
</ul>
<button class="navbar-toggler navbar-toggler-right d-lg-none align-self-center" type="button" data-toggle="offcanvas">
<i>
{{ ux_icon('bi:list', {height: '20px', width: '20px'}) }}
</i>
</button>
</div>
</nav>

View File

@ -1,12 +1,13 @@
{% extends 'base.html.twig' %}
{% block title %}Test - index{% endblock %}
{% block title %}{{application}} - accueil{% endblock %}
{% block body %}
{% if app.user %}
<div class="mb-3">
<div class="w-100 h-100 p-5 m-auto">
You are logged in as {{ app.user.userIdentifier }}, <a href="{{ path('sso_logout') }}">Logout</a>
</div>
{% endif %}
{% endblock %}

View File

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

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

@ -0,0 +1,52 @@
{% extends 'base.html.twig' %}
{% block title %} Gestion des organisations {% endblock %}
{% block body %}
<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>
{% 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">
<thead class="table-light shadow-sm">
<tr>
<th>Logo</th>
<th>Nom</th>
<th>Email</th>
<th>Visualiser</th>
</tr>
</thead>
<tbody>
{% for organization in organizations %}
<tr>
<td>
{% if organization.logoUrl %}
<img src="{{ asset('uploads/logos/' ~ organization.logoUrl) }}" alt="Organization logo" class="rounded-circle" style="width:40px; height:40px;">
{% endif %}
</td>
<td>{{ organization.name }}</td>
<td>{{ organization.email }}</td>
<td>
<a href="{{ path('organization_show', {'id': organization.id}) }}" class="p-3 align-middle color-primary">
{{ ux_icon('fa6-regular:eye', {height: '30px', width: '30px'}) }}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
{% endblock %}

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 %}

Some files were not shown because too many files have changed in this diff Show More