Compare commits
67 Commits
main
...
dev/portai
| Author | SHA1 | Date |
|---|---|---|
|
|
993188ac4f | |
|
|
18dc5f8492 | |
|
|
f2166b604e | |
|
|
8d92d3f9fc | |
|
|
5ceed1f2f2 | |
|
|
450543fab7 | |
|
|
d543e69863 | |
|
|
7021b28163 | |
|
|
c55e9fa039 | |
|
|
bdf9f0478e | |
|
|
1053a2ab22 | |
|
|
6efbeb0fa2 | |
|
|
1ee9a0110b | |
|
|
cbdb47fb17 | |
|
|
e6c8d5a462 | |
|
|
7e272b2b2f | |
|
|
6670fbc8b8 | |
|
|
1e8d5e1eaf | |
|
|
2e99457e16 | |
|
|
cde6c529a9 | |
|
|
a3f993b858 | |
|
|
d2c20b9423 | |
|
|
89ed7049b9 | |
|
|
16dd919a5d | |
|
|
301f7bb445 | |
|
|
05d8ca0499 | |
|
|
e17e8e0eb2 | |
|
|
e6391279fe | |
|
|
cf16ec09a1 | |
|
|
943752a002 | |
|
|
a7e7298310 | |
|
|
3337b8c001 | |
|
|
6446eb2ce1 | |
|
|
a10b499522 | |
|
|
e77e92d39f | |
|
|
00e3003257 | |
|
|
f1b953d005 | |
|
|
4aadaa351a | |
|
|
fcb69f987f | |
|
|
d7677db885 | |
|
|
8eb5cf433d | |
|
|
fed351b433 | |
|
|
0a602fb52e | |
|
|
e87fdd32e4 | |
|
|
cfe89f58db | |
|
|
1d2debf364 | |
|
|
3271da59fa | |
|
|
d8df0bc1f4 | |
|
|
c3d3218bff | |
|
|
f24fb0180d | |
|
|
e360019e58 | |
|
|
8e38fe47db | |
|
|
b54fe41795 | |
|
|
798ca4ba07 | |
|
|
d43b516826 | |
|
|
c99b575814 | |
|
|
65ff838dd9 | |
|
|
4a2f9d9547 | |
|
|
10a8eb2255 | |
|
|
8f232498b8 | |
|
|
bbe50dbfd9 | |
|
|
436284c9c7 | |
|
|
228ef8cbe9 | |
|
|
c81142e4a5 | |
|
|
6a9b7568af | |
|
|
57e115a6a8 | |
|
|
79c5596766 |
5
.env
|
|
@ -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,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
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
37
README.MD
|
|
@ -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`
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 888 B |
|
After Width: | Height: | Size: 888 B |
|
|
@ -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 "";
|
||||
}
|
||||
|
|
@ -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,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);
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
@ -12,7 +12,8 @@ security:
|
|||
|
||||
role_hierarchy:
|
||||
ROLE_ADMIN: ROLE_USER
|
||||
ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
|
||||
ROLE_SUPER_ADMIN: [ROLE_ALLOWED_TO_SWITCH, ROLE_ADMIN]
|
||||
|
||||
|
||||
firewalls:
|
||||
dev:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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)\'');
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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)');
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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',
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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()]);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -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',
|
||||
]]);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?User
|
||||
// {
|
||||
// return $this->createQueryBuilder('u')
|
||||
// ->andWhere('u.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
// 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?UsersOrganizations
|
||||
// {
|
||||
// return $this->createQueryBuilder('u')
|
||||
// ->andWhere('u.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
@ -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'];
|
||||
}
|
||||
}
|
||||
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
21
symfony.lock
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
@ -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 %}
|
||||
|
||||
{% 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>
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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 %}
|
||||