Compare commits

...

430 Commits

Author SHA1 Message Date
Charles 0a4cb375e9 set up webhook for organization creation ( hard Coded ) 2026-03-09 10:07:16 +01:00
Charles 2dae055ae9 fix modal issue 2026-03-09 10:06:46 +01:00
Charles-Edouard MARGUERITE 91304d95dc Merge branch 'dev/user-bugfix' into 'develop'
Fix user creation bug

See merge request easy-solutions/apps/easyportal!52
2026-03-04 08:07:54 +00:00
Charles edf91ae01d Fix user creation bug 2026-03-04 09:06:33 +01:00
Charles-Edouard MARGUERITE 3200d05ed6 Merge branch 'dev/uoa-bugfix' into 'develop'
fix role not recognized

See merge request easy-solutions/apps/easyportal!51
2026-03-03 15:43:39 +00:00
Charles 5fea79cafa fix role not recognized 2026-03-03 16:43:08 +01:00
Mathis Buchet 32b42beb37 Merge branch 'fix' into 'develop'
Fix commented-out code in LoginSubscriber to enable access token creation

See merge request easy-solutions/apps/easyportal!50
2026-03-03 15:20:27 +00:00
mathis afc1b16dea Fix commented-out code in LoginSubscriber to enable access token creation 2026-03-03 16:19:00 +01:00
Charles-Edouard MARGUERITE 6a4f1f662e Merge branch 'dev/user-bugfix' into 'develop'
fix time issue on token

See merge request easy-solutions/apps/easyportal!49
2026-03-03 15:14:45 +00:00
Charles d603328585 fix time issue on token 2026-03-03 16:14:22 +01:00
Charles-Edouard MARGUERITE 195f841f8c Merge branch 'dev/user-bugfix' into 'develop'
Dev/user bugfix

See merge request easy-solutions/apps/easyportal!48
2026-03-03 15:01:16 +00:00
Charles 683766259c Fix using being able to be invited twice in the same org 2026-03-03 15:58:39 +01:00
Charles 45e14c47ca fix unrecognized field 2026-03-03 15:52:06 +01:00
Mathis Buchet a428cf92f3 Merge branch 'connection-process' into 'develop'
Enhance token revocation process to include refresh tokens and adjust token TTLs

See merge request easy-solutions/apps/easyportal!47
2026-03-03 14:43:10 +00:00
Charles-Edouard MARGUERITE e2ac177f65 Merge branch 'dev/UOA-bugfix' into 'develop'
fix unrecognized field on password setup

See merge request easy-solutions/apps/easyportal!46
2026-03-03 14:32:01 +00:00
Charles 5dffbeaa69 fix unrecognized field on password setup 2026-03-03 15:31:12 +01:00
mathis c7d3251545 Enhance token revocation process to include refresh tokens and adjust token TTLs 2026-03-03 15:26:07 +01:00
Charles-Edouard MARGUERITE ef7684f02b Merge branch 'dev/mailing-bugfix' into 'develop'
Fix 2 mail sending

See merge request easy-solutions/apps/easyportal!45
2026-03-03 14:17:24 +00:00
Charles 813c891477 Fix 2 mail sending 2026-03-03 15:15:45 +01:00
Charles-Edouard MARGUERITE 91f3e06dd5 Merge branch 'dev/tabulator-bugfix' into 'develop'
Dev/tabulator bugfix

See merge request easy-solutions/apps/easyportal!43
2026-03-03 14:06:55 +00:00
Charles 8892cb3a7f fix tabulator name field for orgs 2026-03-03 15:05:46 +01:00
Charles 0c758a9370 fix tabulator name field 2026-03-03 14:12:45 +01:00
Charles a93b94ba7b Normalize name and surname 2026-03-03 14:12:24 +01:00
Charles d5e1ad057e Ajax call to create + edit organizations 2026-03-03 11:29:34 +01:00
Mathis Buchet 4f7c1eb5de Merge branch 'refonte-disconnect' into 'develop'
Refonte disconnect

See merge request easy-solutions/apps/easyportal!42
2026-02-27 13:03:17 +00:00
mathis 7133e76561 Implement multi-application redirect handling using redirect_app parameter 2026-02-27 14:01:23 +01:00
mathis 2d72515f0c Update login handling to allow default redirection and adjust security settings 2026-02-27 13:53:53 +01:00
Mathis Buchet 25a477a8f9 Merge branch 'disconnect' into 'develop'
Implement EasyCheck logout handling and session redirection

See merge request easy-solutions/apps/easyportal!41
2026-02-27 11:12:19 +00:00
mathis cb4a262e89 Implement EasyCheck logout handling and session redirection 2026-02-27 12:10:11 +01:00
Mathis Buchet 443c7d67b3 Merge branch 'fix' into 'develop'
update EasyCheck URL in environment configuration

See merge request easy-solutions/apps/easyportal!40
2026-02-27 10:48:39 +00:00
mathis 089e644d14 update EasyCheck URL in environment configuration 2026-02-27 11:48:01 +01:00
Mathis Buchet 4b89d1a256 Merge branch 'fix-disconnect' into 'develop'
Revert "refactor logout handling to retrieve EasyCheck URL dynamically from database"

See merge request easy-solutions/apps/easyportal!39
2026-02-27 10:31:50 +00:00
mathis 0db330e354 update EasyCheck URL in environment configuration 2026-02-27 11:31:19 +01:00
mathis e545020f42 update EasyCheck URL in environment configuration 2026-02-27 11:28:20 +01:00
mathis 2de17b12f0 Revert "refactor logout handling to retrieve EasyCheck URL dynamically from database"
This reverts commit 55606d43d5.
2026-02-27 11:27:31 +01:00
Mathis Buchet a0db7f64a8 Merge branch 'fix-disconnect' into 'develop'
refactor logout handling to retrieve EasyCheck URL dynamically from database

See merge request easy-solutions/apps/easyportal!38
2026-02-27 10:09:51 +00:00
mathis 55606d43d5 refactor logout handling to retrieve EasyCheck URL dynamically from database 2026-02-27 11:08:09 +01:00
Mathis Buchet 4cb447d4ad Merge branch 'fix-disconnect' into 'develop'
Fix disconnect

See merge request easy-solutions/apps/easyportal!37
2026-02-27 09:39:05 +00:00
mathis 1d6b9f08d3 add SSO/SLO documentation for EasyPortal and EasyCheck integration 2026-02-27 10:31:48 +01:00
mathis 5abbd15b45 add logout subscriber and update SSO logout handling 2026-02-27 10:05:00 +01:00
mathis d50a6bd238 implement logout functionality and improve SSO logout process 2026-02-26 17:03:51 +01:00
Mathis Buchet 8f35520311 Merge branch 'cmd-crea-user' into 'develop'
add command to create super admin user

See merge request easy-solutions/apps/easyportal!36
2026-02-26 08:45:38 +00:00
Charles 2609f41a7c solve empty array error 2026-02-25 16:28:31 +01:00
Charles 716c7d03c1 correct link 2026-02-25 16:19:50 +01:00
Charles 54583185f0 update redirect link 2026-02-25 16:12:45 +01:00
mathis c485740d27 add command to create super admin user 2026-02-25 14:15:02 +01:00
Charles-Edouard MARGUERITE cf90f97f01 Merge branch 'dev/api/feature' into 'develop'
Dev/api/feature

See merge request easy-solutions/apps/easyportal!35
2026-02-25 12:50:51 +00:00
Charles 625ecafda8 Case sensitive search 2026-02-25 13:49:43 +01:00
Charles b0ce17e335 upgrade readme 2026-02-25 11:06:11 +01:00
Charles-Edouard MARGUERITE 3f57b549a5 Merge branch 'dev/api/feature' into 'develop'
upgrade firebase version

See merge request easy-solutions/apps/easyportal!34
2026-02-25 09:31:56 +00:00
Charles 78e770235e upgrade firebase version 2026-02-25 10:30:14 +01:00
Charles-Edouard MARGUERITE 698caebaea Merge branch 'dev/api/feature' into 'develop'
Dev/api/feature

See merge request easy-solutions/apps/easyportal!33
2026-02-25 09:19:51 +00:00
Charles cad6a4f370 apply changes for api calls 2026-02-25 09:21:06 +01:00
Charles 9f430a3656 add information on oauth2/userinfo 2026-02-25 09:20:41 +01:00
Charles f89bd101fe Update to match new role logic 2026-02-23 11:55:08 +01:00
Charles 4ac179f7b6 Create org on remote call 2026-02-23 10:30:21 +01:00
Charles 829469b1c5 Change name for futureProofing 2026-02-23 10:05:39 +01:00
Charles 406ff27e27 change to https 2026-02-23 09:58:09 +01:00
Charles f08bf51c70 dynamic remote project CRUD 2026-02-23 09:57:06 +01:00
Charles 6569af4720 dynamic self client identification for portal 2026-02-18 16:43:32 +01:00
Charles e388999ff7 update doc 2026-02-18 16:33:27 +01:00
Charles b2bb9fc78b set up doc for API 2026-02-18 16:27:29 +01:00
Charles c0a8a9ab82 set up edit for project in api 2026-02-18 15:38:52 +01:00
Charles 782ca27b5e dynamic sso client data 2026-02-18 12:13:50 +01:00
Charles e941363ca6 dynamic sso client data 2026-02-18 12:13:44 +01:00
Charles 4b92e83f15 dynamic sso client data 2026-02-18 12:13:36 +01:00
Charles e50bb0402a Set up api calls 2026-02-18 11:35:44 +01:00
Charles b9b0efd6c6 enabled ajax function to edit user 2026-02-17 14:32:33 +01:00
Charles c2ea41f0a1 format enty data 2026-02-17 11:21:52 +01:00
Charles a893c09fcf changed creating logic to modal 2026-02-17 11:02:47 +01:00
Charles 72b40e965a renamed entity 2026-02-16 16:52:12 +01:00
Charles-Edouard MARGUERITE 3765b8c314 Merge branch 'dev/org/feature' into 'develop'
Add/Remove admin from orgs

See merge request easy-solutions/apps/easyportal!32
2026-02-16 15:03:49 +00:00
Charles 3df22c2dbf Add/Remove admin from orgs 2026-02-16 15:58:44 +01:00
Charles-Edouard MARGUERITE 056325bcf3 Merge branch 'doc' into 'develop'
Doc

See merge request easy-solutions/apps/easyportal!31
2026-02-16 13:28:49 +00:00
Charles 57e7ec8181 set up doc for organization 2026-02-16 14:12:31 +01:00
Charles 2626d27288 set up project CRUD 2026-02-16 13:58:15 +01:00
Charles 3e06a348ff revert on bug 2026-02-11 16:16:32 +01:00
Charles f73723f42b revert on bug 2026-02-11 15:55:17 +01:00
Charles c6833232a0 Generate random prefix for organization projects 2026-02-11 15:45:39 +01:00
Charles f1d219544b update logic to fit new role rework 2026-02-11 15:24:26 +01:00
Charles e536a5ebc5 update logic to fit new role rework 2026-02-11 15:22:11 +01:00
Charles 42bee789ba Removed dead code 2026-02-11 15:16:25 +01:00
Charles d089815069 Update role logic for action display 2026-02-11 15:13:47 +01:00
Charles 4fc059b2a5 Update role logic for organization management 2026-02-11 15:11:04 +01:00
Charles 252fc775bb Dynamically display Organization Tab 2026-02-11 14:39:15 +01:00
Charles f8ba879cc9 Correct bug where page didn't get proper informations 2026-02-11 14:36:43 +01:00
Charles 184bfa2604 update index of org page load logic 2026-02-11 14:34:29 +01:00
Charles fe6e4b44e5 correct role logic 2026-02-11 13:58:37 +01:00
Charles 35bad9eca5 Update tooltip 2026-02-11 12:20:20 +01:00
Charles 64317c226d Update logic 2026-02-11 12:17:16 +01:00
Charles 6a286bff0a moved logic to repository 2026-02-11 12:01:42 +01:00
Charles 8587798619 remove useless code 2026-02-11 11:59:46 +01:00
Charles 5db11384e5 remove deprecated code 2026-02-11 11:58:05 +01:00
Charles 2fce2dd8a5 Removed useless code 2026-02-11 11:56:44 +01:00
Charles e64afa87db New role logic to activate/deactive user in organization 2026-02-11 11:50:47 +01:00
Charles 7d1e2ee4e7 Dynamic display of activation status 2026-02-11 11:07:33 +01:00
Charles 934720b85a ajax request to activate/-/deactive user 2026-02-11 10:54:53 +01:00
Charles d434fecaa5 Update access logic on activate/-/deactive user 2026-02-11 10:32:38 +01:00
Charles 62107aabd2 Update access logic on /user/new 2026-02-11 09:43:21 +01:00
Charles 5a304a8004 Add role doc 2026-02-10 16:03:29 +01:00
Charles db3a59b389 change user view 2026-02-10 16:02:15 +01:00
Charles 709a9f44cb adapt logic to new structure 2026-02-10 16:01:59 +01:00
Charles a9493bfb0f update UO entity to handle roles 2026-02-10 16:01:43 +01:00
Charles-Edouard MARGUERITE 6a147cf6dd Merge branch 'dev/feature' into 'develop'
add url

See merge request easy-solutions/apps/easyportal!30
2026-02-10 11:03:31 +00:00
Charles 20797ada9b add url 2026-02-10 12:03:05 +01:00
Charles-Edouard MARGUERITE c9f7d5f317 Merge branch 'dev/feature' into 'develop'
change to dynamic ur

See merge request easy-solutions/apps/easyportal!29
2026-02-10 10:49:42 +00:00
Charles 455d71693d change to dynamic ur 2026-02-10 11:49:04 +01:00
Charles-Edouard MARGUERITE 133d9b9741 Merge branch 'dev/organization/picture-fixture-2' into 'develop'
update tests

See merge request easy-solutions/apps/easyportal!28
2026-02-04 14:48:31 +00:00
Charles 856e51ff09 update tests 2026-02-04 15:42:55 +01:00
Charles-Edouard MARGUERITE d5c93fef8c Merge branch 'dev/organization/picture-fixture-2' into 'develop'
Dev/organization/picture fixture 2

See merge request easy-solutions/apps/easyportal!26
2026-02-02 15:25:59 +00:00
Charles 2502baf265 solved trailing "/" issue 2026-02-02 16:25:39 +01:00
Charles 0f381af573 Solved no uoa link issue 2026-02-02 16:23:47 +01:00
Charles 901e83e87a reduce action number 2026-02-02 16:14:39 +01:00
Charles-Edouard MARGUERITE 8317617288 Merge branch 'docs' into 'develop'
Docs

See merge request easy-solutions/apps/easyportal!25
2026-02-02 15:08:00 +00:00
Charles 79cba858dd Added doc for client setup 2026-02-02 16:07:21 +01:00
Charles e0d2457e26 add grid like view to the available apps 2026-02-02 15:51:49 +01:00
Charles 8aa1dfbfff remove S3 path 2026-02-02 14:47:20 +01:00
Charles f88495b3f9 Added label 2026-02-02 14:45:04 +01:00
Charles c62f50c2f4 menu dynamic 2026-02-02 14:44:46 +01:00
Charles-Edouard MARGUERITE 0c7968775c Merge branch 'dev/ui/update-1' into 'develop'
test activity org

See merge request easy-solutions/apps/easyportal!24
2026-02-02 13:18:27 +00:00
Charles ca891f434c test activity org 2026-02-02 14:17:45 +01:00
Charles-Edouard MARGUERITE 393adc461d Merge branch 'dev/ui/bugfix-1' into 'develop'
solve image not loading

See merge request easy-solutions/apps/easyportal!23
2026-02-02 10:01:10 +00:00
Charles 0e698be3d1 solve image not loading 2026-02-02 10:59:03 +01:00
Charles-Edouard MARGUERITE a20c1cd1fb Merge branch 'dev/organization/bugfix-2' into 'develop'
fix organization not loading

See merge request easy-solutions/apps/easyportal!22
2026-02-02 08:42:52 +00:00
Charles bcb0cbc9b9 fix organization not loading 2026-02-02 09:42:15 +01:00
Charles-Edouard MARGUERITE e7206d09e3 Merge branch 'dev/organization/bugfix-1' into 'develop'
fix organization not loading

See merge request easy-solutions/apps/easyportal!21
2026-02-02 08:23:55 +00:00
Charles 0a88ad0bde fix organization not loading 2026-02-02 09:10:30 +01:00
Charles-Edouard MARGUERITE 03d9cf5392 Merge branch 'dev/application/feature-1' into 'develop'
update logo path

See merge request easy-solutions/apps/easyportal!20
2026-02-02 07:49:00 +00:00
Charles cc98e539c7 update logo path 2026-02-02 08:47:05 +01:00
Charles-Edouard MARGUERITE 0d835573e8 Merge branch 'dev/organization/bugfix-1' into 'develop'
bugfix

See merge request easy-solutions/apps/easyportal!19
2026-01-28 15:43:40 +00:00
Charles 281e58c4a7 bugfix 2026-01-28 16:41:16 +01:00
Charles-Edouard MARGUERITE 0549494786 Merge branch 'dev/organization/bugfix-1' into 'develop'
solve org error

See merge request easy-solutions/apps/easyportal!17
2026-01-28 15:28:45 +00:00
Charles bc730155ba solve org error 2026-01-28 16:27:22 +01:00
Charles-Edouard MARGUERITE fef74fea64 Merge branch 'dev/user/update-1' into 'develop'
Dev/user/update 1

See merge request easy-solutions/apps/easyportal!16
2026-01-28 15:19:47 +00:00
Charles 99a68dad4d deactivate user on creation 2026-01-28 16:18:39 +01:00
Charles a41c5095b5 correct form method 2026-01-28 16:15:30 +01:00
Charles-Edouard MARGUERITE ac532dcf89 Merge branch 'mercure/setup' into 'develop'
setup mercure

See merge request easy-solutions/apps/easyportal!15
2026-01-28 15:11:42 +00:00
Charles 5b137b71b2 setup mercure 2026-01-28 16:10:28 +01:00
Charles-Edouard MARGUERITE f42accdbaa Merge branch 'ui/fixture-1' into 'develop'
Visual update

See merge request easy-solutions/apps/easyportal!14
2026-01-28 14:59:25 +00:00
Charles e6b575afd8 Visual update 2026-01-28 15:51:46 +01:00
Charles-Edouard MARGUERITE 0d3da8dfdf Merge branch 'dev/mailing/update-1' into 'develop'
change expiry time

See merge request easy-solutions/apps/easyportal!13
2026-01-28 14:38:05 +00:00
Charles 2bf438cb27 change expiry time 2026-01-28 15:36:08 +01:00
Charles-Edouard MARGUERITE 7dc369ee67 Merge branch 'dev/mailing/bugfix-3' into 'develop'
resend correct mail to existing user

See merge request easy-solutions/apps/easyportal!12
2026-01-28 13:18:22 +00:00
Charles df4363dd37 resend correct mail to existing user 2026-01-28 14:15:17 +01:00
Charles-Edouard MARGUERITE 2c7402249d Merge branch 'dev/mailing/bugfix-2' into 'develop'
update mail sender

See merge request easy-solutions/apps/easyportal!11
2026-01-28 10:44:07 +00:00
Charles 6403048e44 update mail sender 2026-01-28 11:43:03 +01:00
Charles-Edouard MARGUERITE 63396cafe6 Merge branch 'dev/user/clean-up-1' into 'develop'
Dev/user/clean up 1

See merge request easy-solutions/apps/easyportal!10
2026-01-28 10:30:00 +00:00
Charles 4b3a960e59 small word changes 2026-01-28 11:24:21 +01:00
Charles ce82296fa6 solved logger error bug 2026-01-28 11:07:31 +01:00
Charles-Edouard MARGUERITE 85e3de647c Merge branch 'dev/organization/picture-feature-1' into 'develop'
Dev/organization/picture feature 1

See merge request easy-solutions/apps/easyportal!9
2026-01-28 09:28:26 +00:00
Charles 3a610ba4d7 removed S3 and handles bugs that came with it 2026-01-28 10:26:32 +01:00
Charles 59bc78fe47 removed S3 and handles bugs that came with it 2026-01-28 10:08:55 +01:00
Charles-Edouard MARGUERITE 3d23e9cec3 Merge branch 'dev/ui/menu-feature-1' into 'develop'
remove unused code

See merge request easy-solutions/apps/easyportal!8
2026-01-27 10:36:52 +00:00
Charles 01a35c75a6 remove unused code 2026-01-27 11:33:02 +01:00
Charles-Edouard MARGUERITE d42dde624b Merge branch 'dev/organization/name-feature-1' into 'develop'
drop not null on org name

See merge request easy-solutions/apps/easyportal!7
2026-01-27 09:25:37 +00:00
Charles 9f619b5293 Drop not null on Org name 2026-01-27 10:23:18 +01:00
Charles 4c3c8c3043 resolve conflict 2026-01-27 09:49:19 +01:00
Charles-Edouard MARGUERITE 2c346c0484 Merge branch 'dev/portailV2' into 'develop'
update csrf token

See merge request easy-solutions/apps/easyportal!5
2026-01-26 15:20:31 +00:00
Charles ce4f045c00 update csrf 2026-01-26 16:18:43 +01:00
Charles-Edouard MARGUERITE 339d83cae2 Delete public.key 2026-01-26 14:58:27 +00:00
Charles-Edouard MARGUERITE 8fa42c6d1b Delete private.key 2026-01-26 14:58:20 +00:00
Charles-Edouard MARGUERITE 89c8ae5f4d Merge branch 'dev/portailV2' into 'develop'
update version

See merge request easy-solutions/apps/easyportal!4
2026-01-26 14:30:54 +00:00
Charles a6aba4f7d5 update version 2026-01-26 15:29:56 +01:00
Charles-Edouard MARGUERITE 0ad22cc465 Merge branch 'dev/portailV2' into 'develop'
update user checker

See merge request easy-solutions/apps/easyportal!3
2026-01-26 13:05:07 +00:00
Charles 940361ab4b update user checker 2026-01-26 14:03:33 +01:00
Charles df9f102ecf don't perform user check if SUPER ADMIN 2026-01-26 13:58:38 +01:00
Qada 64ebaa4e05 Merge branch 'dockerize-portal' into 'develop'
Dockerize portal

See merge request easy-solutions/apps/easyportal!2
2026-01-21 15:33:51 +00:00
qadamscqueezy 3113313ad3 Merge branch 'develop' into dockerize-portal 2026-01-21 16:33:37 +01:00
Charles a1b92aebce added flash to files 2026-01-21 16:08:21 +01:00
Charles 01f73c2ef4 Test for User Controller 2026-01-21 10:01:13 +01:00
qadamscqueezy 29c04cd843 add deployment and notification stages to CI pipeline 2026-01-19 22:58:05 +01:00
qadamscqueezy 07fbb7af2c update caddy file 2026-01-15 20:36:31 +01:00
Charles d26d1cb118 Organization Controller Tests 2026-01-06 14:29:29 +01:00
Charles 68864b3997 Correct organization display logic 2025-12-22 10:04:11 +01:00
Charles 08ed90a7dc Test on notification controller 2025-12-19 15:45:23 +01:00
Charles 9cc8dc83f3 Test on index controller 2025-12-19 14:05:42 +01:00
Charles 86ef5fa6f6 Test on index controller 2025-12-19 11:14:48 +01:00
Charles 2d0eddaf51 test on app controller 2025-12-18 16:43:48 +01:00
Charles 0ea4829940 60 sec between refresh 2025-12-18 15:32:26 +01:00
Charles cb8eabef4d set up controller test for action 2025-12-15 15:48:27 +01:00
Charles 923d36ba4e Adapt action test to previous refactor 2025-12-15 14:24:09 +01:00
Charles 12f2b39ccd added rate limiter for log in 2025-12-15 14:16:14 +01:00
Charles 0b8890e3d7 typo 2025-12-15 13:53:19 +01:00
Charles b5d56f1d85 put activities in an ajax call 2025-12-15 13:52:54 +01:00
Charles 9af81b1d2c fix actions displaying incorrectly 2025-12-15 11:00:13 +01:00
Charles cdd61123ea solve refactor problem 2025-12-15 10:17:09 +01:00
Charles 271c2e31d1 refactor user service 2025-12-15 09:03:14 +01:00
Charles 87ecf70d95 wrapped potential error in try catch 2025-12-10 12:07:20 +01:00
Charles 07bd064faa update pwd gen for better security 2025-12-10 12:04:10 +01:00
Charles 76b3af7f2e Test for AccessToken service 2025-12-10 11:53:51 +01:00
Charles ec561ef0a1 Test for action service 2025-12-10 11:49:15 +01:00
Charles eeb82277f7 Test for AWS service 2025-12-10 11:41:15 +01:00
Charles 2a09564323 Test for CguService 2025-12-10 09:38:21 +01:00
Charles 8045ff03c8 Test for emailService 2025-12-10 08:48:54 +01:00
Charles 14366b5ed4 Test for Logger Service 2025-12-09 16:48:42 +01:00
Charles 70ef717506 Test for notification Service 2025-12-09 16:45:31 +01:00
Charles 78583620fa Correct monolog behavior 2025-12-09 16:31:33 +01:00
Charles fdf52465fe Organization service test 2025-12-09 16:31:20 +01:00
Charles 30901335d9 userOrganizationApp service test 2025-12-09 15:45:35 +01:00
Charles 0df623ba17 userOrganization service test 2025-12-09 15:25:46 +01:00
Charles 55c42c81fa user service test 2025-12-09 15:22:35 +01:00
Charles 0cd33e84f8 Refactor monolog of Application controller 2025-12-09 11:58:16 +01:00
Charles 4022e905a8 Refactor monolog of OAuth2controller controller 2025-12-09 11:47:59 +01:00
Charles 6b4ad1d6fd Refactor monolog of organization controller 2025-12-09 11:40:20 +01:00
Charles 530c7df5e2 correct writting level 2025-12-09 10:35:40 +01:00
Charles c47d7877bb Display correct information 2025-12-09 10:21:01 +01:00
Charles 88e9c6db6a solve security access issue 2025-12-09 10:20:50 +01:00
Charles 79ef977e1b removed unused button 2025-12-09 10:06:48 +01:00
Charles 3f9d388f7f Refactor for monolog in security controller 2025-12-09 09:40:13 +01:00
Charles 5f4336d824 Refactor for monolog in user controller 2025-12-08 16:27:55 +01:00
qadamscqueezy 6c8cc37313 add missing .env 2025-12-08 15:33:02 +01:00
qadamscqueezy b09544eb71 update .env 2025-12-08 15:25:16 +01:00
qadamscqueezy 361fbc1ebe add new variable 2025-12-08 15:15:48 +01:00
qadamscqueezy 321bfd883a update composer file 2025-12-08 15:05:34 +01:00
qadamscqueezy c528407991 dockerize portal 2025-12-08 14:35:06 +01:00
Charles 659eb08d6e refactor for monolog user activateStatus 2025-12-03 14:38:03 +01:00
Charles 3c789dc68e refactor for monolog user activateStatus 2025-12-03 14:33:28 +01:00
Charles 47724734a2 refactor for monolog user create 2025-12-03 07:58:21 +01:00
Charles cb34a18948 update error handling monolog.yaml 2025-12-01 15:52:07 +01:00
Charles fe0432cef1 refactor 2025-12-01 14:17:27 +01:00
Charles bf602143a2 added custom monolog to view user function 2025-12-01 14:16:00 +01:00
Charles 9c07542c1c added custom monolog to edit user function 2025-12-01 14:15:10 +01:00
Charles 2bf48de23a removed formatter 2025-12-01 14:12:37 +01:00
Charles b0da189fa2 modify type : stream -> rotating_file 2025-12-01 14:02:45 +01:00
Charles f54f953029 Add error log and phph error log 2025-12-01 13:53:01 +01:00
Charles b4a68bcc4d set max fdiles to 30 2025-12-01 13:52:47 +01:00
Charles 1d8a4a7215 Setup monolog channels 2025-12-01 13:44:53 +01:00
Charles d8934e2cf5 format user data on form submit 2025-12-01 11:13:35 +01:00
Charles c673fcd83b Fused activate and deactived user for organization route 2025-11-26 09:20:05 +01:00
Charles f0ae5a8c8a Fused activate and deactived user route 2025-11-25 16:57:02 +01:00
Charles f6a2159177 Refactor new user function 2025-11-25 16:02:10 +01:00
Charles 9513067ac5 Removed unused variables 2025-11-25 15:48:19 +01:00
Charles 52b8ba0a10 Removed unused functions 2025-11-25 15:46:15 +01:00
Charles 70842d6fe9 corrected redirection logic 2025-11-25 15:26:22 +01:00
Charles 7da97b0d02 Corrected UO link deactivation logic 2025-11-25 15:11:36 +01:00
Charles f09dd20d2b Removed debugging console log 2025-11-25 15:04:13 +01:00
Charles 94c0fd7c42 Added remote filtering 2025-11-25 15:03:57 +01:00
Charles 9d541410c4 Added remote filtering to users in organization dashboard 2025-11-25 14:43:59 +01:00
Charles a24849abe3 uniformisation des cartes 2025-11-24 16:10:08 +01:00
Charles 83c4c221af uniformisation des cartes 2025-11-24 16:01:19 +01:00
Charles 9d394c34f4 Refactor user view 2025-11-24 15:53:19 +01:00
Charles 04b8b26d65 Correct bug due to refactor 2025-11-24 12:05:38 +01:00
Charles 82c7c15068 Correct Role names display 2025-11-24 08:56:59 +01:00
Charles 9d37a7c549 Correct mercure token route 2025-11-24 08:53:48 +01:00
Charles d884ff4155 correction visuel 2025-11-19 17:27:35 +01:00
Charles 7e08998005 gestion des roles et applications 2025-11-19 16:58:49 +01:00
Charles 8c9a5da604 remove useless file 2025-11-18 10:54:58 +01:00
Charles f2123e911d Updated notification style 2025-11-17 16:43:40 +01:00
Charles 9dc97c5843 add dynamic notification with mercure 2025-11-17 16:11:35 +01:00
Charles 3744d81035 update project to allow sending of email 2025-11-17 11:50:34 +01:00
Charles a6fdb59521 refactor 2025-11-05 15:02:21 +01:00
Charles f6ce0e6229 en -> fr 2025-11-04 11:16:42 +01:00
Charles 7db986468c Added logo to bucket 2025-11-04 10:34:56 +01:00
Charles 53c3180d33 Added logo to bucket 2025-11-04 09:39:18 +01:00
Charles 8193e339b0 Added log to create user 2025-10-29 11:18:15 +01:00
Charles 346a05e51d start notification/mailing process 2025-10-29 10:34:05 +01:00
Charles 75e5921be1 added statut to user organization link 2025-10-29 10:09:28 +01:00
Charles 2d84ee8ec4 Refactor tabulator fr 2025-10-29 09:37:43 +01:00
Charles cb7afab382 update index page 2025-10-29 09:29:51 +01:00
Charles 00ed7ef491 Added new user in organization 2025-10-29 09:25:02 +01:00
Charles b974b56a17 remove "project" select 2025-10-28 10:29:37 +01:00
Charles 5f50584f0d correction bug remove all roles 2025-10-27 16:16:22 +01:00
Charles 6aacf0cefc remove "gerer l'application" 2025-10-27 16:07:53 +01:00
Charles e6068fd538 bug correction 2025-10-27 16:04:52 +01:00
Charles a219f0f067 Added dynamic filtering 2025-10-27 15:57:14 +01:00
Charles ec3fc7f5ca refactor 2025-10-27 15:34:45 +01:00
Charles aee352924e activate/deactivate user from index table 2025-10-27 15:30:50 +01:00
Charles 83da6d0be4 refresh row on activate/deactivate user 2025-10-27 14:56:45 +01:00
Charles afac1467fa Added Active/Inactive badge 2025-10-27 14:35:38 +01:00
Charles 519556d35e Only display active new user 2025-10-27 14:35:08 +01:00
Charles e4f63c9b85 removed deactivateAllUserOrganizationLinks 2025-10-27 14:06:50 +01:00
Charles 772b920a44 added check on login 2025-10-27 14:01:50 +01:00
Charles c54df8a327 Typed controller route 2025-10-27 13:54:26 +01:00
Charles 2418e43703 Typed controller route 2025-10-27 12:56:31 +01:00
Charles 36fe5f5588 change redirect to http 204 2025-10-27 12:53:13 +01:00
Charles 003ee40992 refactor 2025-10-27 12:20:03 +01:00
Charles b430e13e3b Deny access to app if user is deleted 2025-10-27 11:25:09 +01:00
Charles 2d7adf20ec revoke user token if he is deleted 2025-10-27 11:10:32 +01:00
Charles 5a39804dd4 remove todo 2025-10-27 11:04:51 +01:00
Charles 9a51c2d86f Remove repeating text 2025-10-27 11:04:23 +01:00
Charles 3680621fcc added deactivate button in organization dashboard 2025-10-27 10:50:45 +01:00
Charles e1659accab added deleted button in tables 2025-10-27 08:37:08 +01:00
Charles 016c415c11 gestion droit d'access au application pour les compagnies 2025-10-22 11:27:44 +02:00
Charles 2b9b030d9a gestion droit d'access 2025-10-21 16:45:02 +02:00
Charles bb959a1ac1 petite refonte graphique 2025-10-21 16:38:19 +02:00
Charles 143277455a remove user role from select options on user/show/{id} 2025-10-21 15:30:52 +02:00
Charles 0fc507d4c7 remove user role from select options on user/show/{id} 2025-10-21 15:05:18 +02:00
Charles 8c7336b821 Added tabulator for all user table 2025-10-21 11:48:54 +02:00
Charles 9270849e12 Correct image circle 2025-10-14 16:40:01 +02:00
Charles abbaf016cc Refactor 2025-10-14 15:18:44 +02:00
Charles 00c58b55d1 Added tabulator to organization index 2025-10-14 14:12:12 +02:00
Charles e818a17371 Refactor 2025-10-14 09:34:09 +02:00
Charles 6e6d02e658 Added tabulator to organization index 2025-10-14 09:29:14 +02:00
Charles 0222274a17 Added S3 bucket for users 2025-10-13 15:41:41 +02:00
Charles ead3666a4f Added S3 bucket for organizations 2025-10-13 15:12:53 +02:00
Charles 25bad81f03 Added quill to Apps editor 2025-10-13 15:09:57 +02:00
Charles fd02fc26f1 refactor 2025-10-13 15:08:20 +02:00
Charles 6dc6d3bfa9 edit application 2025-10-08 11:37:18 +02:00
Charles 20509385f6 bug correction 2025-09-10 12:57:16 +02:00
Charles 3485bcc48f Ajout commande delete role 2025-09-09 16:44:03 +02:00
Charles 1a49265658 Ajout commande creation role 2025-09-09 16:39:02 +02:00
Charles a01df6345a add Admin when Super Admin is added 2025-09-09 12:04:15 +02:00
Charles 41c6e82a13 roles logic updated 2025-09-08 08:57:50 +02:00
Charles 307e615fb3 redirect on login 2025-09-05 11:08:27 +02:00
Charles 20bc6e92bc Display roles for user only 2025-09-05 10:34:09 +02:00
Charles 1788ec9062 Manage roles in application 2025-09-05 10:01:59 +02:00
Charles 3ef774d7e0 Refactor 2025-09-04 15:15:37 +02:00
Charles dc5eb702a3 Index des applications 2025-09-04 14:59:17 +02:00
Charles 346d89f42e Refactor 2025-09-04 12:24:01 +02:00
Charles ec29f42f90 Redirect to Organization Dashboard 2025-09-04 11:43:42 +02:00
Charles c75eda74a3 Display logic updated 2025-09-04 11:38:25 +02:00
Charles 633c255598 Add organization status 2025-09-04 10:18:40 +02:00
Charles 5e52386233 activate/deactivate organizations 2025-09-04 10:17:24 +02:00
Charles 1008d636a6 Delete organization 2025-09-04 09:52:56 +02:00
Charles 3ca5eea877 Log action 2025-09-04 09:11:37 +02:00
Charles 0bcab27a1d Security correction 2025-09-03 16:00:42 +02:00
Charles eaff14acc6 add user to organization 2025-09-03 15:51:51 +02:00
Charles 889010b5ad Refactor 2025-09-03 14:39:06 +02:00
Charles a540bb5d9e Show an organization informations 2025-09-03 14:38:16 +02:00
Charles 9257709605 Adjust Admin Logic 2025-09-03 10:30:24 +02:00
Charles 1516e8c890 Edit organization 2025-09-03 10:13:53 +02:00
Charles 6964bc0214 Creation organizations 2025-09-03 10:03:55 +02:00
Charles febd2ad6b2 bug correction user creation 2025-09-03 09:27:15 +02:00
Charles e33b0b8248 Display all organization for Admin 2025-08-29 12:09:21 +02:00
Charles 8d095a368f Display all organization for super Admin 2025-08-29 11:02:19 +02:00
Charles 5a7be977ba Activate users 2025-08-29 09:22:15 +02:00
Charles 1e33782f75 display inactive users 2025-08-29 08:53:00 +02:00
Charles 245f044a40 Delete User 2025-08-28 16:08:06 +02:00
Charles 446f585cc9 activate a user from organization 2025-08-28 15:44:51 +02:00
Charles 218923dfb7 deactivate a user from organization 2025-08-28 14:40:08 +02:00
Charles 84e5d7c87a Correction 2025-08-28 09:21:38 +02:00
Charles f52ad375b4 profile picture 2025-08-28 09:21:15 +02:00
Charles 7b7f58363a Deactivate user 2025-08-27 16:49:16 +02:00
Charles 52f3d2a3de create user 2025-08-27 15:30:07 +02:00
Charles 3d832b4280 Edit user data 2025-08-27 15:23:15 +02:00
Charles 9dd820d47f Edit user data 2025-08-27 14:52:46 +02:00
Charles 3b1a3dee9a Added log action 2025-08-27 14:06:10 +02:00
Charles 71c6f82b77 Display indivodual user informations 2025-08-27 13:56:08 +02:00
Charles 26637e497a display users for admin 2025-08-22 12:12:50 +02:00
Charles 3ca1446b91 display users for super admin 2025-08-22 11:45:29 +02:00
Charles 8a19b01893 Visualisation Utilisateurs 2025-08-12 13:39:37 +02:00
Charles 2dc5710e06 Visualisation Utilisateurs 2025-08-12 13:39:32 +02:00
Charles 92dd3a3d23 refonte base v2 2025-08-12 11:06:40 +02:00
Charles 2d9b44ddb6 refonte base v2 2025-08-12 11:05:39 +02:00
Charles 9dc79eaa7d refonte role 2025-08-12 10:01:00 +02:00
Charles 716bfb8ce1 app info(don't work) 2025-08-08 10:04:35 +02:00
Charles cc93387154 update action logic 2025-08-07 15:48:16 +02:00
Charles 61e43dcd98 Log action 2025-08-07 15:01:59 +02:00
Charles 0d498d4570 Log action 2025-08-07 14:17:49 +02:00
Charles 328a89f11f Redirect 2025-08-07 12:22:36 +02:00
Charles ccd44e3560 Review of access logic 2025-08-07 12:06:31 +02:00
Charles 9da1edaa92 Review of access logic 2025-08-07 12:04:06 +02:00
Charles 3f55eefddc Review of access logic 2025-08-07 12:03:02 +02:00
Charles b81b168ec3 Log actions 2025-08-07 10:06:15 +02:00
Charles 3894d72439 Log actions 2025-08-07 09:23:37 +02:00
Charles 2f3e28757e typo 2025-08-07 09:23:03 +02:00
Charles 1f7d844d6f Create Action function 2025-08-07 09:06:58 +02:00
Charles c757a841c5 update Role logic 2025-08-07 09:06:15 +02:00
Charles 790f77c430 update Role logic 2025-08-06 16:40:20 +02:00
Charles f9c63d6753 update Action 2025-08-06 16:40:11 +02:00
Charles 95f806efce change display logic 2025-08-06 15:52:48 +02:00
Charles 37ba0a5e6a activate organization 2025-08-06 15:44:00 +02:00
Charles 371c511ecf deactivate organization 2025-08-06 14:48:41 +02:00
Charles-Edouard 993188ac4f Delete public/uploads/logos/6893102127c028.11727111.jpg 2025-08-06 12:22:00 +02:00
Charles-Edouard 18dc5f8492 Delete public/uploads/logos/2025-08-06_fsadf_logofile.jpg 2025-08-06 12:21:53 +02:00
Charles f2166b604e ignore uploads 2025-08-06 12:20:55 +02:00
Charles 8d92d3f9fc update organisation 2025-08-06 12:20:05 +02:00
Charles 5ceed1f2f2 Visual 2025-08-06 12:06:50 +02:00
Charles 450543fab7 logo 2025-08-06 11:54:24 +02:00
Charles d543e69863 Add role protection 2025-08-06 11:52:55 +02:00
Charles 7021b28163 Création d'organisation 2025-08-06 11:15:53 +02:00
Charles c55e9fa039 Correction 2025-08-06 08:52:58 +02:00
Charles bdf9f0478e update userOrg update info 2025-08-05 15:09:51 +02:00
Charles 1053a2ab22 Add user to organization 2025-08-05 11:16:45 +02:00
Charles 6efbeb0fa2 Display applications 2025-08-05 10:11:05 +02:00
Charles 1ee9a0110b AWS 2025-08-05 09:13:04 +02:00
Charles cbdb47fb17 set action log on user entity 2025-08-04 13:57:13 +02:00
Charles e6c8d5a462 update reamdme 2025-08-04 12:16:02 +02:00
Charles 7e272b2b2f Add actions display 2025-08-04 12:01:40 +02:00
Charles 6670fbc8b8 Ajout organistion Id au Actions 2025-08-04 10:57:05 +02:00
Charles 1e8d5e1eaf Ajout organistion Id au Actions 2025-08-04 10:56:53 +02:00
Charles 2e99457e16 get applications access per organization 2025-07-29 16:33:16 +02:00
Charles cde6c529a9 update access logic 2025-07-29 16:07:51 +02:00
Charles a3f993b858 organizations user information 2025-07-29 15:55:55 +02:00
Charles d2c20b9423 access token on log in 2025-07-29 14:38:19 +02:00
Charles 89ed7049b9 correction 2025-07-29 14:37:55 +02:00
Charles 16dd919a5d activity template 2025-07-28 16:23:47 +02:00
Charles 301f7bb445 statut template 2025-07-28 16:05:20 +02:00
Charles 05d8ca0499 organization dashboard User management 2025-07-28 15:19:59 +02:00
Charles e17e8e0eb2 refactor 2025-07-28 12:13:04 +02:00
Charles e6391279fe refactor 2025-07-28 11:45:59 +02:00
Charles cf16ec09a1 refactor 2025-07-28 11:40:45 +02:00
Charles 943752a002 Display all organizations 2025-07-28 11:38:16 +02:00
Charles a7e7298310 refactor 2025-07-28 11:27:18 +02:00
Charles 3337b8c001 refactor 2025-07-28 11:22:57 +02:00
Charles 6446eb2ce1 Handle permission 2025-07-28 11:20:31 +02:00
Charles a10b499522 refactor 2025-07-28 11:02:26 +02:00
Charles e77e92d39f refactor 2025-07-28 11:02:19 +02:00
Charles 00e3003257 changed user index logic 2025-07-28 10:26:13 +02:00
Charles f1b953d005 helper function 2025-07-28 10:23:07 +02:00
Charles 4aadaa351a Comments 2025-07-28 09:53:26 +02:00
Charles fcb69f987f refactor 2025-07-25 14:32:02 +02:00
Charles d7677db885 refactor 2025-07-25 12:03:54 +02:00
Charles 8eb5cf433d remove app from user organization 2025-07-25 11:50:06 +02:00
Charles fed351b433 remove user from organization 2025-07-25 11:03:21 +02:00
Charles 0a602fb52e Deactivate roles 2025-07-25 10:54:01 +02:00
Charles e87fdd32e4 refactor + default value 2025-07-25 10:38:03 +02:00
Charles cfe89f58db edit user roles and app per organization 2025-07-25 10:30:00 +02:00
Charles 1d2debf364 Roles adjustment 2025-07-17 15:55:09 +02:00
Charles 3271da59fa delete and set delete user 2025-07-17 14:10:55 +02:00
Charles d8df0bc1f4 front modifications 2025-07-17 14:10:20 +02:00
Charles c3d3218bff update userForm 2025-07-17 11:45:02 +02:00
Charles f24fb0180d Edit user 2025-07-17 11:33:34 +02:00
Charles e360019e58 Type refacto 2025-07-17 11:32:28 +02:00
Charles 8e38fe47db Type refacto 2025-07-17 11:26:05 +02:00
Charles b54fe41795 const execption 2025-07-17 10:39:32 +02:00
Charles 798ca4ba07 unique email address on creation 2025-07-17 10:39:17 +02:00
Charles d43b516826 Create user 2025-07-17 09:16:09 +02:00
Charles c99b575814 Deactivate user 2025-07-16 15:59:56 +02:00
Charles 65ff838dd9 handle error 2025-07-16 15:50:45 +02:00
Charles 4a2f9d9547 Read User information 2025-07-16 15:12:45 +02:00
Charles 10a8eb2255 Correction 2025-07-16 15:04:35 +02:00
Charles 8f232498b8 Add phone number 2025-07-16 15:03:41 +02:00
Charles bbe50dbfd9 Last connection on user entity 2025-07-10 09:26:54 +02:00
Charles 436284c9c7 add name to Organizations entity 2025-07-09 14:11:31 +02:00
Charles 228ef8cbe9 refactor + update DB 2025-07-09 09:30:34 +02:00
Charles c81142e4a5 refactor 2025-07-09 09:21:47 +02:00
Charles 6a9b7568af refactor 2025-07-09 09:16:05 +02:00
Charles 57e115a6a8 update to Template 2025-07-09 09:14:29 +02:00
Charles 79c5596766 refactor 2025-07-04 15:55:35 +02:00
289 changed files with 21648 additions and 11969 deletions

49
.dockerignore Normal file
View File

@ -0,0 +1,49 @@
# Git files
.git
.gitignore
.gitattributes
# IDE files
.idea
.vscode
*.swp
*.swo
# Documentation
README.md
*.MD
HELPER.MD
NOTIFICATION.MD
# Test files
tests
phpunit.xml.dist
# CI/CD
.github
.gitlab-ci.yml
# Platform.sh
.platform
.platform.app.yaml
# Docker files (don't copy Docker files into the image)
Dockerfile
docker-compose*.yml
compose*.yaml
.dockerignore
# Environment files (will be provided at runtime)
.env.local
.env.*.local
# Development dependencies
.php-cs-fixer.cache
# Cache and logs (will be created at runtime)
var/cache/*
var/log/*
var/sessions/*
# Vendor (will be installed during build)
# vendor is installed during the build process, so we don't ignore it

19
.env
View File

@ -17,6 +17,7 @@
###> symfony/framework-bundle ### ###> symfony/framework-bundle ###
APP_ENV=prod APP_ENV=prod
APP_SECRET='kjuusshgvk35434judshfgvkusd224444hvg' APP_SECRET='kjuusshgvk35434judshfgvkusd224444hvg'
APPLICATION=EasyPortal
###< symfony/framework-bundle ### ###< symfony/framework-bundle ###
###> doctrine/doctrine-bundle ### ###> doctrine/doctrine-bundle ###
@ -43,10 +44,12 @@ MAILER_DSN=null://null
TRUSTED_PROXY='185.116.130.121','10.8.34.21' TRUSTED_PROXY='185.116.130.121','10.8.34.21'
###> league/oauth2-server-bundle ### ###> league/oauth2-server-bundle ###
OAUTH_PRIVATE_KEY=%kernel.project_dir%/config/jwt/private.pem OAUTH_PRIVATE_KEY=%kernel.project_dir%/config/jwt/private.key
OAUTH_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem OAUTH_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.key
OAUTH_PASSPHRASE=8170ea18d2e3e05b5c7ae0672a754bf4 OAUTH_PASSPHRASE=8170ea18d2e3e05b5c7ae0672a754bf4
OAUTH_ENCRYPTION_KEY=f1b7c279f7992205a0df45e295d07066 OAUTH_ENCRYPTION_KEY=f1b7c279f7992205a0df45e295d07066
OAUTH_SSO_IDENTIFIER='sso-own-identifier'
OAUTH_SSO_IDENTIFIER_LOGIN='sso-own-identifier'
###< league/oauth2-server-bundle ### ###< league/oauth2-server-bundle ###
###> nelmio/cors-bundle ### ###> nelmio/cors-bundle ###
@ -62,3 +65,15 @@ MERCURE_PUBLIC_URL=https://example.com/.well-known/mercure
# The secret used to sign the JWTs # The secret used to sign the JWTs
MERCURE_JWT_SECRET="!ChangeThisMercureHubJWTSecretKey!" MERCURE_JWT_SECRET="!ChangeThisMercureHubJWTSecretKey!"
###< symfony/mercure-bundle ### ###< symfony/mercure-bundle ###
###> aws/aws-sdk-php-symfony ###
AWS_KEY=not-a-real-key
AWS_SECRET=@@not-a-real-secret
AWS_REGION=us-east-1
AWS_ENDPOINT=https://s3.amazonaws.com
AWS_S3_PORTAL_URL=https://s3.amazonaws.com/portal
###< aws/aws-sdk-php-symfony ###
APP_URL='https://example.com'
APP_DOMAIN='example.com'
EASYCHECK_URL='https://testcheck.solutions-easy.com'

View File

@ -1,4 +0,0 @@
###> symfony/framework-bundle ###
APP_SECRET=bba5a2c490c3f92030618ecb97b5138e
###< symfony/framework-bundle ###

View File

@ -1,6 +0,0 @@
# define your env variables for the test env here
KERNEL_CLASS='App\Kernel'
APP_SECRET='$ecretf0rt3st'
SYMFONY_DEPRECATIONS_HELPER=999999
PANTHER_APP_ENV=panther
PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots

18
.gitignore vendored
View File

@ -1,10 +1,12 @@
###> symfony/framework-bundle ### ###> symfony/framework-bundle ###
/.env.local /.env.local
/.env.test
/.env.local.php /.env.local.php
/.env.*.local /.env.*.local
/config/secrets/prod/prod.decrypt.private.php /config/secrets/prod/prod.decrypt.private.php
/public/bundles/ /public/bundles/
/public/uploads/
/var/ /var/
/vendor/ /vendor/
###< symfony/framework-bundle ### ###< symfony/framework-bundle ###
@ -14,12 +16,18 @@
.phpunit.result.cache .phpunit.result.cache
###< phpunit/phpunit ### ###< phpunit/phpunit ###
###> symfony/phpunit-bridge ###
.phpunit.result.cache
/phpunit.xml
###< symfony/phpunit-bridge ###
###> symfony/asset-mapper ### ###> symfony/asset-mapper ###
/public/assets/ /public/assets/
/assets/vendor/ /assets/vendor/
###< symfony/asset-mapper ### ###< symfony/asset-mapper ###
###> IntelliJ IDEA ###
.idea/
*.iml
###< IntelliJ IDEA ###
###>jwt keys###
/config/jwt/private.key
/config/jwt/public.key
###<jwt keys###
/composer.lock

302
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,302 @@
stages:
- build
- deploy
- notify
variables:
# OVH Container Registry URL (without https://)
# Image name and tag (format: registry/project/image)
IMAGE_NAME: "easyportal"
IMAGE_TAG: "${OVH_REGISTRY_URL}/${IMAGE_NAME}:${CI_COMMIT_REF_SLUG}-${CI_PIPELINE_ID}"
IMAGE_LATEST: "${OVH_REGISTRY_URL}/${IMAGE_NAME}:latest"
build:
stage: build
image: docker:20.10.16
rules:
- if: '$CI_COMMIT_BRANCH == "develop"'
when: always
- when: manual
before_script:
# Use host Docker daemon via socket
- docker info
# Login to OVH Container Registry
- echo "Logging into OVH Container Registry..."
- echo "$OVH_REGISTRY_PASSWORD" | docker login -u "$OVH_REGISTRY_USERNAME" "$OVH_REGISTRY_URL" --password-stdin
script:
# Build the FrankenPHP image
- echo "Building Docker image..."
- docker build --build-arg APP_ENV=prod --target frankenphp_prod -t $IMAGE_TAG -t $IMAGE_LATEST .
# Push both tags to OVH registry
- echo "Pushing image to OVH registry..."
- docker push $IMAGE_TAG
- docker push $IMAGE_LATEST
# Display image info
- echo "Successfully pushed:"
- echo " - $IMAGE_TAG"
- echo " - $IMAGE_LATEST"
after_script:
- docker logout $OVH_REGISTRY_URL || true
deploy:
stage: deploy
image: alpine:latest
needs:
- build
rules:
- if: '$CI_COMMIT_BRANCH == "develop"'
when: on_success
- when: manual
before_script:
- apk add --no-cache openssh-client
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- echo "Host *\n StrictHostKeyChecking no\n UserKnownHostsFile=/dev/null" > ~/.ssh/config
- chmod 600 ~/.ssh/config
- ssh-keyscan -H $SERVER_IP >> ~/.ssh/known_hosts || true
script:
- |
ssh $SSH_USER@$SERVER_IP << ENDSSH
set -e
cd /mnt/external-disk/easy-monitor/easyportal
# ===== PRE-DEPLOYMENT DOCKER CLEANUP =====
echo '===== Starting pre-deployment Docker cleanup ====='
# Show current Docker space usage
echo 'Current Docker space usage:'
docker system df
# Clean build cache
echo 'Cleaning Docker build cache...'
docker builder prune -f || true
# Clean stopped containers
echo 'Cleaning stopped containers...'
docker container prune -f || true
# Clean unused networks
echo 'Cleaning unused networks...'
docker network prune -f || true
# Show space usage after cleanup
echo 'Docker space usage after initial cleanup:'
docker system df
# Login to OVH registry
echo "$OVH_REGISTRY_PASSWORD" | docker login -u "$OVH_REGISTRY_USERNAME" "$OVH_REGISTRY_URL" --password-stdin
# Pull latest image
echo 'Pulling latest image...'
docker pull $IMAGE_LATEST
# Clean unused images before deployment
echo 'Cleaning unused Docker images...'
docker image prune -f || true
# Update .env.compose with new image tag
echo 'Updating .env.compose with new image...'
echo "EASYPORTAL_IMAGE=$IMAGE_LATEST" > .env.compose
# Recreate php and messenger containers with new image
echo 'Restarting EasyPortal services with new image...'
docker compose --env-file .env.compose up -d --force-recreate --no-deps php messenger
# Wait for service to be ready
sleep 15
# Run migrations
echo 'Running database migrations...'
docker compose --env-file .env.compose exec -T php php bin/console doctrine:migrations:migrate --no-interaction
# Clear cache
echo 'Clearing Symfony cache...'
docker compose --env-file .env.compose exec -T php php bin/console cache:clear --env=prod --no-debug
# ===== POST-DEPLOYMENT CLEANUP =====
echo '===== Final Docker cleanup ====='
docker builder prune -f || true
docker container prune -f || true
# Keep last 3 image versions
echo 'Cleaning old image versions (keeping last 3)...'
docker images --format 'table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedAt}}' | grep -v REPOSITORY | sort -k1,1 -k4,4r | awk '
{
repo_tag = \$1":" \$2
if (repo_count[\$1]++ >= 3 && \$2 != "latest" && \$1 != "<none>") {
print \$3
}
}' | xargs -r docker rmi -f || true
echo 'Final Docker space usage:'
docker system df
echo '===== Deployment completed successfully! ====='
ENDSSH
after_script:
- rm -f ~/.ssh/id_rsa
environment:
name: production
url: https://testportail.solutions-easy.com
notify_success:
stage: notify
image: curlimages/curl:latest
needs:
- deploy
rules:
- when: on_success
- when: manual
allow_failure: true
script:
- |
COMMIT_SHORT_SHA=$(echo $CI_COMMIT_SHA | cut -c1-8)
DEPLOYMENT_TYPE="Déploiement Beta"
if [ "$CI_COMMIT_BRANCH" != "develop" ]; then
DEPLOYMENT_TYPE="Déploiement Beta"
fi
curl -H "Content-Type: application/json" -d "{
\"type\": \"message\",
\"attachments\": [
{
\"contentType\": \"application/vnd.microsoft.card.adaptive\",
\"content\": {
\"type\": \"AdaptiveCard\",
\"body\": [
{
\"type\": \"TextBlock\",
\"text\": \"$DEPLOYMENT_TYPE\",
\"weight\": \"Bolder\",
\"size\": \"Large\"
},
{
\"type\": \"TextBlock\",
\"text\": \"**App:** EasyPortal\",
\"wrap\": true
},
{
\"type\": \"TextBlock\",
\"text\": \"**Version:** [$CI_COMMIT_REF_NAME - $COMMIT_SHORT_SHA]($CI_PROJECT_URL/-/commit/$CI_COMMIT_SHA)\",
\"wrap\": true,
\"markdown\": true
},
{
\"type\": \"TextBlock\",
\"text\": \"**Pipeline:** [Voir le pipeline]($CI_PIPELINE_URL)\",
\"wrap\": true,
\"markdown\": true
},
{
\"type\": \"TextBlock\",
\"text\": \"**Auteur:** $GITLAB_USER_LOGIN\",
\"wrap\": true
},
{
\"type\": \"TextBlock\",
\"text\": \"**Statut:** Succès ✓\",
\"wrap\": true,
\"color\": \"Good\"
}
],
\"actions\": [
{
\"type\": \"Action.OpenUrl\",
\"title\": \"Voir le pipeline\",
\"url\": \"$CI_PIPELINE_URL\"
},
{
\"type\": \"Action.OpenUrl\",
\"title\": \"Voir le commit\",
\"url\": \"$CI_PROJECT_URL/-/commit/$CI_COMMIT_SHA\"
}
],
\"\$schema\": \"http://adaptivecards.io/schemas/adaptive-card.json\",
\"version\": \"1.0\"
}
}
]
}" "$TEAMS_WEBHOOK_URL"
notify_failure:
stage: notify
image: curlimages/curl:latest
needs:
- deploy
rules:
- when: on_failure
script:
- |
COMMIT_SHORT_SHA=$(echo $CI_COMMIT_SHA | cut -c1-8)
DEPLOYMENT_TYPE="Déploiement Beta"
if [ "$CI_COMMIT_BRANCH" != "develop" ]; then
DEPLOYMENT_TYPE="Déploiement Beta"
fi
curl -H "Content-Type: application/json" -d "{
\"type\": \"message\",
\"attachments\": [
{
\"contentType\": \"application/vnd.microsoft.card.adaptive\",
\"content\": {
\"type\": \"AdaptiveCard\",
\"body\": [
{
\"type\": \"TextBlock\",
\"text\": \"$DEPLOYMENT_TYPE\",
\"weight\": \"Bolder\",
\"size\": \"Large\"
},
{
\"type\": \"TextBlock\",
\"text\": \"**App:** EasyPortal\",
\"wrap\": true
},
{
\"type\": \"TextBlock\",
\"text\": \"**Version:** [$CI_COMMIT_REF_NAME - $COMMIT_SHORT_SHA]($CI_PROJECT_URL/-/commit/$CI_COMMIT_SHA)\",
\"wrap\": true,
\"markdown\": true
},
{
\"type\": \"TextBlock\",
\"text\": \"**Pipeline:** [Voir le pipeline]($CI_PIPELINE_URL)\",
\"wrap\": true,
\"markdown\": true
},
{
\"type\": \"TextBlock\",
\"text\": \"**Auteur:** $GITLAB_USER_LOGIN\",
\"wrap\": true
},
{
\"type\": \"TextBlock\",
\"text\": \"**Statut:** Échec ✗\",
\"wrap\": true,
\"color\": \"Attention\"
}
],
\"actions\": [
{
\"type\": \"Action.OpenUrl\",
\"title\": \"Voir le pipeline\",
\"url\": \"$CI_PIPELINE_URL\"
},
{
\"type\": \"Action.OpenUrl\",
\"title\": \"Voir le commit\",
\"url\": \"$CI_PROJECT_URL/-/commit/$CI_COMMIT_SHA\"
}
],
\"\$schema\": \"http://adaptivecards.io/schemas/adaptive-card.json\",
\"version\": \"1.0\"
}
}
]
}" "$TEAMS_WEBHOOK_URL"

View File

@ -8,8 +8,23 @@
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mercure" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mercure" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mercure-bundle" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mercure-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/firebase/php-jwt" /> <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" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/rate-limiter" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/remote-event" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/webhook" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="quill.snow" level="application" />
<orderEntry type="library" name="quill" level="application" />
</component> </component>
</module> </module>

View File

@ -10,6 +10,11 @@
<option name="highlightLevel" value="WARNING" /> <option name="highlightLevel" value="WARNING" />
<option name="transferred" value="true" /> <option name="transferred" value="true" />
</component> </component>
<component name="PhpCodeSniffer">
<phpcs_settings>
<phpcs_by_interpreter asDefaultInterpreter="true" interpreter_id="e61c7a46-7290-4f1b-ace7-5462f6da9ae0" timeout="30000" />
</phpcs_settings>
</component>
<component name="PhpIncludePathManager"> <component name="PhpIncludePathManager">
<include_path> <include_path>
<path value="$PROJECT_DIR$/vendor/symfony/twig-bridge" /> <path value="$PROJECT_DIR$/vendor/symfony/twig-bridge" />
@ -83,7 +88,6 @@
<path value="$PROJECT_DIR$/vendor/lcobucci/jwt" /> <path value="$PROJECT_DIR$/vendor/lcobucci/jwt" />
<path value="$PROJECT_DIR$/vendor/lcobucci/clock" /> <path value="$PROJECT_DIR$/vendor/lcobucci/clock" />
<path value="$PROJECT_DIR$/vendor/twig/twig" /> <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/composer" />
<path value="$PROJECT_DIR$/vendor/theseer/tokenizer" /> <path value="$PROJECT_DIR$/vendor/theseer/tokenizer" />
<path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" /> <path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" />
@ -141,7 +145,6 @@
<path value="$PROJECT_DIR$/vendor/defuse/php-encryption" /> <path value="$PROJECT_DIR$/vendor/defuse/php-encryption" />
<path value="$PROJECT_DIR$/vendor/symfony/ux-turbo" /> <path value="$PROJECT_DIR$/vendor/symfony/ux-turbo" />
<path value="$PROJECT_DIR$/vendor/symfony/stimulus-bundle" /> <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/symfony/type-info" />
<path value="$PROJECT_DIR$/vendor/doctrine/event-manager" /> <path value="$PROJECT_DIR$/vendor/doctrine/event-manager" />
<path value="$PROJECT_DIR$/vendor/symfony/console" /> <path value="$PROJECT_DIR$/vendor/symfony/console" />
@ -164,9 +167,32 @@
<path value="$PROJECT_DIR$/vendor/symfony/mercure" /> <path value="$PROJECT_DIR$/vendor/symfony/mercure" />
<path value="$PROJECT_DIR$/vendor/symfony/mercure-bundle" /> <path value="$PROJECT_DIR$/vendor/symfony/mercure-bundle" />
<path value="$PROJECT_DIR$/vendor/firebase/php-jwt" /> <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" />
<path value="$PROJECT_DIR$/vendor/twig/extra-bundle" />
<path value="$PROJECT_DIR$/vendor/staabm/side-effects-detector" />
<path value="$PROJECT_DIR$/vendor/symfony/rate-limiter" />
<path value="$PROJECT_DIR$/vendor/dama/doctrine-test-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php85" />
<path value="$PROJECT_DIR$/vendor/symfony/remote-event" />
<path value="$PROJECT_DIR$/vendor/symfony/webhook" />
</include_path> </include_path>
</component> </component>
<component name="PhpProjectSharedConfiguration" php_language_level="8.2" /> <component name="PhpProjectSharedConfiguration" php_language_level="8.2" />
<component name="PhpStan">
<PhpStan_settings>
<phpstan_by_interpreter asDefaultInterpreter="true" interpreter_id="e61c7a46-7290-4f1b-ace7-5462f6da9ae0" timeout="60000" />
</PhpStan_settings>
</component>
<component name="PhpStanOptionsConfiguration"> <component name="PhpStanOptionsConfiguration">
<option name="transferred" value="true" /> <option name="transferred" value="true" />
</component> </component>
@ -175,6 +201,11 @@
<PhpUnitSettings configuration_file_path="$PROJECT_DIR$/phpunit.xml.dist" custom_loader_path="$PROJECT_DIR$/vendor/autoload.php" use_configuration_file="true" /> <PhpUnitSettings configuration_file_path="$PROJECT_DIR$/phpunit.xml.dist" custom_loader_path="$PROJECT_DIR$/vendor/autoload.php" use_configuration_file="true" />
</phpunit_settings> </phpunit_settings>
</component> </component>
<component name="Psalm">
<Psalm_settings>
<psalm_fixer_by_interpreter asDefaultInterpreter="true" interpreter_id="e61c7a46-7290-4f1b-ace7-5462f6da9ae0" timeout="60000" />
</Psalm_settings>
</component>
<component name="PsalmOptionsConfiguration"> <component name="PsalmOptionsConfiguration">
<option name="transferred" value="true" /> <option name="transferred" value="true" />
</component> </component>

File diff suppressed because one or more lines are too long

104
Dockerfile Normal file
View File

@ -0,0 +1,104 @@
#syntax=docker/dockerfile:1
# Versions
FROM dunglas/frankenphp:1-php8.3 AS frankenphp_upstream
# The different stages of this Dockerfile are meant to be built into separate images
# https://docs.docker.com/develop/develop-images/multistage-build/#stop-at-a-specific-build-stage
# https://docs.docker.com/compose/compose-file/#target
# Base FrankenPHP image
FROM frankenphp_upstream AS frankenphp_base
WORKDIR /app
VOLUME /app/var/
# persistent / runtime deps
# hadolint ignore=DL3008
RUN apt-get update && apt-get install -y --no-install-recommends \
acl \
file \
gettext \
git \
&& rm -rf /var/lib/apt/lists/*
RUN set -eux; \
install-php-extensions \
@composer \
apcu \
intl \
opcache \
zip \
;
# https://getcomposer.org/doc/03-cli.md#composer-allow-superuser
ENV COMPOSER_ALLOW_SUPERUSER=1
# Transport to use by Mercure (default to Bolt)
ENV MERCURE_TRANSPORT_URL=bolt:///data/mercure.db
ENV PHP_INI_SCAN_DIR=":$PHP_INI_DIR/app.conf.d"
###> recipes ###
###> doctrine/doctrine-bundle ###
RUN install-php-extensions pdo_pgsql
###< doctrine/doctrine-bundle ###
###< recipes ###
COPY --link frankenphp/conf.d/10-app.ini $PHP_INI_DIR/app.conf.d/
COPY --link --chmod=755 frankenphp/docker-entrypoint.sh /usr/local/bin/docker-entrypoint
COPY --link frankenphp/Caddyfile.prod /etc/frankenphp/Caddyfile
ENTRYPOINT ["docker-entrypoint"]
HEALTHCHECK --start-period=60s CMD curl -f http://localhost:2019/metrics || exit 1
CMD [ "frankenphp", "run", "--config", "/etc/frankenphp/Caddyfile" ]
# Dev FrankenPHP image
FROM frankenphp_base AS frankenphp_dev
ENV APP_ENV=dev
ENV XDEBUG_MODE=off
ENV FRANKENPHP_WORKER_CONFIG=watch
RUN mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini"
RUN set -eux; \
install-php-extensions \
xdebug \
;
COPY --link frankenphp/conf.d/20-app.dev.ini $PHP_INI_DIR/app.conf.d/
CMD [ "frankenphp", "run", "--config", "/etc/frankenphp/Caddyfile", "--watch" ]
# Prod FrankenPHP image
FROM frankenphp_base AS frankenphp_prod
ENV APP_ENV=prod
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
COPY --link frankenphp/conf.d/20-app.prod.ini $PHP_INI_DIR/app.conf.d/
# prevent the reinstallation of vendors at every changes in the source code
COPY --link composer.* symfony.* ./
RUN set -eux; \
composer install --no-cache --prefer-dist --no-dev --no-autoloader --no-scripts --no-progress
# copy sources
COPY --link . ./
RUN rm -Rf frankenphp/
RUN set -eux; \
mkdir -p var/cache var/log; \
composer dump-autoload --classmap-authoritative --no-dev; \
composer dump-env prod; \
chmod +x bin/console; \
# Install JavaScript vendor assets and compile assets for production \
php bin/console importmap:install; \
php bin/console asset-map:compile; \
php bin/console assets:install public; \
sync;

41
HELPER.MD Normal file
View File

@ -0,0 +1,41 @@
## 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`
- A delete command is available to delete roles
### ROLES
```bash
php bin/console app:delete-role ROLE_NAME
```
### Tabulator
- Certaines fonctions sont déjà disponibles (snippet) mais commentées, car on ne les utilise pas
- Exemples de sorting et filtering sont disponibles dans 'src/controller/organization.php' L.268
### Frontend:
- Le Body/content de chaque page sont dans des div avec le style suivant :
``` html
<div class="w-100 h-100 p-5 m-auto">
```
- L'espace entre les éléments cartes est avec l'un des styles suivants :
``` html
<div class="mb-3"> <!-- margin bottom -->
<div class="mt-3"> <!-- margin top -->
<div class="me-3"> <!-- margin end/right -->
<div class="ms-3"> <!-- margin start/left -->
<div class="mx-3"> <!-- margin left and right -->
<div class="my-3"> <!-- margin top and bottom -->
<div class="m-3"> <!-- margin -->
<div class="d-flex gap-2"> <!-- gap entre les boutons -->
```
- Chaque élément est une carte afin de donner un style uniforme :
``` html
<div class="card p-3">
```
php bin/console messenger:consume async -vv

190
NOTIFICATION.MD Normal file
View File

@ -0,0 +1,190 @@
# Système de notification de l'application
## Vue d'ensemble
Le système de notification de l'application permet d'informer les utilisateurs de diverse action.
## Architecture
### Composants principaux
1. **Service de Notification** : Gère la création, l'envoi et le suivi des notifications.
2. **Interface Utilisateur** : Affiche les notifications aux utilisateurs via des pops-ups, des bannières ou des emails.
3. **Template Email** : Modèles prédéfinis pour les notifications par email.
4. **Type D'action** : Enum d'action qui déclenche des notifications (ex: nouvel utilisateur, utilisateur actif, ...).
### Service de Notification
Le service de notification est responsable de la gestion des notifications. Il inclut les fonctionnalités suivantes :
- Création de notifications basées sur des événements spécifiques.
- Envoi de notifications via différents canaux (email, in-app).
- Suivi de l'état des notifications (envoyé, lu, etc.).
### Interface Utilisateur
L'interface utilisateur affiche les notifications de manière conviviale. Les notifications peuvent apparaître sous forme
de pops-ups, de bannières ou d'emails. (Possibilité d'intéragir avec les notifications)
### Template Email
Les templates email sont utilisés pour formater les notifications envoyées par email. Chaque type de notification
a son propre template pour assurer une communication claire et cohérente.
### Type d'action
```
enum ActionType: String {
case NewUser = "NEW_USER";
case ActiveUser = "ACTIVE_USER";
case PasswordReset = "PASSWORD_RESET";
case SubscriptionExpired = "SUBSCRIPTION_EXPIRED";
case OrganizationInvited = "ORGANIZATION_INVITED";
case OrganizationInactive = "ORGANIZATION_INACTIVE";
case OrganizationDeleted = "ORGANIZATION_DELETED";
case OrginizationUserInvited = "ORGANIZATION_USER_INVITED";
case UserDeleted = "USER_DELETED";
}
```
## Flux de travail
1. Ladministrateur crée un utilisateur depuis linterface (formulaire “Créer un utilisateur”).
2. Le contrôleur valide la requête et appelle le cas dusage UserAdministrationService->handle(ActionType::NewUser, $admin, $payload).
3. Le service crée lutilisateur en base avec le statut INVITED, associe lorganisation de ladmin, et génère un lien signé/jeton de setup de mot de passe (TTL).
4. Le service publie un événement de domaine UserInvitedEvent { userId, adminId, organizationId } sur Messenger (transport async).
5. Handler async A — SendUserInvitationEmailHandler:
6. Construit lemail via Symfony Mailer + Twig (emails/user_invitation.html.twig) avec le lien de définition de mot de passe.
7. Envoie le mail à lutilisateur invité.
8. Handler async B — NotifyAdminInvitationSentHandler:
9. Crée une notification interne (Notifier, canal “inapp”).
10. Pousse un événement temps réel via Mercure sur le topic admin/{adminId}/events avec le type INVITATION_EMAIL_SENT.
11. LUI admin affiche un toast/bannière confirmant “Email dinvitation envoyé”.
12. Lutilisateur ouvre lemail et clique le lien de définition de mot de passe.
13. Le PasswordSetupController vérifie la signature/le jeton et la validité (TTL), affiche le formulaire, puis enregistre le nouveau mot de passe.
14. À la réussite, lutilisateur passe au statut ACTIVE et laction publie UserActivatedEvent { userId, adminId, organizationId } sur Messenger (async).
15. Handler async C — NotifyAdminUserActivatedHandler:
16. Crée une notification interne (Notifier, canal “inapp”) “Compte activé”.
17. Pousse un événement Mercure sur admin/{adminId}/events avec le type USER_ACTIVATED.
18. LUI admin met à jour la liste des membres (badge “Actif”) et affiche un toast confirmant lactivation.
19. Journalisation/Audit:
20. Chaque handler écrit une trace (succès/échec) en base ou dans un EmailLog/NotificationLog.
21. En cas déchec denvoi, Messenger applique la stratégie de retry puis bascule en file failed si nécessaire (tableau de bord de supervision).
22. Cas “utilisateur existant ajouté à une autre organisation”:
23. Si lemail existe déjà, on rattache lutilisateur à la nouvelle organisation et on publie OrganizationUserInvitedEvent.
24. Handler dédié envoie un email dinformation (“Vous avez été ajouté à une nouvelle organisation”) et notifie ladmin via Notifier + Mercure.
25. Cas dactions dérivées par enum:
26. ActionType::NewUser → déclenche UserInvitedEvent (steps 36).
27. ActionType::ActiveUser (si activé par un flux admin) → déclenche directement UserActivatedEvent (steps 910).
28. ActionType::OrganizationUserInvited → flux similaire au point 12 pour la multiorganisation.
29. Autres actions (PasswordReset, UserDeleted, etc.) suivent le même patron: contrôleur → service (match enum) → événement Messenger → handlers (Mailer/Notifier/Mercure) → UI temps réel.
## Stack technologique
- Symfony Messenger: asynchrone, retries, découplage des I/O lents.
- Symfony Mailer + Twig: emails dinvitation et dinformation.
- Symfony Notifier (canal inapp) + Mercure: notifications persistées + push temps réel vers lUI admin.
- Enum ActionType: routage clair dans lapplication, évite la logique stringbased.
```mermaid
flowchart LR
%% Couche 1: Action initiale
A[User action event - Admin cree un utilisateur] --> B[HTTP controller API - Symfony]
B --> C[Domain service - UserAdministrationService]
C -->|Inspecte enum ActionType::NewUser| C1[Create user - status INVITED - liaison organisation - genere lien jeton mot de passe TTL]
C1 --> D[Dispatch UserInvitedEvent - userId adminId organizationId - vers Symfony Messenger bus]
%% Couche 2: Messaging / Infra
D --> E[Transport async - AMQP / Redis / Doctrine]
E --> RQ[Retry queue]
E --> FQ[Failed queue - dead letter]
E --> W[Workers Messenger]
F[Supervisor / systemd] --> W
%% Monolog transversal (logs a chaque etape)
A --> LOG_GLOBAL[Monolog - log event initial]
B --> LOG_GLOBAL
C --> LOG_GLOBAL
C1 --> LOG_GLOBAL
D --> LOG_GLOBAL
E --> LOG_GLOBAL
RQ --> LOG_GLOBAL
FQ --> LOG_GLOBAL
W --> LOG_GLOBAL
%% Handlers pour l'invitation
W --> H1[Handler A - Symfony Mailer + Twig]
H1 --> H1o[Email d'invitation avec lien setup mot de passe]
H1 --> LOG_GLOBAL
W --> H2[Handler B - Symfony Notifier in-app]
H2 --> UI1[Notification UI admin - Email d'invitation envoye]
H2 --> LOG_GLOBAL
W -. optionnel .-> WH1[Webhook HTTP sortant - invitation envoyee]
WH1 --> LOG_GLOBAL
W -. optionnel .-> SMS1[SMS gateway - SMS invitation]
SMS1 --> LOG_GLOBAL
W -. optionnel .-> PUSH1[Mobile push service - notification mobile]
PUSH1 --> LOG_GLOBAL
RQ --> METRICS[Metrics et dashboard]
FQ --> METRICS
LOG_GLOBAL --> METRICS
%% Flux activation utilisateur
subgraph Activation du compte
UA[User action event - Invite clique le lien] --> PS[HTTP controller API - PasswordSetupController]
PS -->|Verifie signature et TTL| PSOK[Set password - user status ACTIVE]
PS --> LOG_GLOBAL
PSOK --> LOG_GLOBAL
PSOK --> D2[Dispatch UserActivatedEvent - userId adminId organizationId - vers Messenger bus]
D2 --> E2[Transport async]
E2 --> RQ2[Retry queue]
E2 --> FQ2[Failed queue]
E2 --> W2[Workers Messenger]
F --> W2
D2 --> LOG_GLOBAL
E2 --> LOG_GLOBAL
RQ2 --> LOG_GLOBAL
FQ2 --> LOG_GLOBAL
W2 --> LOG_GLOBAL
W2 --> H3[Handler C - Notifier in-app]
H3 --> UI2[Notification UI admin - Compte active]
H3 --> LOG_GLOBAL
W2 -. optionnel .-> WH2[Webhook HTTP sortant - user active]
WH2 --> LOG_GLOBAL
W2 -. optionnel .-> MAIL2[Mailer ou SMS ou Push - confirmation utilisateur]
MAIL2 --> LOG_GLOBAL
RQ2 --> METRICS
FQ2 --> METRICS
end
%% Cas particulier : utilisateur existant ajoute a une nouvelle organisation
C -->|Email deja existant| SP1[Rattache a nouvelle organisation]
SP1 --> LOG_GLOBAL
SP1 --> D3[Dispatch OrganizationUserInvitedEvent]
D3 --> E3[Transport async] --> W3[Workers]
F --> W3
D3 --> LOG_GLOBAL
E3 --> LOG_GLOBAL
W3 --> LOG_GLOBAL
W3 --> M3[Mailer - ajoute a une nouvelle organisation]
M3 --> LOG_GLOBAL
W3 --> N3[Notifier in-app - toast admin Utilisateur ajoute]
N3 --> LOG_GLOBAL
W3 -. optionnel .-> WH3[Webhook ou SMS ou Mobile]
WH3 --> LOG_GLOBAL
M3 --> METRICS
N3 --> METRICS
WH3 --> METRICS
%% Styles
classDef infra fill:#e8f0fe,stroke:#5b8def,stroke-width:1px;
classDef handler fill:#dcf7e9,stroke:#2ea66a,stroke-width:1px;
classDef ui fill:#f0d9ff,stroke:#9c27b0,stroke-width:1px;
classDef audit fill:#eeeeee,stroke:#9e9e9e,stroke-width:1px;
class E,E2,E3,RQ,FQ,RQ2,FQ2,METRICS infra;
class W,W2,W3,H1,H2,H3,M3,N3 handler;
class H1o,UI1,UI2 ui;
class LOG_GLOBAL audit;
```

View File

@ -1,23 +1,26 @@
## Template de base pour les applications de la suite Solutions-easy ## Template de base pour les applications de la suite Solutions-easy
### Stack technique ### Stack technique
- Symfony 7.2 - Symfony 7.4
- php 8.2 ou supérieur - php 8.2 ou supérieur
- Stimulus - Stimulus
- Turbo - Turbo
- Bootstrap 5.3 - 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
```
#### Roles
```bash
php bin/console app:create-role USER
php bin/console app:create-role ADMIN
php bin/console app:create-role "SUPER ADMIN"
```
#### Choices.js
```bash
php bin/console importmap:require choices.js
php bin/console importmap:require choices.js/public/assets/styles/choices.min.css
```

View File

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

View File

@ -0,0 +1,169 @@
import {Controller} from '@hotwired/stimulus'
import Quill from 'quill'
export default class extends Controller {
static values = {
application: String,
organization: String,
user: Number,
}
static targets = ['hidden', 'submitBtn', 'appList']
connect() {
// Map each editor to its toolbar and hidden field
if (document.querySelector('#editor-description')) {
this.editors = [
{
editorSelector: '#editor-description',
toolbarSelector: '#toolbar-description',
hiddenTarget: this.hiddenTargets[0],
},
{
editorSelector: '#editor-descriptionSmall',
toolbarSelector: '#toolbar-descriptionSmall',
hiddenTarget: this.hiddenTargets[1],
},
]
this.editors.forEach(({editorSelector, toolbarSelector, hiddenTarget}) => {
const quill = new Quill(editorSelector, {
modules: {
toolbar: toolbarSelector,
},
theme: 'snow',
placeholder: 'Écrivez votre texte...',
})
quill.on('text-change', () => {
hiddenTarget.value = quill.root.innerHTML
})
hiddenTarget.value = quill.root.innerHTML
})
}
if(this.userValue){
this.loadApplications();
}
}
handleAuthorizeSubmit(event) {
event.preventDefault();
const originalText = this.submitBtnTarget.textContent;
if (!confirm(`Vous vous apprêtez à donner l'accès à ${this.organizationValue} pour ${this.applicationValue}. Êtesvous sûr(e) ?`)) {
return;
}
this.submitBtnTarget.textContent = 'En cours...';
this.submitBtnTarget.disabled = true;
fetch(event.target.action, {
method: 'POST',
body: new FormData(event.target)
})
.then(response => {
if (response.ok) {
this.submitBtnTarget.textContent = 'Autorisé ✓';
this.submitBtnTarget.classList.replace('btn-secondary', 'btn-success');
} else {
this.submitBtnTarget.textContent = originalText;
this.submitBtnTarget.disabled = false;
alert('Erreur lors de l\'action');
}
})
.catch(error => {
this.submitBtnTarget.textContent = originalText;
this.submitBtnTarget.disabled = false;
alert('Erreur lors de l\'action');
});
}
handleRemoveSubmit(event) {
event.preventDefault();
const originalText = this.submitBtnTarget.textContent;
if (!confirm(`Vous vous apprêtez à retirer l'accès à ${this.applicationValue} pour ${this.organizationValue}. Êtesvous sûr(e) ?`)) {
return;
}
this.submitBtnTarget.textContent = 'En cours...';
this.submitBtnTarget.disabled = true;
fetch(event.target.action, {
method: 'POST',
body: new FormData(event.target)
})
.then(response => {
if (response.ok) {
this.submitBtnTarget.textContent = 'Retiré ✓';
this.submitBtnTarget.classList.replace('btn-secondary', 'btn-danger');
} else {
this.submitBtnTarget.textContent = originalText;
this.submitBtnTarget.disabled = false;
alert('Erreur lors de l\'action');
}
})
.catch(error => {
this.submitBtnTarget.textContent = originalText;
this.submitBtnTarget.disabled = false;
alert('Erreur lors de l\'action');
});
}
async loadApplications() {
if (!this.userValue) return;
try {
// Note: Ensure the URL matches your route prefix (e.g. /application/user/123)
// Adjust the base path below if your controller route is prefixed!
const response = await fetch(`/application/user/${this.userValue}`);
if (!response.ok) throw new Error("Failed to load apps");
const apps = await response.json();
this.renderApps(apps);
} catch (error) {
console.error(error);
this.appListTarget.innerHTML = `<span class="text-danger small">Erreur</span>`;
}
}
renderApps(apps) {
if (apps.length === 0) {
// Span 2 columns if empty so the message is centered
this.appListTarget.innerHTML = `<span class="text-muted small" style="grid-column: span 2; text-align: center;">Aucune application</span>`;
return;
}
const html = apps.map(app => {
const url = `https://${app.subDomain}.solutions-easy.com`;
// Check for logo string vs object
const logoSrc = (typeof app.logoMiniUrl === 'string') ? app.logoMiniUrl : '';
// Render Icon (Image or Fallback)
const iconHtml = logoSrc
? `<img src="${logoSrc}" style="width:32px; height:32px; object-fit:contain; margin-bottom: 5px;">`
: `<i class="bi bi-box-arrow-up-right text-primary" style="font-size: 24px; margin-bottom: 5px;"></i>`;
// Return a Card-like block
return `
<a href="${url}" target="_blank"
class="d-flex flex-column align-items-center justify-content-center p-3 rounded text-decoration-none text-dark bg-light-hover"
style="transition: background 0.2s; height: 100%;">
${iconHtml}
<span class="fw-bold text-center text-truncate w-100" style="font-size: 0.85rem;">
${app.name}
</span>
</a>
`;
}).join('');
this.appListTarget.innerHTML = html;
}
}

View File

@ -0,0 +1,27 @@
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
async fetchAndRenderApplications(targetElement) {
try {
const response = await fetch('/application/data/all');
const apps = await response.json();
targetElement.innerHTML = apps.map(app => `
<div class="col-md-4 mb-3">
<div class="form-check border p-2 rounded d-flex align-items-center gap-2">
<input class="form-check-input ms-1" type="checkbox" name="applications[]" value="${app.id}" id="app_${app.id}">
<label class="form-check-label d-flex align-items-center gap-2" for="app_${app.id}">
<img src="${app.logoMiniUrl}" alt="${app.name}" style="height: 20px; width: 20px; object-fit: contain;">
${app.name}
</label>
</div>
</div>
`).join('');
return apps;
} catch (error) {
targetElement.innerHTML = '<div class="text-danger">Erreur de chargement.</div>';
console.error("App load error:", error);
}
}
}

View File

@ -0,0 +1,324 @@
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
static targets = ['badge', 'list'];
static values = {
userId: Number,
mercureUrl: String,
url: String,
};
connect() {
this.loadNotifications();
this.connectToMercure();
this.toastContainer = this.createToastContainer();
}
disconnect() {
if (this.eventSource) {
this.eventSource.close();
}
}
async loadNotifications() {
try {
const response = await fetch('/notifications/unread');
const data = await response.json();
this.updateBadge(data.unreadCount);
this.renderNotifications(data.notifications);
} catch (error) {
console.error('Failed to load notifications:', error);
}
}
async connectToMercure() {
try {
// Fetch the JWT token and topic from the server
const response = await fetch('/mercure-token');
const data = await response.json();
// Use server-provided topic if available, otherwise fallback to default per-user topic
const topic = data.topic || `${this.urlValue}/notifications/user/${this.userIdValue}`;
const url = new URL(this.mercureUrlValue);
url.searchParams.append('topic', topic);
// Add authorization token as URL param if provided (Mercure can accept it this way)
if (data.token) {
url.searchParams.append('authorization', data.token);
}
try {
this.eventSource = new EventSource(url.toString());
} catch (e) {
console.error('❌ Failed to create EventSource:', e);
return;
}
this.eventSource.onopen = () => {
};
this.eventSource.onmessage = (event) => {
try {
const notification = JSON.parse(event.data);
this.handleNewNotification(notification);
} catch (parseError) {
console.error('Failed to parse Mercure message data:', parseError, 'raw data:', event.data);
}
};
this.eventSource.onerror = (error) => {
try {
console.error('EventSource readyState:', this.eventSource.readyState);
} catch (e) {
console.error('Could not read EventSource.readyState:', e);
}
// EventSource will automatically try to reconnect.
// If closed, log it for debugging.
try {
if (this.eventSource.readyState === EventSource.CLOSED) {
console.log();
} else if (this.eventSource.readyState === EventSource.CONNECTING) {
console.log();
} else if (this.eventSource.readyState === EventSource.OPEN) {
console.log();
}
} catch (e) {
console.error('Error while checking EventSource state:', e);
}
};
} catch (error) {
console.error('Failed to connect to Mercure:', error);
}
}
handleNewNotification(notification) {
this.showToast(notification);
this.loadNotifications();
}
updateBadge(count) {
const badge = this.badgeTarget;
if (count > 0) {
badge.textContent = count > 99 ? '99+' : count;
badge.style.display = 'block';
} else {
badge.style.display = 'none';
}
}
renderNotifications(notifications) {
const list = this.listTarget;
if (notifications.length === 0) {
list.innerHTML = `
<div class="text-center py-4 text-muted">
<i class="mx-0 mb-2">
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" fill="currentColor" viewBox="0 0 16 16">
<path d="M8.865 2.5a.75.75 0 0 0-1.73 0L6.5 5.5H4.75a.75.75 0 0 0 0 1.5h1.5l-.5 3H3.5a.75.75 0 0 0 0 1.5h1.5l-.635 3.135a.75.75 0 0 0 1.47.28L6.5 11.5h3l-.635 3.135a.75.75 0 0 0 1.47.28L11 11.5h1.75a.75.75 0 0 0 0-1.5h-1.5l.5-3h2.25a.75.75 0 0 0 0-1.5h-2l.635-3.135zM9.5 10l.5-3h-3l-.5 3h3z"/>
</svg>
</i>
<p class="mb-0">Aucune notification</p>
</div>
`;
return;
}
list.innerHTML = notifications.map(notif => this.renderNotificationItem(notif)).join('');
}
renderNotificationItem(notification) {
const iconHtml = this.getIcon(notification.type);
const timeAgo = this.getTimeAgo(notification.createdAt);
const readClass = notification.isRead ? 'opacity-75' : '';
return `
<a class="dropdown-item preview-item ${readClass}"
href="#"
data-notification-id="${notification.id}"
data-action="click->notification#markAsRead">
<div class="preview-thumbnail">
<div class="preview-icon ${this.getIconBgClass(notification.type)}">
${iconHtml}
</div>
</div>
<div class="preview-item-content">
<h6 class="preview-subject font-weight-normal mb-1">${this.escapeHtml(notification.title)}</h6>
<p class="font-weight-light small-text mb-0 text-muted">${this.escapeHtml(notification.message)}</p>
<p class="font-weight-light small-text mb-0 text-muted mt-1">
<small>${timeAgo}</small>
</p>
</div>
<button class="btn btn-sm btn-link text-danger ms-2"
data-action="click->notification#deleteNotification"
data-notification-id="${notification.id}"
style="padding: 0.25rem;">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
</svg>
</button>
</a>
`;
}
getIcon(type) {
const icons = {
user_joined: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="mx-0 mb-1" viewBox="0 0 16 16"><path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4zm-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10c-2.29 0-3.516.68-4.168 1.332-.678.678-.83 1.418-.832 1.664h10z"/></svg>',
user_invited: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="mx-0 mb-1" viewBox="0 0 640 512"><path fill="currentColor" d="M224 0a128 128 0 1 1 0 256a128 128 0 1 1 0-256m-45.7 304h91.4c20.6 0 40.4 3.5 58.8 9.9C323 331 320 349.1 320 368c0 59.5 29.5 112.1 74.8 144H29.7C13.3 512 0 498.7 0 482.3C0 383.8 79.8 304 178.3 304M352 368a144 144 0 1 1 288 0a144 144 0 1 1-288 0m144-80c-8.8 0-16 7.2-16 16v64c0 8.8 7.2 16 16 16h48c8.8 0 16-7.2 16-16s-7.2-16-16-16h-32v-48c0-8.8-7.2-16-16-16"/></svg>',
user_accepted: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="mx-0 mb-1" viewBox="0 0 640 512"><path fill="currentColor" d="M96 128a128 128 0 1 1 256 0a128 128 0 1 1-256 0M0 482.3C0 383.8 79.8 304 178.3 304h91.4c98.5 0 178.3 79.8 178.3 178.3c0 16.4-13.3 29.7-29.7 29.7H29.7C13.3 512 0 498.7 0 482.3M625 177L497 305c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L591 143c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"/></svg>',
user_removed: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="mx-0 mb-1" viewBox="0 0 640 512"><path fill="currentColor" d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2s-6.3 25.5 4.1 33.7l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L353.3 251.6C407.9 237 448 187.2 448 128C448 57.3 390.7 0 320 0c-69.8 0-126.5 55.8-128 125.2zm225.5 299.2C170.5 309.4 96 387.2 96 482.3c0 16.4 13.3 29.7 29.7 29.7h388.6c3.9 0 7.6-.7 11-2.1z"/></svg>',
user_deactivated: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="mx-0 mb-1" viewBox="0 0 640 512"><path fill="currentColor" d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2s-6.3 25.5 4.1 33.7l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L353.3 251.6C407.9 237 448 187.2 448 128C448 57.3 390.7 0 320 0c-69.8 0-126.5 55.8-128 125.2zm225.5 299.2C170.5 309.4 96 387.2 96 482.3c0 16.4 13.3 29.7 29.7 29.7h388.6c3.9 0 7.6-.7 11-2.1z"/></svg>',
org_update: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="mx-0 mb-1" viewBox="0 0 16 16"><path d="M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492zM5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0z"/><path d="M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.434-.902 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52l-.094-.319z"/></svg>',
app_access: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="mx-0 mb-1" viewBox="0 0 16 16"><path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/></svg>',
role_changed: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="mx-0 mb-1" viewBox="0 0 16 16"><path d="M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 1 0-5.86 2.929 2.929 0 0 1 0 5.858z"/></svg>',
};
return icons[type] || icons.user_joined;
}
getIconBgClass(type) {
const classes = {
user_joined: 'bg-primary',
user_invited: 'bg-info',
user_accepted: 'bg-primary',
user_removed: 'bg-danger',
user_deactivated: 'bg-warning',
org_update: 'bg-warning',
app_access: 'bg-primary',
role_changed: 'bg-info',
};
return classes[type] || 'bg-primary';
}
getTimeAgo(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
if (isNaN(date)) return '';
const now = new Date();
const seconds = Math.floor((now - date) / 1000);
if (seconds < 60) return 'À l\'instant';
if (seconds < 3600) {
const mins = Math.floor(seconds / 60);
return `Il y a ${mins} ${mins > 1 ? 'mins' : 'min'}`;
}
if (seconds < 86400) {
const hours = Math.floor(seconds / 3600);
return `Il y a ${hours} ${hours > 1 ? 'h' : 'h'}`;
}
if (seconds < 604800) {
const days = Math.floor(seconds / 86400);
return `Il y a ${days} ${days > 1 ? 'j' : 'j'}`;
}
// For older dates, show a localized date string
try {
return date.toLocaleDateString('fr-FR', {year: 'numeric', month: 'short', day: 'numeric'});
} catch (e) {
return date.toLocaleDateString();
}
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
async markAsRead(event) {
event.preventDefault();
event.stopPropagation();
const notificationId = event.currentTarget.dataset.notificationId;
try {
await fetch(`/notifications/${notificationId}/read`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
this.loadNotifications();
} catch (error) {
console.error('Failed to mark notification as read:', error);
}
}
async markAllAsRead(event) {
event.preventDefault();
event.stopPropagation();
try {
await fetch('/notifications/mark-all-read', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
this.loadNotifications();
} catch (error) {
console.error('Failed to mark all as read:', error);
}
}
async deleteNotification(event) {
event.preventDefault();
event.stopPropagation();
const notificationId = event.currentTarget.dataset.notificationId;
try {
await fetch(`/notifications/${notificationId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
}
});
this.loadNotifications();
} catch (error) {
console.error('Failed to delete notification:', error);
}
}
markDropdownAsRead(event) {
}
createToastContainer() {
let container = document.getElementById('notification-toast-container');
if (!container) {
container = document.createElement('div');
container.id = 'notification-toast-container';
container.className = 'notification-toast-container';
document.body.appendChild(container);
}
return container;
}
showToast(notification) {
const toast = document.createElement('div');
toast.className = 'notification-toast';
toast.innerHTML = `
<div class="notification-toast-icon ${this.getIconBgClass(notification.type)}">
${this.getIcon(notification.type)}
</div>
<div class="notification-toast-content">
<div class="notification-toast-title">${this.escapeHtml(notification.title)}</div>
<div class="notification-toast-message">${this.escapeHtml(notification.message)}</div>
</div>
<button class="notification-toast-close" onclick="this.parentElement.remove()">×</button>
`;
this.toastContainer.appendChild(toast);
setTimeout(() => toast.classList.add('show'), 10);
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, 5000);
}
}

View File

@ -0,0 +1,227 @@
import {Controller} from '@hotwired/stimulus'
// Important: include a build with Ajax + pagination (TabulatorFull is simplest)
import {TabulatorFull as Tabulator} from 'tabulator-tables';
import {capitalizeFirstLetter, eyeIconLink, TABULATOR_FR_LANG} from "../js/global.js";
import { Modal } from "bootstrap";
export default class extends Controller {
static values = {
id: Number,
activities: Boolean,
table: Boolean,
sadmin: Boolean,
user: Number
};
static targets = ["activityList", "emptyMessage", "modal", "modalTitle", "nameInput", "emailInput", "numberInput", "addressInput"];
connect() {
if(this.activitiesValue){
this.loadActivities();
setInterval(() => {
this.loadActivities();
}, 300000); // Refresh every 5 minutes
}
if (this.tableValue && this.sadminValue) {
this.table();
}
if (this.hasModalTarget) {
this.modal = new Modal(this.modalTarget);
this.currentOrgId = null;
}
}
table(){
const table = new Tabulator("#tabulator-org", {
// Register locales here
langs: TABULATOR_FR_LANG,
placeholder: "Aucun résultat trouvé pour cette recherche",
locale: "fr", //'en' for English, 'fr' for French (en is default, no need to include it)
ajaxURL: `/organization/data`,
ajaxConfig: "GET",
pagination: true,
paginationMode: "remote",
paginationSize: 10,
//paginationSizeSelector: [5, 10, 20, 50], // Désactivé pour l'instant car jpp faire de jolie style
ajaxResponse: (url, params, response) => response,
paginationDataSent: { page: "page", size: "size" },
paginationDataReceived: { last_page: "last_page",
data: "data"},
filterMode: "remote",
ajaxURLGenerator: function(url, config, params) {
let queryParams = new URLSearchParams();
queryParams.append('page', params.page || 1);
queryParams.append('size', params.size || 10);
// Add filters
if (params.filter) {
params.filter.forEach(filter => {
queryParams.append(`filter[${filter.field}]`, filter.value);
});
}
return `${url}?${queryParams.toString()}`;
},
ajaxSorting: true,
ajaxFiltering: true,
rowHeight: 60,
layout: "fitColumns",
columns: [
{
title: "Logo",
field: "logoUrl",
formatter: "image",
formatterParams: {
height: "50px",
width: "50px",
urlPrefix: "",
urlSuffix: "",
},
width: 100,
},
{title: "Nom", field: "name", headerFilter: "input", widthGrow: 2, vertAlign: "middle", headerHozAlign: "left"},
{title: "Email", field: "email", headerFilter: "input", widthGrow: 2, vertAlign: "middle", hozAlign: "center"},
{
title: "Actions",
field: "showUrl",
hozAlign: "center",
width: 100,
vertAlign: "middle",
headerSort: false,
formatter: (cell) => {
const url = cell.getValue();
if (url) {
return eyeIconLink(url);
}
return '';
}
}],
});
}
async loadActivities() {
try {
// 1. Fetch the data using the ID from values
const response = await fetch(`/actions/organization/${this.idValue}/activities-ajax`);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const activities = await response.json();
// 2. Render
this.renderActivities(activities);
} catch (error) {
console.error('Error fetching activities:', error);
this.activityListTarget.innerHTML = `<div class="text-danger">Erreur lors du chargement.</div>`;
}
}
renderActivities(activities) {
// Clear the loading spinner
this.activityListTarget.innerHTML = '';
if (activities.length === 0) {
// Show empty message
this.activityListTarget.innerHTML = this.emptyMessageTarget.innerHTML;
return;
}
// Loop through JSON and build HTML
const html = activities.map(activity => {
return `
<div class="card shadow-sm mb-3 border-0 bg-white rounded-end"
style="border-left: 6px solid ${activity.color} !important;">
<div class="card-header bg-transparent border-0 pb-0 pt-3">
<h6 class="text-muted text-uppercase fw-bold mb-0" style="font-size: 0.85rem;">
${activity.date}
</h6>
</div>
<div class="card-body pt-2 pb-4">
<div class="card-text fs-5 lh-sm">
<span class="fw-bold text-dark">${activity.userName}</span>
<div class="text-secondary mt-1">${activity.actionType}</div>
</div>
</div>
</div>
`;
}).join('');
//com
this.activityListTarget.innerHTML = html;
}
openCreateModal() {
this.currentOrgId = null;
this.modalTitleTarget.textContent = "Créer une organisation";
this.resetForm();
this.modal.show();
}
async openEditModal(event) {
this.currentOrgId = event.currentTarget.dataset.id;
this.modalTitleTarget.textContent = "Modifier l'organisation";
try {
const response = await fetch(`/organization/editModal/${this.currentOrgId}`);
const data = await response.json();
// Fill targets
this.nameInputTarget.value = data.name;
this.emailInputTarget.value = data.email;
this.numberInputTarget.value = data.number || '';
this.addressInputTarget.value = data.address || '';
this.modal.show();
} catch (error) {
alert("Erreur lors du chargement des données.");
}
}
async submitForm(event) {
event.preventDefault();
const formData = new FormData(event.target);
const method = this.currentOrgId ? 'PUT' : 'POST';
const url = this.currentOrgId ? `/organization/${this.currentOrgId}` : `/organization/`;
if (this.currentOrgId) {
formData.append('_method', 'PUT');
}
formData.set('name', capitalizeFirstLetter(formData.get('name')));
try {
const response = await fetch(url, {
method: 'POST',
body: formData,
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
if (response.ok) {
this.modal.hide();
location.reload();
} else {
const result = await response.json();
alert(result.error || "Une erreur est survenue.");
}
} catch (e) {
alert("Erreur réseau.");
}
}
resetForm() {
this.nameInputTarget.value = "";
this.emailInputTarget.value = "";
this.numberInputTarget.value = "";
this.addressInputTarget.value = "";
}
}

View File

@ -0,0 +1,247 @@
import {Controller} from '@hotwired/stimulus';
import { Modal } from "bootstrap";
import {TabulatorFull as Tabulator} from 'tabulator-tables';
import {eyeIconLink, pencilIcon, TABULATOR_FR_LANG, trashIcon} from "../js/global.js";
export default class extends Controller {
static values = {
listProject : Boolean,
orgId: Number,
admin: Boolean
}
static targets = ["modal", "appList", "nameInput", "formTitle", "timestampSelect", "deletionSelect"];
connect(){
if(this.listProjectValue){
this.table();
}
this.modal = new Modal(this.modalTarget);
}
table(){
const columns = [
{title: "<b>ID</b> ", field: "id", visible: false},
{title: "<b>Nom du projet</b> ", field: "name", headerFilter: "input", widthGrow: 2, vertAlign: "middle"},
{
title: "Applications",
field: "applications",
headerSort: false,
hozAlign: "left",
formatter: (cell) => {
const apps = cell.getValue();
if (!apps || apps.length === 0) {
return "<span class='text-muted' style='font-size: 0.8rem;'>Aucune</span>";
}
// Wrap everything in a flex container to keep them on one line
const content = apps.map(app => `
<div class="me-1" title="${app.name}">
<img src="${app.logoMiniUrl}"
alt="${app.name}"
style="height: 35px; width: 35px; object-fit: contain; border-radius: 4px;">
</div>
`).join('');
return `<div class="d-flex flex-wrap align-items-center">${content}</div>`;
}
}
];
// 2. Add the conditional column if admin value is true
if (this.adminValue) {
columns.push({
title: "<b>Base de données</b>",
field: "bddName",
hozAlign: "left",
},
{
title: "<b>Actions</b>",
field: "id",
width: 120,
hozAlign: "center",
headerSort: false,
formatter: (cell) => {
const id = cell.getValue();
// Return a button that Stimulus can listen to
return `<div class="d-flex gap-2 align-content-center">
<button class="btn btn-link p-0 border-0" data-action="click->project#openEditModal"
data-id="${id}">
${pencilIcon()}</button>
<button class="btn btn-link p-0 border-0" data-action="click->project#deleteProject"
data-id="${id}"> ${trashIcon()} </button>
</div>`;
}
});
}
const tabulator = new Tabulator("#tabulator-projectListOrganization", {
langs: TABULATOR_FR_LANG,
locale: "fr",
ajaxURL: `/project/organization/data`,
ajaxConfig: "GET",
pagination: true,
paginationMode: "remote",
paginationSize: 15,
ajaxParams: {orgId: this.orgIdValue},
ajaxResponse: (url, params, response) => response,
paginationDataSent: {page: "page", size: "size"},
paginationDataReceived: {last_page: "last_page"},
ajaxSorting: true,
ajaxFiltering: true,
filterMode: "remote",
ajaxURLGenerator: function(url, config, params) {
let queryParams = new URLSearchParams();
queryParams.append('orgId', params.orgId);
queryParams.append('page', params.page || 1);
queryParams.append('size', params.size || 15);
// Add filters
if (params.filter) {
params.filter.forEach(filter => {
queryParams.append(`filter[${filter.field}]`, filter.value);
});
}
return `${url}?${queryParams.toString()}`;
},
rowHeight: 60,
layout: "fitColumns", // activate French
columns
})
}
async loadApplications() {
try {
const response = await fetch('/application/data/all');
const apps = await response.json();
this.appListTarget.innerHTML = apps.map(app => `
<div class="col-md-4 mb-3">
<div class="form-check border p-2 rounded d-flex align-items-center gap-2">
<input class="form-check-input ms-1" type="checkbox" name="applications[]" value="${app.id}" id="app_${app.id}">
<label class="form-check-label d-flex align-items-center gap-2" for="app_${app.id}">
<img src="${app.logoMiniUrl}" alt="${app.name}" style="height: 20px; width: 20px; object-fit: contain;">
${app.name}
</label>
</div>
</div>
`).join('');
} catch (error) {
this.appListTarget.innerHTML = '<div class="text-danger">Erreur de chargement.</div>';
}
}
async submitForm(event) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form); // This automatically picks up the 'logo' file
// 1. Validate File Format
const logoFile = formData.get('logo');
if (logoFile && logoFile.size > 0) {
const allowedTypes = ['image/jpeg', 'image/png', 'image/jpg'];
if (!allowedTypes.includes(logoFile.type)) {
alert("Format invalide. Veuillez utiliser uniquement des fichiers PNG ou JPG.");
return; // Stop submission
}
}
// 2. Prepare for Multipart sending
// Since we are using FormData, we don't need JSON.stringify or 'Content-Type': 'application/json'
// We add the extra fields to the formData object
formData.append('organizationId', this.orgIdValue);
const url = this.currentProjectId
? `/project/edit/${this.currentProjectId}/ajax`
: `/project/new/ajax`;
const response = await fetch(url, {
method: 'POST',
// IMPORTANT: Do NOT set Content-Type header when sending FormData with files
// The browser will set 'multipart/form-data' and the boundary automatically
body: formData
});
if (response.ok) {
this.modal.hide();
location.reload();
} else {
const result = await response.json();
if (response.status === 409) {
alert("Un projet avec ce nom existe déjà.");
} else {
alert(result.error || "Une erreur est survenue.");
}
}
}
async openEditModal(event) {
const projectId = event.currentTarget.dataset.id;
this.currentProjectId = projectId;
this.modal.show();
this.nameInputTarget.disabled = true;
this.formTitleTarget.textContent = "Modifier le projet";
try {
// 1. Ensure checkboxes are loaded first
await this.loadApplications();
// 2. Fetch the project data
const response = await fetch(`/project/data/${projectId}`);
const project = await response.json();
// 3. Set the name
this.nameInputTarget.value = project.name;
console.log(project);
this.timestampSelectTarget.value = project.timestampPrecision;
this.deletionSelectTarget.value = project.deletionAllowed;
// 4. Check the boxes
// We look for all checkboxes inside our appList target
const checkboxes = this.appListTarget.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach(cb => {
cb.checked = project.applications.includes(cb.value);
});
} catch (error) {
console.error("Error loading project data", error);
alert("Erreur lors de la récupération des données du projet.");
}
}
// Update your openCreateModal to reset the state
openCreateModal() {
this.currentProjectId = null;
this.modal.show();
this.nameInputTarget.disabled = false;
this.nameInputTarget.value = "";
this.formTitleTarget.textContent = "Nouveau Projet";
this.loadApplications();
}
async deleteProject(event) {
const projectId = event.currentTarget.dataset.id;
if (!confirm("Êtes-vous sûr de vouloir supprimer ce projet ?")) {
return;
}
try {
const response = await fetch(`/project/delete/${projectId}/ajax`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
if (response.ok) {
location.reload();
}
}catch (error) {
console.error("Error deleting project", error);
alert("Erreur lors de la suppression du projet.");
}
}
}

File diff suppressed because it is too large Load Diff

View File

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

After

Width:  |  Height:  |  Size: 965 B

View File

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

After

Width:  |  Height:  |  Size: 598 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="currentColor" d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5M1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4z"/></svg>

After

Width:  |  Height:  |  Size: 272 B

View File

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

After

Width:  |  Height:  |  Size: 236 B

View File

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

After

Width:  |  Height:  |  Size: 236 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g fill="currentColor"><path fill-rule="evenodd" d="M5 2a.5.5 0 0 1 .5-.5c.862 0 1.573.287 2.06.566c.174.099.321.198.44.286c.119-.088.266-.187.44-.286A4.17 4.17 0 0 1 10.5 1.5a.5.5 0 0 1 0 1c-.638 0-1.177.213-1.564.434a3.5 3.5 0 0 0-.436.294V7.5H9a.5.5 0 0 1 0 1h-.5v4.272c.1.08.248.187.436.294c.387.221.926.434 1.564.434a.5.5 0 0 1 0 1a4.17 4.17 0 0 1-2.06-.566A5 5 0 0 1 8 13.65a5 5 0 0 1-.44.285a4.17 4.17 0 0 1-2.06.566a.5.5 0 0 1 0-1c.638 0 1.177-.213 1.564-.434c.188-.107.335-.214.436-.294V8.5H7a.5.5 0 0 1 0-1h.5V3.228a3.5 3.5 0 0 0-.436-.294A3.17 3.17 0 0 0 5.5 2.5A.5.5 0 0 1 5 2"/><path d="M10 5h4a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-4v1h4a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-4zM6 5V4H2a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h4v-1H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1z"/></g></svg>

After

Width:  |  Height:  |  Size: 827 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="currentColor" d="M6.5 1h3a.5.5 0 0 1 .5.5v1H6v-1a.5.5 0 0 1 .5-.5M11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3A1.5 1.5 0 0 0 5 1.5v1H1.5a.5.5 0 0 0 0 1h.538l.853 10.66A2 2 0 0 0 4.885 16h6.23a2 2 0 0 0 1.994-1.84l.853-10.66h.538a.5.5 0 0 0 0-1zm1.958 1l-.846 10.58a1 1 0 0 1-.997.92h-6.23a1 1 0 0 1-.997-.92L3.042 3.5zm-7.487 1a.5.5 0 0 1 .528.47l.5 8.5a.5.5 0 0 1-.998.06L5 5.03a.5.5 0 0 1 .47-.53Zm5.058 0a.5.5 0 0 1 .47.53l-.5 8.5a.5.5 0 1 1-.998-.06l.5-8.5a.5.5 0 0 1 .528-.47M8 4.5a.5.5 0 0 1 .5.5v8.5a.5.5 0 0 1-1 0V5a.5.5 0 0 1 .5-.5"/></svg>

After

Width:  |  Height:  |  Size: 609 B

View File

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

After

Width:  |  Height:  |  Size: 425 B

View File

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

After

Width:  |  Height:  |  Size: 428 B

View File

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

After

Width:  |  Height:  |  Size: 528 B

View File

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

After

Width:  |  Height:  |  Size: 787 B

View File

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

After

Width:  |  Height:  |  Size: 548 B

View File

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

After

Width:  |  Height:  |  Size: 152 B

View File

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

After

Width:  |  Height:  |  Size: 473 B

View File

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

After

Width:  |  Height:  |  Size: 560 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M4 20q-.825 0-1.412-.587T2 18V6q0-.825.588-1.412T4 4h16q.825 0 1.413.588T22 6v12q0 .825-.587 1.413T20 20zm0-2h16V6H4zm0 0V6zm8-5h6q.425 0 .713-.288T19 12V8q0-.425-.288-.712T18 7h-6q-.425 0-.712.288T11 8v4q0 .425.288.713T12 13m1-2V9h4v2z"/></svg>

After

Width:  |  Height:  |  Size: 334 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M11 17h2l.3-1.5q.3-.125.563-.262t.537-.338l1.45.45l1-1.7l-1.15-1q.05-.35.05-.65t-.05-.65l1.15-1l-1-1.7l-1.45.45q-.275-.2-.537-.338T13.3 8.5L13 7h-2l-.3 1.5q-.3.125-.562.263T9.6 9.1l-1.45-.45l-1 1.7l1.15 1q-.05.35-.05.65t.05.65l-1.15 1l1 1.7l1.45-.45q.275.2.538.338t.562.262zm1-3q-.825 0-1.412-.587T10 12t.588-1.412T12 10t1.413.588T14 12t-.587 1.413T12 14m-7 7q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h14q.825 0 1.413.588T21 5v14q0 .825-.587 1.413T19 21zm0-2h14V5H5zM5 5v14z"/></svg>

After

Width:  |  Height:  |  Size: 577 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
assets/img/sudalys_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 888 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 888 B

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

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

86
assets/js/global.js Normal file
View File

@ -0,0 +1,86 @@
export const TABULATOR_FR_LANG = {
fr: {
ajax: {loading: "Chargement...", error: "Erreur"},
pagination: {
page_size: "Taille de page",
page_title: "Afficher la page",
first: "Premier",
first_title: "Première page",
last: "Dernier",
last_title: "Dernière page",
prev: "Précédent",
prev_title: "Page précédente",
next: "Suivant",
next_title: "Page suivante",
all: "Tout",
counter: {showing: "Affiche", of: "de", rows: "lignes", pages: "pages"},
},
headerFilters: {default: "Filtrer la colonne...", columns: {}},
data: {loading: "Chargement des données...", error: "Erreur de chargement des données"},
groups: {item: "élément", items: "éléments"},
},
};
export function eyeIconLink(url) {
return `<a href="${url}" class="p-3 align-middle color-primary" title="Accéder au profil" >
<svg xmlns="http://www.w3.org/2000/svg"
width="35px"
height="35px"
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>
</a>`;
}
export function pencilIcon() {
return `
<span class="align-middle color-primary">
<svg xmlns="http://www.w3.org/2000/svg" width="35px" height="35px" viewBox="0 0 24 24">
<path fill="currentColor" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168zM11.207 2.5L13.5 4.793L14.793 3.5L12.5 1.207zm1.586 3L10.5 3.207L4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293zm-9.761 5.175l-.106.106l-1.528 3.821l3.821-1.528l.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325"/></svg>
</span>
`
}
export function trashIcon() {
return `
<span class="align-middle color-delete">
<svg xmlns="http://www.w3.org/2000/svg" width="35px" height="35px" viewBox="0 0 24 24">
<path fill="currentColor" d="M6.5 1h3a.5.5 0 0 1 .5.5v1H6v-1a.5.5 0 0 1 .5-.5M11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3A1.5 1.5 0 0 0 5 1.5v1H1.5a.5.5 0 0 0 0 1h.538l.853 10.66A2 2 0 0 0 4.885 16h6.23a2 2 0 0 0 1.994-1.84l.853-10.66h.538a.5.5 0 0 0 0-1zm1.958 1l-.846 10.58a1 1 0 0 1-.997.92h-6.23a1 1 0 0 1-.997-.92L3.042 3.5zm-7.487 1a.5.5 0 0 1 .528.47l.5 8.5a.5.5 0 0 1-.998.06L5 5.03a.5.5 0 0 1 .47-.53Zm5.058 0a.5.5 0 0 1 .47.53l-.5 8.5a.5.5 0 1 1-.998-.06l.5-8.5a.5.5 0 0 1 .528-.47M8 4.5a.5.5 0 0 1 .5.5v8.5a.5.5 0 0 1-1 0V5a.5.5 0 0 1 .5-.5"/></svg>
</span>
`
}
export function trashIconForm(url, organizationId) {
return `
<button class="btn btn-link p-0 border-0 color-delete align-middle"
data-action="click->user#removeAdmin"
data-url="${url}"
data-org-id="${organizationId}">
<svg xmlns="http://www.w3.org/2000/svg" width="35px" height="35px" viewBox="0 0 24 24">
<path fill="currentColor" d="M6.5 1h3a.5.5 0 0 1 .5.5v1H6v-1a.5.5 0 0 1 .5-.5M11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3A1.5 1.5 0 0 0 5 1.5v1H1.5a.5.5 0 0 0 0 1h.538l.853 10.66A2 2 0 0 0 4.885 16h6.23a2 2 0 0 0 1.994-1.84l.853-10.66h.538a.5.5 0 0 0 0-1zm1.958 1l-.846 10.58a1 1 0 0 1-.997.92h-6.23a1 1 0 0 1-.997-.92L3.042 3.5zm-7.487 1a.5.5 0 0 1 .528.47l.5 8.5a.5.5 0 0 1-.998.06L5 5.03a.5.5 0 0 1 .47-.53Zm5.058 0a.5.5 0 0 1 .47.53l-.5 8.5a.5.5 0 1 1-.998-.06l.5-8.5a.5.5 0 0 1 .528-.47M8 4.5a.5.5 0 0 1 .5.5v8.5a.5.5 0 0 1-1 0V5a.5.5 0 0 1 .5-.5"/></svg>
</button>`;
}
export function deactivateUserIcon() {
return `<svg xmlns="http://www.w3.org/2000/svg" width="35" height="35" viewBox="0 0 640 512">
<path fill="currentColor" d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2s-6.3 25.5 4.1 33.7l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L353.3 251.6C407.9 237 448 187.2 448 128C448 57.3 390.7 0 320 0c-69.8 0-126.5 55.8-128 125.2zm225.5 299.2C170.5 309.4 96 387.2 96 482.3c0 16.4 13.3 29.7 29.7 29.7h388.6c3.9 0 7.6-.7 11-2.1z"/>
</svg>`
}
export function activateUserIcon() {
return `<svg xmlns="http://www.w3.org/2000/svg" width="35" height="35" viewBox="0 0 640 512">
<path fill="currentColor" d="M96 128a128 128 0 1 1 256 0a128 128 0 1 1-256 0M0 482.3C0 383.8 79.8 304 178.3 304h91.4c98.5 0 178.3 79.8 178.3 178.3c0 16.4-13.3 29.7-29.7 29.7H29.7C13.3 512 0 498.7 0 482.3M625 177L497 305c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L591 143c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"/>
</svg>`
}
export function sendEmailIcon(userId, orgId) {
return `<a href="#" class="color-primary resend-invitation pt-3" data-id="${userId}" data-org-id="${orgId}" title="Renvoyer l'invitation" >
<svg xmlns="http://www.w3.org/2000/svg" width="35px" height="35px" viewBox="0 0 24 24"><path fill="currentColor" d="M18.175 17H15q-.425 0-.712-.288T14 16t.288-.712T15 15h3.175l-.9-.9Q17 13.825 17 13.413t.3-.713q.275-.275.7-.275t.7.275l2.6 2.6q.125.125.2.312t.075.388t-.075.387t-.2.313l-2.6 2.6q-.275.275-.687.288T17.3 19.3q-.275-.275-.275-.7t.275-.7zM4 17q-.825 0-1.412-.587T2 15V5q0-.825.588-1.412T4 3h13q.825 0 1.413.588T19 5v4.075q0 .4-.3.7t-.7.3q-.425 0-.712-.288T17 9.076V6.4L10.4 11L4 6.425V15h7.075q.425 0 .713.288t.287.712t-.287.713t-.713.287zM5.45 5l4.95 3.55L15.5 5zM4 15V5z"/></svg>
</a>`
}
export function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}

View File

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

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

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

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

View File

@ -1,3 +1,19 @@
/*variable*/
:root{
--primary-blue-light : #086572;
--primary-blue-dark : #094754;
--black-font: #1D1E1C;
--delete : #E42E31;
--delete-dark : #aa1618;
--disable : #A3A3A3;
--check : #5cae09;
--check-dark: #3a6e05;
--secondary : #cc664c;
--secondary-dark : #a5543d;
--warning : #d2b200;
--warning-dark: #c4a600;
}
html { html {
margin: 0; margin: 0;
padding: 0; padding: 0;
@ -31,3 +47,141 @@ body {
overflow: hidden; 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-success{
background: var(--check);
color : #FFFFFF;
border: var(--check-dark);
border-radius: 1rem;
}
.btn-success:hover{
background: var(--check-dark);
color : #FFFFFF;
border: var(--check);
}
.btn-warning{
background: var(--warning);
color : #FFFFFF;
border: var(--warning-dark);
border-radius: 1rem;
}
.btn-warning:hover{
background: var(--warning-dark);
color : #FFFFFF;
border: var(--warning);
}
.btn-danger{
background: var(--delete);
color : #FFFFFF;
border: var(--delete-dark);
border-radius: 1rem;
}
.btn-danger:hover{
background: var(--delete-dark);
color : #FFFFFF;
border: var(--delete);
}
.color-primary{
color: var(--primary-blue-light) !important;
}
.color-primary-dark{
color: var(--primary-blue-dark);
}
.color-delete{
color: var(--delete) !important;
}
.color-delete-dark{
color: var(--delete-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);
}
.btn-warning{
border-radius: 1rem;
}
.color-secondary{
color: var(--secondary);
}
.bg-primary{
background-color: var(--primary-blue-light) !important;
}
.bg-warning{
background-color: var(--secondary) !important;
}

4
assets/styles/card.css Normal file
View File

@ -0,0 +1,4 @@
.card.no-header-bg .card-header{
background-color: transparent !important;
border-bottom: none;
}

63
assets/styles/choices.css Normal file
View File

@ -0,0 +1,63 @@
.choices {
font-size: 0.9rem;
width: 100%;
}
/* Input style */
.choices__inner {
background: #fff;
border: 1px solid var(--primary-blue-light);
border-radius: 0.375rem; /* same as Bootstrap `.form-control` */
padding: 0.5rem;
min-height: 2.5rem;
box-shadow: none;
cursor: text;
}
/* Placeholder */
.choices__placeholder {
color: #6c757d; /* Bootstrap muted */
opacity: 0.9;
}
/* Selected items (tags) */
.choices__list--multiple .choices__item {
background-color: var(--primary-blue-light) !important;
color: #fff;
border: none;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
margin: 0.15rem;
font-size: 0.85rem;
}
/* Remove "x" button */
.choices__list--multiple .choices__item .choices__button {
border-left: 1px solid rgba(255,255,255,0.3);
margin-left: 0.25rem;
color: #fff;
opacity: 0.9;
}
.choices__list--multiple .choices__item .choices__button:hover {
opacity: 1;
}
/* Dropdown list */
.choices__list--dropdown {
border: 1px solid var(--primary-blue-light);
border-radius: 0.25rem;
box-shadow: 0 3px 6px rgba(0,0,0,0.1);
margin-top: 0.2rem;
}
/* Dropdown options */
.choices__list--dropdown .choices__item {
padding: 0.5rem;
font-size: 0.9rem;
}
/* Hover/active in dropdown */
.choices__list--dropdown .choices__item--highlighted {
background-color: var(--primary-blue-light);
color: #fff;
}

View File

@ -69,8 +69,11 @@
border-radius: 0; border-radius: 0;
} }
.navbar-nav-right{ .navbar .navbar-menu-wrapper .navbar-toggler:active,
flex-direction: row; .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) { .navbar .navbar-menu-wrapper .navbar-toggler:not(.navbar-toggler-right) {
@ -82,12 +85,24 @@
transition: transform 0.3s linear; 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{ .navbar .navbar-menu-wrapper .navbar-nav .nav-item.nav-search .input-group .form-control{
margin-left: 0.7rem; margin-left: 0.7rem;
} }
#navbar-search-icon > #search{ #navbar-search-icon > #search{
vertical-align: middle; vertical-align: baseline;
} }
.navbar .navbar-menu-wrapper .navbar-nav.navbar-nav-right { .navbar .navbar-menu-wrapper .navbar-nav.navbar-nav-right {
@ -244,4 +259,16 @@
#logo_orga{ #logo_orga{
width:auto; width:auto;
max-height:40px; 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;
} }

View File

@ -0,0 +1,156 @@
.notification-toast-container {
position: fixed;
top: 80px;
right: 20px;
z-index: 9999;
display: flex;
flex-direction: column;
gap: 10px;
pointer-events: none;
}
.notification-toast {
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
padding: 16px;
min-width: 320px;
max-width: 400px;
display: flex;
align-items: flex-start;
gap: 12px;
opacity: 0;
transform: translateX(400px);
transition: all 0.3s ease;
pointer-events: all;
}
.notification-toast.show {
opacity: 1;
transform: translateX(0);
}
.notification-toast-icon {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: white;
}
.notification-toast-icon svg {
width: 20px;
height: 20px;
}
.notification-toast-content {
flex: 1;
min-width: 0;
}
.notification-toast-title {
font-weight: 600;
font-size: 14px;
margin-bottom: 4px;
color: #333;
}
.notification-toast-message {
font-size: 13px;
color: #666;
line-height: 1.4;
}
.notification-toast-close {
background: none;
border: none;
font-size: 24px;
line-height: 1;
color: #999;
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
flex-shrink: 0;
transition: color 0.2s;
}
.notification-toast-close:hover {
color: #333;
}
.nav-notif .count-notification {
position: absolute;
top: 15px;
right: -5px;
background: var(--primary-blue-light);
color: white;
border-radius: 12px;
padding: 3px 7px;
font-size: 8px;
font-weight: bold;
min-width: 5px;
height: 10px;
line-height:0.5;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.dropdown-item.preview-item {
display: flex;
align-items: flex-start;
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
transition: background-color 0.2s;
text-decoration: none;
color: inherit;
}
.dropdown-item.preview-item:hover {
background-color: #f8f9fa;
}
.dropdown-item.preview-item:last-child {
border-bottom: none;
}
.preview-thumbnail {
margin-right: 12px;
}
.preview-icon {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.preview-icon svg,
.preview-icon i {
width: 20px;
height: 20px;
}
.preview-item-content {
flex: 1;
min-width: 0;
}
.preview-subject {
font-size: 14px;
font-weight: 600;
margin-bottom: 4px;
color: #333;
}
.preview-item-content p {
font-size: 12px;
margin-bottom: 0;
line-height: 1.4;
}

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

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

114
assets/styles/tabulator.css Normal file
View File

@ -0,0 +1,114 @@
/* Remove outer table border */
.tabulator {
border: none !important;
font-size: 18px !important;
}
/* Remove header and row cell borders */
.tabulator-header,
.tabulator-header .tabulator-col,
.tabulator-tableholder,
.tabulator-table,
.tabulator-row,
.tabulator-row .tabulator-cell {
border: none !important;
}
/* Remove column header bottom border and row separators */
.tabulator-header {
border-bottom: none !important;
background-color: transparent !important;
/*border-top-left-radius: 25%;*/
/*border-top-right-radius: 25%;*/
}
.tabulator-row {
border-bottom: none !important;
background-color: transparent !important;
}
/* Remove look on hover/selected without borders */
.tabulator-row:hover {
box-shadow: none !important;
}
.tabulator-row.tabulator-selected {
box-shadow: none !important;
}
.tabulator-row.tabulator-row-odd {
background-color: transparent !important;
}
/* Rounded border for images in cells */
.tabulator-cell img {
border-radius: 50%;
object-fit: cover;
}
/* Scope to this table only */
.tabulator,
.tabulator-header,
.tabulator-header .tabulator-header-contents,
.tabulator-header .tabulator-col{
background: none !important;
}
.tabulator-footer {border-top: none !important;
background-color: transparent !important;
}
.tabulator-footer .tabulator-page.active{
background-color: var(--primary-blue-light) !important;
border: 1px solid var(--primary-blue-light) !important;
color: #FFFFFF/* text color */ !important
}
.tabulator-footer .tabulator-page {
background-color: transparent !important;
border: 1px solid var(--primary-blue-light) !important;
color: var(--black-font)/* text color */ !important;
}
.tabulator-footer .tabulator-page:hover,
.tabulator-footer .tabulator-page.active:hover{
background-color: var(--primary-blue-dark) !important;
border: 1px solid var(--primary-blue-dark) !important;
color: #FFFFFF/* text color */ !important
}
.tabulator-footer select{
border: 1px solid var(--primary-blue-light) !important;
background-color: transparent !important;
color: var(--black-font)/* text color */ !important;
}
.tabulator-header input{
border: 0;
border-radius: 10px;
height: 40px;
background-color: lightgray !important;
padding-left: 15px !important;
}
.tabulator-header input::placeholder{
color: var(--black-font) !important;
font-size: 14px !important;
opacity: 1 !important; /* Firefox */
}
.tabulator-header input:focus {
border:0;
}
.tabulator .tabulator-header .tabulator-col .tabulator-col-title {
font-size: 18px !important;
}
/* Select hover Désactivé pour l'instant car jpp faire de jolie style */
/*#tabulator-org .tabulator-footer select:hover {*/
/* border: 1px solid var(--primary-blue-dark) !important;*/
/* background-color: var(--primary-blue-dark) !important;*/
/* color: #fff !important;*/
/*}*/
/*.tabulator-footer select:focus {*/
/* border: 1px solid var(--primary-blue-dark) !important;*/
/* outline: none !important;*/
/* background-color: var(--primary-blue-dark) !important;*/
/* color: #fff !important;*/
/*}*/

View File

@ -8,48 +8,52 @@
"ext-ctype": "*", "ext-ctype": "*",
"ext-iconv": "*", "ext-iconv": "*",
"ext-openssl": "*", "ext-openssl": "*",
"aws/aws-sdk-php-symfony": "^2.8",
"doctrine/dbal": "^3", "doctrine/dbal": "^3",
"doctrine/doctrine-bundle": "^2.14", "doctrine/doctrine-bundle": "^2.14",
"doctrine/doctrine-migrations-bundle": "^3.4", "doctrine/doctrine-migrations-bundle": "^3.4",
"doctrine/orm": "^3.3", "doctrine/orm": "^3.3",
"firebase/php-jwt": "^6.11", "firebase/php-jwt": "^7.0.0",
"knplabs/knp-time-bundle": "^2.4",
"league/oauth2-server-bundle": "^0.11.0", "league/oauth2-server-bundle": "^0.11.0",
"nelmio/cors-bundle": "^2.5", "nelmio/cors-bundle": "^2.5",
"phpdocumentor/reflection-docblock": "^5.6", "phpdocumentor/reflection-docblock": "^5.6",
"phpstan/phpdoc-parser": "^2.1", "phpstan/phpdoc-parser": "^2.1",
"symfony/asset": "7.2.*", "symfony/asset": "7.4.*",
"symfony/asset-mapper": "7.2.*", "symfony/asset-mapper": "7.4.*",
"symfony/console": "7.2.*", "symfony/console": "7.4.*",
"symfony/doctrine-messenger": "7.2.*", "symfony/doctrine-messenger": "7.4.*",
"symfony/dotenv": "7.2.*", "symfony/dotenv": "7.4.*",
"symfony/expression-language": "7.2.*", "symfony/expression-language": "7.4.*",
"symfony/flex": "^2", "symfony/flex": "^2",
"symfony/form": "7.2.*", "symfony/form": "7.4.*",
"symfony/framework-bundle": "7.2.*", "symfony/framework-bundle": "7.4.*",
"symfony/http-client": "7.2.*", "symfony/http-client": "7.4.*",
"symfony/intl": "7.2.*", "symfony/intl": "7.4.*",
"symfony/mailer": "7.2.*", "symfony/mailer": "7.4.*",
"symfony/mercure-bundle": "^0.3.9", "symfony/mercure-bundle": "^0.3.9",
"symfony/mime": "7.2.*", "symfony/messenger": "7.4.*",
"symfony/monolog-bundle": "^3.0", "symfony/mime": "7.4.*",
"symfony/notifier": "7.2.*", "symfony/monolog-bundle": "^3.10",
"symfony/process": "7.2.*", "symfony/notifier": "7.4.*",
"symfony/property-access": "7.2.*", "symfony/process": "7.4.*",
"symfony/property-info": "7.2.*", "symfony/property-access": "7.4.*",
"symfony/runtime": "7.2.*", "symfony/property-info": "7.4.*",
"symfony/security-bundle": "7.2.*", "symfony/rate-limiter": "7.4.*",
"symfony/serializer": "7.2.*", "symfony/runtime": "7.4.*",
"symfony/security-bundle": "7.4.*",
"symfony/serializer": "7.4.*",
"symfony/stimulus-bundle": "^2.24", "symfony/stimulus-bundle": "^2.24",
"symfony/string": "7.2.*", "symfony/string": "7.4.*",
"symfony/translation": "7.2.*", "symfony/translation": "7.4.*",
"symfony/twig-bundle": "7.2.*", "symfony/twig-bundle": "7.4.*",
"symfony/ux-icons": "^2.24", "symfony/ux-icons": "^2.24",
"symfony/ux-toggle-password": "^2.24", "symfony/ux-toggle-password": "^2.24",
"symfony/ux-turbo": "^2.24", "symfony/ux-turbo": "^2.24",
"symfony/validator": "7.2.*", "symfony/validator": "7.4.*",
"symfony/web-link": "7.2.*", "symfony/web-link": "7.4.*",
"symfony/yaml": "7.2.*", "symfony/webhook": "7.4.*",
"twig/extra-bundle": "^2.12|^3.0", "symfony/yaml": "7.4.*",
"twig/twig": "^2.12|^3.0" "twig/twig": "^2.12|^3.0"
}, },
"config": { "config": {
@ -100,17 +104,18 @@
"extra": { "extra": {
"symfony": { "symfony": {
"allow-contrib": false, "allow-contrib": false,
"require": "7.2.*" "require": "7.4.*"
} }
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^9.5", "dama/doctrine-test-bundle": "^8.3",
"symfony/browser-kit": "7.2.*", "phpunit/phpunit": "^11.0",
"symfony/css-selector": "7.2.*", "symfony/browser-kit": "7.4.*",
"symfony/debug-bundle": "7.2.*", "symfony/css-selector": "7.4.*",
"symfony/debug-bundle": "7.4.*",
"symfony/maker-bundle": "^1.62", "symfony/maker-bundle": "^1.62",
"symfony/phpunit-bridge": "^7.2", "symfony/phpunit-bridge": "7.4.*",
"symfony/stopwatch": "7.2.*", "symfony/stopwatch": "7.4.*",
"symfony/web-profiler-bundle": "7.2.*" "symfony/web-profiler-bundle": "7.4.*"
} }
} }

11436
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,30 +0,0 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFHDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQIQ5VGud/OnzsCAggA
MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECH23Z4CwP/AxBIIEyCUA/sYfp1Vo
x+pBPHs8EH5zW+oeCCqihANPGcYXHp3SusSQEA6jyEQz1GHEQF7nt5SxxqYTfe8D
ovU2TsspUlycUcZhQQmtM6n2f375G9L2fvIhnOMvD34YvYQ0yhgkToS3h+pORpqb
9abJbqkyTHSHOD4utSEaUnFapAQnKPWgFzYSjt4w6pVfX2AUZCXisC3QlcUh4782
S62jIfqggMcDU2mfkBBUHE7z1syC5eqYwWEs2P1S1fvSYjpFv7L8LlkZY+6w/qgf
IH1ht0A+rs6cQKILXW9yySqRfuumaGJtqQvlql4M7tb7zvDynVgN3/GQxm6oQmFB
pFlNWINN7WmKqhRjnJBpKl5L1+s2EWpxBiHXXQLdznqzKyx3tPuLoeVh2S96NEy6
f9380eDhNBYKdyUeZP+IBdwtV6U6vbwhtBQ53T9BQ6ykbKpRUm1fI4Y6AcVYW903
KCkFR3j6pF4My41AMgcBSJ+GON/PEzIgIIDlYnM+7DVhlentY71Bhf9la7YSaFtx
0r8d/CKdgGwPB2+QAEANOo0Z73RmovNRyadvZTcJqEgRC9R+DRbQHNYXN44TEhIU
MZaFLTTZHEmWs0f4kf+UUo7RjUi5HdNCjuXErvKgVhtqMofeKc1p6fnNbBCrFYzs
Knbwn+u2aLcI7LNLXaSWMOOGrbBw3g5FWObCxZ/FlNABp4yE+YT5YYeCLcemS/8n
ZD0l3b2wkLK7dze0DB3TKv+wZoBHtl1s3RYm9UDjB8ejHSzzRMpLK514+Q8Qthx9
IexC6hTzIG3wyjst98spb/6VQqDyA4TvMzW+pAA+9JgXyp7auHiU217nx5rSpdTe
hWCyRgaBOP48W/ZoBjVxx81/voKixfvBZbYJSciw7K4iNRMjPXNg2BvCZZLZ9Ubd
zLVCE15bzdGDZJis/VLawEFHZ1Urku9HfT4FhLxAweEc5VdnbdjEXIXqgf99VaK2
V6xXWF9EQ1pVaavR+jzm0ZHs5Ltwnc095+Y9flmvzm+eD00Ftm1IyrfInMl6f8Sv
gkxe1lxo6wbTE12CodhkH0woNd+y4K82ZSRzaTg6cUb+YTDKNAfoAnctghPe5+PX
3j1ITAQeBFjVXckDrm8jeqF/gbcQYJwFjY0/wtl84gN/Ffs8DSTt1FYwsxgGew43
L0KsVt/1Rd4Xb4czBRk7uUpR0U2R7iZ0yIrnGSh657tywpcn2thsJY4X6QjNRWNw
yDg5Yrw10tIsLYJLJuxAauGjXbk23lelwnwn8EtMYPI2XtzGSS3b7uGXjTu2pncQ
YVFbI/DxrRi3tyF7JLSC1mfNxikMfY6cPp489djDWC0iQVcltrHiHDkmMrloptE7
tTUvPfBRNAbonMaXR+pSSQBlDkUMk6uNpk9EYIlLGg2WggOYnikEKsLoX19Mjg5M
ZuX7fu2ETHJjvHBetMnQobtqKEGf5RJn+mdKBzia9ZAcjxhKRqYUCK8Qoi3mjvJa
x+5idCj2etNJA5idmH50NSXSFbO69Au+LCj1zSikN6h+YgOjqwF0TPQ7Qw4qQQkH
4NwYi6S4wCI13LyLVN1XTh9nn99Y/WnORyyegmAfQif1vbcPcfU6mY3VGK5tX2Yl
NhLAyGw7m2xDDmIefpRi+w==
-----END ENCRYPTED PRIVATE KEY-----

View File

@ -1,9 +0,0 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwLRhFcrWQWeuGhgrFclT
c08RTX52kl9XMXatgJi/mfa/o7lbZbhQfNzrMXVZFDTs97YTZXotZVwL1pI7yGtv
bjbPNuhs+lvOSmWribIacYx47EqKeYhQ29rOYx7fnr+SvK10QZoFnT58tduQVrER
79b+lcbqKMKNeI6zZeVdBrhstuI/PtXnM4kMHThaTz3iLbWkoDyl06VIMFVsvHpc
HaPJsBH1I45M9l4gTfshknY3Dz5ESVd41XTH2c/PSa6geNUn5kpslCuxKv3DnmWE
h+V87ASCIM7tKE6bc0eFQLKPQo52/TUWfDa8nFbeIrbsQJwq5VhYK21TANNFFL3g
GwIDAQAB
-----END PUBLIC KEY-----

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

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

View File

@ -0,0 +1,5 @@
when@test:
dama_doctrine_test:
enable_static_connection: true
enable_static_meta_data_cache: true
enable_static_query_cache: true

View File

@ -1,17 +1,21 @@
# see https://symfony.com/doc/current/reference/configuration/framework.html # see https://symfony.com/doc/current/reference/configuration/framework.html
framework: framework:
secret: '%env(APP_SECRET)%' secret: '%env(APP_SECRET)%'
trusted_proxies: '%env(TRUSTED_PROXY)%' csrf_protection: true
http_method_override: true
annotations: false
handle_all_throwables: true
trusted_proxies: '%env(TRUSTED_PROXY)%'
# Note that the session will be started ONLY if you read or write from it.
session: true
#esi: true # Note that the session will be started ONLY if you read or write from it.
#fragments: true session: true
#esi: true
#fragments: true
when@test: when@test:
framework: framework:
test: true test: true
session: session:
storage_factory_id: session.storage.factory.mock_file storage_factory_id: session.storage.factory.mock_file

View File

@ -3,9 +3,9 @@ league_oauth2_server:
private_key: '%env(resolve:OAUTH_PRIVATE_KEY)%' private_key: '%env(resolve:OAUTH_PRIVATE_KEY)%'
private_key_passphrase: '%env(resolve:OAUTH_PASSPHRASE)%' private_key_passphrase: '%env(resolve:OAUTH_PASSPHRASE)%'
encryption_key: '%env(resolve:OAUTH_ENCRYPTION_KEY)%' encryption_key: '%env(resolve:OAUTH_ENCRYPTION_KEY)%'
access_token_ttl: PT3H # 3 hours access_token_ttl: PT15M # 15 minutes
refresh_token_ttl: P1M # 1 month refresh_token_ttl: P7D # 7 days
auth_code_ttl: PT3H # 10 minutes auth_code_ttl: PT30M # 30 minutes
require_code_challenge_for_public_clients: false require_code_challenge_for_public_clients: false
resource_server: resource_server:
public_key: '%env(resolve:OAUTH_PUBLIC_KEY)%' public_key: '%env(resolve:OAUTH_PUBLIC_KEY)%'

View File

@ -6,3 +6,4 @@ mercure:
jwt: jwt:
secret: '%env(MERCURE_JWT_SECRET)%' secret: '%env(MERCURE_JWT_SECRET)%'
publish: '*' publish: '*'
subscribe: '*'

View File

@ -1,27 +1,99 @@
monolog: monolog:
channels: channels:
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists - user_management
- authentication
- organization_management
- access_control
- email_notifications
- admin_actions
- security
- php
- error
- aws_management
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
when@dev: when@dev:
monolog: monolog:
handlers: handlers:
main: critical_errors:
type: stream type: fingers_crossed
path: "%kernel.logs_dir%/%kernel.environment%.log" action_level: critical
level: debug handler: error_nested
channels: ["!event"] buffer_size: 50
# uncomment to get logging in your browser
# you may have to allow bigger header sizes in your Web server configuration error_nested:
#firephp: type: rotating_file
# type: firephp path: "%kernel.logs_dir%/error.log"
# level: info level: debug
#chromephp: max_files: 30
# type: chromephp
# level: info error:
console: type: rotating_file
type: console path: "%kernel.logs_dir%/error.log"
process_psr_3_messages: false level: error # logs error, critical, alert, emergency
channels: ["!event", "!doctrine", "!console"] max_files: 30
channels: [ error ]
php_errors:
type: rotating_file
path: "%kernel.logs_dir%/php_error.log"
level: warning # warnings, errors, fatals…
max_files: 30
channels: [ php ]
# User Management
user_management:
type: rotating_file
path: "%kernel.logs_dir%/user_management.log"
level: debug
channels: [ user_management ]
max_files: 30
# Authentication
authentication:
type: rotating_file
path: "%kernel.logs_dir%/authentication.log"
level: debug
channels: [ authentication ]
max_files: 30
# Organization Management
organization_management:
type: rotating_file
path: "%kernel.logs_dir%/organization_management.log"
level: debug
channels: [ organization_management ]
max_files: 30
# Access Control
access_control:
type: rotating_file
path: "%kernel.logs_dir%/access_control.log"
level: debug
channels: [ access_control ]
max_files: 30
# Email Notifications
email_notifications:
type: rotating_file
path: "%kernel.logs_dir%/email_notifications.log"
level: debug
channels: [ email_notifications ]
max_files: 30
# Admin Actions
admin_actions:
type: rotating_file
path: "%kernel.logs_dir%/admin_actions.log"
level: debug
channels: [ admin_actions ]
max_files: 30
# Security
security:
type: rotating_file
path: "%kernel.logs_dir%/security.log"
level: debug
channels: [ security ]
max_files: 30
when@test: when@test:
monolog: monolog:
@ -40,23 +112,89 @@ when@test:
when@prod: when@prod:
monolog: monolog:
handlers: handlers:
main: critical_error:
type: fingers_crossed type: fingers_crossed
action_level: error action_level: critical
handler: nested handler: error_nested
excluded_http_codes: [404, 405] buffer_size: 50
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
nested: error_nested:
type: stream type: rotating_file
path: php://stderr path: "%kernel.logs_dir%/critical.log"
level: debug level: debug
formatter: monolog.formatter.json max_files: 30
console:
type: console error:
process_psr_3_messages: false type: rotating_file
channels: ["!event", "!doctrine"] path: "%kernel.logs_dir%/error.log"
deprecation: level: error # logs error, critical, alert, emergency
type: stream max_files: 30
channels: [deprecation] channels: [ error ]
path: php://stderr
formatter: monolog.formatter.json php_errors:
type: rotating_file
path: "%kernel.logs_dir%/php_error.log"
level: warning # warnings, errors, fatals…
max_files: 30
channels: [ php ]
# User Management
user_management:
type: rotating_file
path: "%kernel.logs_dir%/user_management.log"
level: info
channels: [user_management]
max_files: 30
#AWS
aws_management:
type: rotating_file
path: "%kernel.logs_dir%/aws_management.log"
level: info
channels: [aws_management]
max_files: 30
# Authentication
authentication:
type: rotating_file
path: "%kernel.logs_dir%/authentication.log"
level: info
channels: [authentication]
max_files: 30
# Organization Management
organization_management:
type: rotating_file
path: "%kernel.logs_dir%/organization_management.log"
level: info
channels: [organization_management]
max_files: 30
# Access Control
access_control:
type: rotating_file
path: "%kernel.logs_dir%/access_control.log"
level: info
channels: [access_control]
max_files: 30
# Email Notifications
email_notifications:
type: rotating_file
path: "%kernel.logs_dir%/email_notifications.log"
level: info
channels: [email_notifications]
max_files: 30
# Admin Actions
admin_actions:
type: rotating_file
path: "%kernel.logs_dir%/admin_actions.log"
level: info
channels: [admin_actions]
max_files: 30
# Security
security:
type: rotating_file
path: "%kernel.logs_dir%/security.log"
level: warning
channels: [security]
max_files: 30

View File

@ -11,13 +11,18 @@ security:
property: email property: email
role_hierarchy: role_hierarchy:
ROLE_ADMIN: ROLE_USER ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH] ROLE_SUPER_ADMIN: [ROLE_ALLOWED_TO_SWITCH, ROLE_ADMIN]
firewalls: firewalls:
dev: dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/ pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false security: false
api_token_validation:
pattern: ^/api/validate-token
stateless: true
oauth2: true
oauth_userinfo: oauth_userinfo:
pattern: ^/oauth2/userinfo pattern: ^/oauth2/userinfo
stateless: true stateless: true
@ -29,23 +34,35 @@ security:
auth_token: auth_token:
pattern: ^/token pattern: ^/token
stateless: true stateless: true
api_m2m:
pattern: ^/api/v1/
stateless: true
oauth2: true
api: api:
pattern: ^/oauth/api pattern: ^/oauth/api
security: true security: true
stateless: true stateless: true
oauth2: true oauth2: true
password_setup:
pattern: ^/password_setup
stateless: true
main: main:
user_checker: App\Security\UserChecker
lazy: true lazy: true
provider: app_user_provider provider: app_user_provider
login_throttling:
max_attempts: 3
interval: '1 minute'
form_login: form_login:
login_path: app_login login_path: app_login
check_path: app_login check_path: app_login
enable_csrf: true enable_csrf: true
default_target_path: app_index default_target_path: app_index
use_referer: true always_use_default_target_path: false
# logout: logout:
# path: app_logout path: app_logout
# target: app_login enable_csrf: false
target: app_login
# activate different ways to authenticate # activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall # https://symfony.com/doc/current/security.html#the-firewall
@ -57,6 +74,9 @@ security:
# Note: Only the *first* access control that matches will be used # Note: Only the *first* access control that matches will be used
access_control: access_control:
- { path: ^/login, roles: PUBLIC_ACCESS } - { path: ^/login, roles: PUBLIC_ACCESS }
- { path: ^/api/validate-token, roles: PUBLIC_ACCESS }
- { path: ^/password_setup, roles: PUBLIC_ACCESS }
- { path: ^/password_reset, roles: PUBLIC_ACCESS }
- { path: ^/sso_logout, roles: IS_AUTHENTICATED_FULLY } - { path: ^/sso_logout, roles: IS_AUTHENTICATED_FULLY }
- { path: ^/token, roles: PUBLIC_ACCESS } - { path: ^/token, roles: PUBLIC_ACCESS }
- { path: ^/oauth2/revoke_tokens, roles: PUBLIC_ACCESS } - { path: ^/oauth2/revoke_tokens, roles: PUBLIC_ACCESS }
@ -66,8 +86,6 @@ security:
- { path: ^/oauth2/userinfo, roles: IS_AUTHENTICATED_FULLY } - { path: ^/oauth2/userinfo, roles: IS_AUTHENTICATED_FULLY }
- { path: ^/, roles: ROLE_USER } - { path: ^/, roles: ROLE_USER }
when@test: when@test:
security: security:
password_hashers: password_hashers:

View File

@ -1,5 +1,5 @@
framework: framework:
default_locale: en default_locale: fr
translator: translator:
default_path: '%kernel.project_dir%/translations' default_path: '%kernel.project_dir%/translations'
fallbacks: fallbacks:

View File

@ -2,6 +2,18 @@ twig:
file_name_pattern: '*.twig' file_name_pattern: '*.twig'
form_themes: ['bootstrap_5_layout.html.twig'] form_themes: ['bootstrap_5_layout.html.twig']
globals:
application: '%env(APPLICATION)%'
domain: '%env(APP_DOMAIN)%'
aws_url: '%env(AWS_S3_PORTAL_URL)%'
app_url: '%env(APP_URL)%'
version: '0.5'
paths:
'%kernel.project_dir%/assets/img': images
when@test: when@test:
twig: twig:
strict_variables: true strict_variables: true

1783
config/reference.php Normal file

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,17 @@
# Put parameters here that don't need to change on each machine where the app is deployed # 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 # https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
parameters: parameters:
aws_url: '%env(AWS_ENDPOINT)%'
aws_public_url: '%env(AWS_ENDPOINT)%'
aws_bucket: '%env(S3_PORTAL_BUCKET)%'
app_url: '%env(APP_URL)%'
app_domain: '%env(APP_DOMAIN)%'
mercure_secret: '%env(MERCURE_JWT_SECRET)%'
logos_directory: '%kernel.project_dir%/public/uploads/logos'
oauth_sso_identifier: '%env(OAUTH_SSO_IDENTIFIER)%'
oauth_sso_identifier_login: '%env(OAUTH_SSO_IDENTIFIER_LOGIN)%'
easycheck_url: '%env(EASYCHECK_URL)%'
webhook_secret: '%env(WEBHOOK_SECRET)%'
services: services:
# default configuration for services in *this* file # default configuration for services in *this* file
@ -19,15 +30,42 @@ services:
- '../src/DependencyInjection/' - '../src/DependencyInjection/'
- '../src/Entity/' - '../src/Entity/'
- '../src/Kernel.php' - '../src/Kernel.php'
App\MessageHandler\NotificationMessageHandler:
arguments:
$appUrl: '%app_url%'
App\Service\SSO\ProjectService:
arguments:
$appUrl: '%app_url%'
$clientIdentifier: '%oauth_sso_identifier%'
App\EventSubscriber\: App\EventSubscriber\:
resource: '../src/EventSubscriber/' resource: '../src/EventSubscriber/'
tags: ['kernel.event_subscriber'] tags: ['kernel.event_subscriber']
App\EventSubscriber\LoginSubscriber:
arguments:
$clientIdentifier: '%oauth_sso_identifier_login%'
$easycheckUrl: '%env(EASYCHECK_URL)%'
App\Service\AwsService:
arguments:
$awsPublicUrl: '%aws_public_url%'
App\Service\OrganizationsService:
arguments:
$logoDirectory: '%logos_directory%'
App\EventSubscriber\ScopeResolveListener: App\EventSubscriber\ScopeResolveListener:
tags: tags:
- { name: kernel.event_listener, event: league.oauth2_server.event.scope_resolve, method: onScopeResolve } - { name: kernel.event_listener, event: league.oauth2_server.event.scope_resolve, method: onScopeResolve }
League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface: League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface:
class: App\Repository\AccessTokenRepository class: App\Repository\AccessTokenRepository
decorates: 'League\Bundle\OAuth2ServerBundle\Repository\AccessTokenRepository' decorates: 'League\Bundle\OAuth2ServerBundle\Repository\AccessTokenRepository'
App\Command\CreateSuperAdminCommand:
# add more service definitions when explicit configuration is needed arguments:
# please note that last definitions always *replace* previous ones $environment: '%kernel.environment%'
App\EventListener\LogoutSubscriber:
arguments:
$easycheckUrl: '%env(EASYCHECK_URL)%'
tags:
- { name: kernel.event_subscriber }
App\Webhook\OrganizationNotifier:
arguments:
$easycheckUrl: '%easycheck_url%'
$webhookSecret: '%webhook_secret%'

225
docs/API.md Normal file
View File

@ -0,0 +1,225 @@
# Intro
The Api made some changes to the current structure of the project. These changes include the following:
- **Security** : the security.yaml file has been updated both on the server and the client side
- **Controllers**: Controllers need to be updated on the client side
- **Services**: Services and token management need updates on the client side
- **Health**: I now want to commit a Talon E over a wall IRL
- **Entities**: Entities need to be updated on the client side to include a new field (sso_id).
- This field will be used a lot, so add it to an entity if you are going to work with the API and the SSO. PLEASE.
- **Roles**: A new role was added because we are doing M2M. Only God know how that work now, but it works, so I might start praying from now on.
# Security
new firewall was added. Keep the same structure for future firewalls.
### Client side
```yaml
api_project:
pattern: ^/api/v1/project #ofc, this is an example, please THINK and change the name
stateless: true
access_token:
token_handler: App\Security\SsoTokenHandler
```
Same thing, new firewall was added.
### Server side
```yaml
api_token_validation:
pattern: ^/api/validate-token #this is NOT an example. DON'T change or it will all go to sh.t
stateless: true
oauth2: true
```
```yaml
# A rajouter dans l'access_control aussi !!! IMPORTANT !!!
- { path: ^/api/validate-token, roles: PUBLIC_ACCESS }
```
# Controllers
On the client side, create a new controller for the API. This controller need will work in a REST manner.
The route should be as follows:
```php
#[Route('/api/v1/project', name: 'api_project')] //ofc, this is an example, please THINK and change the name
```
Keep the same structure for the project tree, create an Api folder in the controller, a V1 folder and then create your controller.
Here is a full example of a controller with the create method.
On crée une organization si elle n'existe pas
```php
<?php
#[Route('/api/v2/project', name: 'api_project_')]
#[IsGranted('ROLE_API_INTERNAL')]
class ProjectApi extends AbstractController{
public function __construct(
private readonly EntityManagerInterface $entityManager)
{
}
#[Route('/create', name: 'create', methods: ['POST'])]
public function createProject(Request $request): JSONResponse
{
$data = json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR);
$projet = new Projet();
$entity = $this->entityManager->getRepository(Entity::class)->findOneBy([ 'ssoId' => $data['orgId']]);
// Si l'entité n'existe pas, on la crée
if(!$entity){
$this->createEntity($data['orgId'], $data['orgName']);
$entity = $this->entityManager->getRepository(Entity::class)->findOneBy([ 'ssoId' => $data['orgId']]);
}
$precision= $data['timestamp'];
$validPrecisions = array_map(fn($case) => $case->value, TimestampPrecision::cases());
if (!in_array($precision, $validPrecisions, true)) {
return $this->json(['success' => false, 'message' => 'Précision d\'horodatage invalide.'], 400);
}
try {
$timestampPrecision = TimestampPrecision::from($precision);
$projet->setTimestampPrecision($timestampPrecision);
$projet->setProjet($data['name']);
$projet->setEntityId($entity);
$projet->setBdd($data['bdd']);
$projet->setIsactive($data['isActive']);
$projet->setLogo($data['logo']);
$projet->setDeletionAllowed($data['deletion']);
$projet->setSsoId($data['id']); // c'est l'id du projet dans le portail, pas la bdd local
$this->entityManager->persist($projet);
$this->entityManager->flush();
return new JsonResponse(['message' => 'Project created successfully', 'project_id' => $projet->getId()], 201);
}catch ( \Exception $e){
return new JsonResponse(['error' => 'Failed to create project: ' . $e->getMessage()], 500);
}
}
```
# Services
So, now we are getting into the thick of it. We are just COOK 🍗 ( a lot of bugs can come from here ).
We implement a new pretty service called SsoTokenHandler. This is used to get the token received from the portal request, and validate it.
It is validaded by doing a call back to the SSO and asking if the token is valid. ( we create a new token for the SSO so it handles M2M)
```php
<?php
// src/Security/SsoTokenHandler.php
namespace App\Security;
use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\User\InMemoryUser;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class SsoTokenHandler implements AccessTokenHandlerInterface
{
public function __construct(
private HttpClientInterface $httpClient,
private string $ssoUrl
) {}
public function getUserBadgeFrom(string $accessToken): UserBadge
{
// 1. Call the SSO to validate the token
// Note: You need an endpoint on your SSO that returns 200 for M2M tokens
try {
$response = $this->httpClient->request('GET', $this->ssoUrl . '/api/validate-token', [
'headers' => ['Authorization' => 'Bearer ' . $accessToken]
]);
// If the SSO redirects, HttpClient might follow it to the login page.
// Let's see the first response code.
if ($response->getStatusCode() !== 200) {
// Log the content to see the HTML "Login" page that is causing the JSON error
// dump($response->getContent(false));
throw new BadCredentialsException();
}
} catch (\Exception $e) {
// This will show you if you are hitting a 302 Redirect
throw new BadCredentialsException('SSO returned invalid response: ' . $e->getMessage());
}
if (200 !== $response->getStatusCode()) {
throw new BadCredentialsException('Invalid SSO Token');
}
$data = $response->toArray();
// 2. Identify if it's a User or a Machine
$identifier = $data['email'] ?? 'SYSTEM_SSO_SERVER';
// 3. Return the badge with a "loader" closure
return new UserBadge($identifier, function($userIdentifier) use ($data) {
// If it's the SSO server calling, give it a specific role
if ($userIdentifier === 'SYSTEM_SSO_SERVER') {
return new InMemoryUser($userIdentifier, null, ['ROLE_API_INTERNAL']);
}
// Otherwise, let the normal user provider handle it (for standard users)
// You might need to inject your actual UserProvider here if needed
return new InMemoryUser($userIdentifier, null, ['ROLE_USER']);
});
}
}
```
# Important note 1
we need to add the portal url to the .env and declare it as a parameter in services.yaml
```dotenv
SSO_URL='http://portail.solutions-easy.moi'
```
```yaml
parameters:
sso_url: '%env(SSO_URL)%'
App\Security\SsoTokenHandler:
arguments:
$ssoUrl: '%sso_url%'
```
# Server side
On the server side, we need to create a new client, which will be himself, same as earlier, create it, and boom we are good.
The validate route is already created, so dw abt it.
```cmd
php bin/console league:oauth2-server:create-client sso_internal_service --grant-type "client_credentials"
```
now, copy the identifier, and paste it in the .env file
please note that we have 2 client for the application because one is used for m2m and the other is used for the user, so implement both, the one ending with _LOGIN is the one for the user
```dotenv
OAUTH_SSO_IDENTIFIER='sso-own-identifier'
```
and we are smart so what do we do? we add it to the services.yaml
```yaml
parameters:
oauth_sso_identifier: '%env(OAUTH_SSO_IDENTIFIER)%'
App\Service\SSO\ProjectService:
arguments:
$appUrl: '%app_url%'
$clientIdentifier: '%oauth_sso_identifier%'
```
We should be good now ( I hope ). Open the portal, try your call and check if it works, if it doesn't, check the logs and debug, you are a dev for a reason, so use your brain and debug.
If it still doesn't work, start praying, because I have no idea what to do anymore, but it works on my side, so it should work on yours, if not, well, I don't know what to say. Good luck.
Jokes aside, bugs often come from security problem, if the client returns a 401 error, it can be for multiple reasons and not necessarily because of the token but maybe because of the token validation.
Another commun bug is mismatching of the data you send, so double check. GLHF ( you won't have fun, but good luck anyway )
.
.
.
.
.
.
.
.
.
.
.
If you have a problem and that you copy pasted everything, check client controller, I've put the route as /api/v2 just cause I felt like it 🤷 ENJOY

365
docs/Client_Setup.md Normal file
View File

@ -0,0 +1,365 @@
# Client setup
## Add needed dependencies
```bash
composer require nelmio/cors-bundle
composer require knpuniversity/oauth2-client-bundle
```
## Configure the bundle
### nelmio/cors-bundle
```yaml
# config/packages/nelmio_cors.yaml
nelmio_cors:
defaults:
origin_regex: true
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
allow_headers: ['Content-Type', 'Authorization']
expose_headers: ['Link']
max_age: 3600
paths:
'^/token$':
origin_regex: true
allow_origin: ['*']
allow_headers: ['Content-Type', 'Authorization']
allow_methods: ['POST', 'OPTIONS']
allow_credentials: true
max_age: 3600
'^/authorize$':
origin_regex: true
allow_origin: ['*']
allow_headers: ['Content-Type', 'Authorization']
allow_methods: ['GET', 'POST', 'OPTIONS']
allow_credentials: true
max_age: 3600
```
### knpuniversity/oauth2-client-bundle
```yaml
# config/packages/knpu_oauth2_client.yaml
knpu_oauth2_client:
clients:
# configure your clients as described here: https://github.com/knpuniversity/oauth2-client-bundle#configuration
sudalys:
type: generic
provider_class: Sudalys\OAuth2\Client\Provider\Sudalys
client_id: '%env(OAUTH2_CLIENT_ID)%'
client_secret: '%env(OAUTH2_CLIENT_SECRET)%'
redirect_route: uri # The route to redirect to after authentication (must match the one in the server DB uri DB)
provider_options: {
domain: <link to domain>
}
use_state: false
```
### .env
```dotenv
# .env
# CORS
CORS_ALLOW_ORIGIN=http://*.your domain/*'
# OAUTH2
OAUTH2_CLIENT_ID=<client_id>
OAUTH2_CLIENT_SECRET=<client_secret>
```
Copy and paste the client library then modify the conposer.json autoloard directive to include the new library
```json
"autoload": {
"psr-4": {
"App\\": "src/",
"Sudalys\\OAuth2\\Client\\": "libs/sudalys/oauth2-client/src"
}
},
```
```php
<?php
/** @var string */
public $domain = 'link to SSO portal ';
```
Copy and paste the SSOAuthenticator class modify the target url to match the route in server DB and redirect route
### SsoAuthenticator.php
```php
<?php
namespace App\Security;
use App\Entity\User;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use KnpU\OAuth2ClientBundle\Security\Authenticator\OAuth2Authenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
/**
* Class SudalysSSoAuthenticator
*/
class SsoAuthenticator extends OAuth2Authenticator implements AuthenticationEntryPointInterface
{
private $clientRegistry;
private $em;
private $router;
private $urlGenerator;
use TargetPathTrait;
public function __construct(ClientRegistry $clientRegistry, EntityManagerInterface $em, RouterInterface $router, UrlGeneratorInterface $urlGenerator)
{
$this->clientRegistry = $clientRegistry;
$this->em = $em;
$this->router = $router;
$this->urlGenerator = $urlGenerator;
}
public function start(Request $request, AuthenticationException $authException = null): Response
{
// Use the KnpU client to generate the correct authorization URL,
// including state / redirect_uri / scope / pkce as configured.
$client = $this->getSudalysClient();
// Option A: let the client use the configured redirect uri and default scopes:
return $client->redirect();
// Option B (explicit): specify scopes and an explicit redirect_uri (absolute URL)
// $redirectUri = $this->urlGenerator->generate('sudalys_check', [], UrlGeneratorInterface::ABSOLUTE_URL);
// return $client->redirect(['openid', 'profile'], ['redirect_uri' => $redirectUri]);
}
public function supports(Request $request): ?bool
{
// If your OAuth redirect route is named 'sudalys_check', check by route:
if ($request->attributes->get('_route') === 'sudalys_check') {
return true;
}
// fallback: also support requests containing the authorization code
return (bool) $request->query->get('code');
}
public function authenticate(Request $request): Passport
{
$client = $this->getSudalysClient();
$accessToken = $this->fetchAccessToken($client);
$session = $request->getSession();
$session->set('access_token', $accessToken->getToken());
// Stocker également le refresh token s'il est disponible
if ($accessToken->getRefreshToken()) {
$session->set('refresh_token', $accessToken->getRefreshToken());
}
return new SelfValidatingPassport(
new UserBadge($accessToken->getToken(), function () use ($accessToken, $client) {
//show in log the access token
$sudalysSsoUser = $client->fetchUserFromToken($accessToken);
$ssoId = $sudalysSsoUser->getId();
/*
* On regarde si le token est valide
*/
if($accessToken->getExpires() > time()) {
// Token valide, on regarde si l'utilisateur existe en bdd locale
/** @var User $userInDatabase */
$user = $this->em->getRepository(User::class)->findOneBy(['ssoId' => $ssoId]);
/**
* on cree l'utilisateur s'il n'existe pas
**/
if (!$user) {
$user = new User();
$user->setEmail($ssoData->getEmail());
$user->setPrenom($ssoData->getName());
$user->setNom($ssoData->getSurname());
$user->setSsoId($ssoData->getId());
$this->em->persist($user);
}else{
// On met a jour l'utilisateur
$user->setEmail($ssoData->getEmail());
$user->setPrenom($ssoData->getName());
$user->setNom($ssoData->getSurname());
$this->em->persist($user);
}
//handle UOs links
$ssoArray = $ssoData->toArray();
$uoData = $ssoArray['uos'] ?? [];
foreach ($uoData as $uo) {
$ssoOrgId = $uo['id'];
$entity = $this->em->getRepository(Entity::class)->findOneBy(['ssoId' => $ssoOrgId]);
if (!$entity) {
$entity = new Entity();
$entity->setSsoId($ssoOrgId);
$entity->setNom($uo['name']);
$this->em->persist($entity);
}
$role = $this->em->getRepository(Roles::class)->findOneBy(['name' => $uo['role']]);
// Check if the user-organization link already exists
$existingLink = $this->em->getRepository(UsersOrganizations::class)->findOneBy([
'users' => $user,
'organizations' => $entity
]);
if (!$existingLink) {
// Create a new link if it doesn't exist
$newLink = new UsersOrganizations();
$newLink->setUsers($user);
$newLink->setOrganizations($entity);
$newLink->setRole($role);
$this->em->persist($newLink);
} else {
// Update the role if the link already exists
$existingLink->setRole($role);
$existingLink->setModifiedAt(new \DateTimeImmutable());
$this->em->persist($existingLink);
}
}
$this->em->flush();
return $user;
}
})
);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
// change "app_homepage" to some route in your app
$targetUrl = $this->router->generate('app_index');
return new RedirectResponse($targetUrl);
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$message = strtr($exception->getMessageKey(), $exception->getMessageData());
return new Response($message, Response::HTTP_FORBIDDEN);
}
/**
*
*/
private function getSudalysClient()
{
return $this->clientRegistry->getClient('sudalys');
}
}
```
```php
namespace App\Security\SsoAuthenticator;
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
// change "app_homepage" to some route in your app
$targetUrl = $this->router->generate('your redirect route');
return new RedirectResponse($targetUrl);
}
```
### Security.yaml
```yaml
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
main:
lazy: true
provider: app_user_provider
custom_authenticators:
- App\Security\SsoAuthenticator
entry_point: App\Security\SsoAuthenticator
logout:
path: app_logout
target: app_after_logout
invalidate_session: true
delete_cookies: ['PHPSESSID']
access_control:
- { path: ^/sso/login, roles: PUBLIC_ACCESS }
- { path: ^/sso/check, roles: PUBLIC_ACCESS }
- { path: ^/, roles: IS_AUTHENTICATED_FULLY }
```
### Setup oauth controller
```php
<?php
namespace App\Controller;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class SsoController extends AbstractController
{
#[Route('/sso/login', name: 'sudalys_sso_login')]
public function login(ClientRegistry $clientRegistry): RedirectResponse
{
return $clientRegistry->getClient('sudalys')->redirect();
}
#[Route('/sso/check', name: 'sudalys_sso_check')]
public function connectCheckAction(Request $request)
{
return $this->redirectToRoute('app_index');
}
#[Route('/logout', name: 'app_logout')]
public function logout(): void
{
throw new \Exception('This should never be reached!');
}
#[Route('/logout-redirect', name: 'app_after_logout')]
public function afterLogout(): RedirectResponse
{
// SSO logout URL — adjust if necessary
$ssoLogout = 'http://portail.solutions-easy.moi/sso_logout';
return new RedirectResponse($ssoLogout);
}
}
```
# Server setup
## Create OAuth2 client
```cmd
php bin/console league:oauth2-server:create-client <name> --redirect-uri="http://your-client-domain/sso/check" --scope="openid" --scope="profile" --scope="email" --grant-type=authorization_code
```
If there is a scope or grand error, delete the client do the following first
```cmd
php bin/console league:oauth2-server:delete-client <identifier>
```
Identifier can be found in the database oauth2_client table
To recreate the client and enter the scopes and grant types after creating the client directly in the db
```text
scopes = email profile openid
grants = authorization_code
```

35
docs/Organization.md Normal file
View File

@ -0,0 +1,35 @@
# Intro
Each organization are a collection of users and projects.
Users will be able to have multiple organizations and different roles in each of them. For example, a user can be an
admin in one organization and a member in another organization.
Each organization will have a unique slug that will consist of 4 lowercase letters and this slug will be used for the
database name of each project contained in an organization.
## Projects
Each project will have a unique name. Each project will be associated with an organization and will have a unique slug
that will come from the organization. The project will have a JSON field that will contain the different applications it has access to
## Organization Management
The organization management will have different features, such as creating an organization, inviting users to an organization,
managing the roles of users in an organization(admin or not), and deleting an organization.
### CRUD Operations
- **Create Organization**: Super Admin
- **Read Organization**: Super Admin, Admin and admin of the organization
- **Update Organization**: Super Admin, Admin
- **Delete Organization**: Super Admin
### User Management
- **Invite User**: Super Admin, Admin and admin of the organization
- **Remove User**: Super Admin, Admin and admin of the organization
- **Change User Role**: Super Admin, Admin and admin of the organization
- **List Users**: Super Admin, Admin and admin of the organization
- **Accept Invitation**: User
- **Decline Invitation**: User
### Project Management
- **Create Project**: Super Admin
- **Read Project**: Super Admin, Admin and admin of the organization
- **Update Project**: Super Admin
- **Delete Project**: Super Admin

169
docs/Role_Hierarchy.md Normal file
View File

@ -0,0 +1,169 @@
# Intro
Roles will be split into two categories: **System Roles** and **Organizations Roles**.
System roles are global and apply to the entire system, while Organizations roles are specific to individual Organizations.
## System Roles
System roles are global and apply to the entire system. They include:
- **System Super Admin**: Has full access to all system features and settings. Can manage users, projects, organizations and applications. (SI)
- **System Admin**: Has access to most system features and settings. Can manage users, organizations, applications authorizations by projects. (BE)
- **System User**: Has limited access to system features and settings. Can view projects and applications, can manage own information, and organization where they are admin. (Others)
### System Super Admin
Get Access to the following with the following authorisations:
- **Users**: READ, CREATE, UPDATE, DELETE
- **Projects**: READ, CREATE, UPDATE, DELETE
- **Organizations**: READ, CREATE, UPDATE, DELETE
- **Applications**: READ, UPDATE
### System Admin
Get Access to the following with the following authorisations:
- **Users**: READ, CREATE, UPDATE, DELETE
- **Organizations**: READ, UPDATE
- **Applications**: READ
### System User
Get Access to the following with the following authorisations:
- **Users**: READ, UPDATE (own information only), READ (organization where they are admin), CREATE ( organization where they are admin), UPDATE (organization where they are admin), DELETE (organization where they are admin)
- **Projects**: READ ( of organization they are part of)
- **Organizations**: READ
- **Applications**: READ
## Organizations Roles
Organizations roles are specific to individual Organizations. They include:
- **Organization Admin**: Has full access to all organization features and settings. Can manage users of the organizations.
- **Organization User**: Has limited access to organization features and settings. Can view projects and applications, can manage own information
# Set up
Like for the sso, we need to create roles in the system. create the following command and the create the roles.
``` php
#[AsCommand(
name: 'app:create-role',
description: 'Creates a new role in the database'
)]
class CreateRoleCommand extends Command
{
private EntityManagerInterface $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
parent::__construct();
$this->entityManager = $entityManager;
}
protected function configure(): void
{
$this
->addArgument('name', InputArgument::REQUIRED, 'The name of the role'); // role name required
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$roleName = trim($input->getArgument('name'));
$roleName = strtoupper($roleName); // Normalize to uppercase
// Ensure not empty
if ($roleName === '') {
$output->writeln('<error>The role name cannot be empty</error>');
return Command::FAILURE;
}
// Check if role already exists
$existing = $this->entityManager->getRepository(Roles::class)
->findOneBy(['name' => $roleName]);
if ($existing) {
$output->writeln("<comment>Role '{$roleName}' already exists.</comment>");
return Command::SUCCESS; // not failure, just redundant
}
// Create and persist new role
$role = new Roles();
$role->setName($roleName);
$this->entityManager->persist($role);
$this->entityManager->flush();
$output->writeln("<info>Role '{$roleName}' created successfully!</info>");
return Command::SUCCESS;
}
}
```
```php
#[AsCommand(
name: 'app:delete-role',
description: 'Deletes a role from the database'
)]
class DeleteRoleCommand extends Command
{
private EntityManagerInterface $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
parent::__construct();
$this->entityManager = $entityManager;
}
protected function configure(): void
{
$this
->addArgument('name', InputArgument::REQUIRED, 'The name of the role to delete');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$roleName = trim($input->getArgument('name'));
$roleName = strtoupper($roleName); // Normalize to uppercase
if ($roleName === '') {
$output->writeln('<error>The role name cannot be empty</error>');
return Command::FAILURE;
}
// Find the role
$role = $this->entityManager->getRepository(Roles::class)
->findOneBy(['name' => $roleName]);
if (!$role) {
$output->writeln("<error>Role '{$roleName}' not found.</error>");
return Command::FAILURE;
}
// Check if role is being used (optional safety check)
$usageCount = $this->entityManager->getRepository(\App\Entity\UsersOrganizations::class)
->count(['role' => $role]);
if ($usageCount > 0) {
$output->writeln("<error>Cannot delete role '{$roleName}' - it is assigned to {$usageCount} user(s).</error>");
$output->writeln('<comment>Remove all assignments first, then try again.</comment>');
return Command::FAILURE;
}
// Confirmation prompt
$helper = $this->getHelper('question');
$question = new ConfirmationQuestion(
"Are you sure you want to delete role '{$roleName}'? [y/N] ",
false
);
if (!$helper->ask($input, $output, $question)) {
$output->writeln('<comment>Operation cancelled.</comment>');
return Command::SUCCESS;
}
// Delete the role
$this->entityManager->remove($role);
$this->entityManager->flush();
$output->writeln("<info>Role '{$roleName}' deleted successfully!</info>");
return Command::SUCCESS;
}
}
```
``` bash
php bin/console app:create-role USER
php bin/console app:create-role ADMIN
```

View File

@ -0,0 +1,208 @@
# Documentation SSO/SLO - EasyPortal & EasyCheck
## Vue d'ensemble
Cette documentation décrit l'implémentation du **Single Sign-On (SSO)** et du **Single Logout (SLO)** entre deux applications Symfony :
- **EasyPortal** : Serveur d'autorisation OAuth2 (Identity Provider)
- **EasyCheck** : Application cliente OAuth2
## Architecture
```
┌─────────────────┐ ┌─────────────────┐
│ EasyPortal │ │ EasyCheck │
│ (OAuth2 Server)│◄──────────────────►│ (OAuth2 Client) │
│ │ │ │
│ - Authentifie │ │ - Utilise le │
│ - Émet tokens │ │ token OAuth2 │
│ - Révoque │ │ - Valide token │
└─────────────────┘ └─────────────────┘
```
## Single Sign-On (SSO)
### Principe
L'utilisateur s'authentifie **une seule fois** sur le portail et accède ensuite à toutes les applications sans re-saisir ses identifiants.
### Flux d'authentification
```
1. Utilisateur → EasyCheck
└─> Pas de session active
2. EasyCheck → Redirection vers EasyPortal
└─> /authorize?client_id=...&redirect_uri=...
3. Utilisateur → Connexion sur EasyPortal
└─> Login/Password ou session existante
4. EasyPortal → Redirection vers EasyCheck
└─> /sso_check?code=AUTHORIZATION_CODE
5. EasyCheck → Échange du code contre un token
└─> POST /token avec authorization_code
└─> Reçoit access_token + refresh_token
6. EasyCheck → Création de session locale
└─> Stockage du token en session
└─> Utilisateur connecté
```
## Single Logout (SLO)
### Principe
Lorsqu'un utilisateur se déconnecte d'une application, il est **automatiquement déconnecté de toutes les applications** SSO via un appel API asynchrone, évitant les boucles de redirections.
### Flux de déconnexion depuis EasyCheck
```
1. Utilisateur → Clic "Déconnexion" sur EasyCheck
└─> GET /logout
2. Symfony → Invalide la session EasyCheck
└─> Session détruite, cookies supprimés
3. LogoutSubscriber → Interception de l'événement
└─> Détecte que ce n'est pas une déconnexion depuis le portail
4. EasyCheck → Redirection vers portail
└─> GET https://portail.../sso_logout?from_easycheck=1
5. EasyPortal → Révocation des tokens OAuth2
└─> Tous les access_token de l'utilisateur sont révoqués
└─> Paramètre redirect_app=easycheck propagé dans l'URL
6. EasyPortal → Redirection vers /logout
└─> GET /logout
7. Symfony → Invalide la session EasyPortal
└─> Session détruite
8. LogoutSubscriber → Redirection vers EasyCheck
└─> GET https://check.../logout?from_portal=1&redirect_app=easycheck
9. EasyCheck → Détecte from_portal=1
└─> Invalide la session (si elle existe encore)
└─> Redirection vers portail login avec redirect_app
10. EasyPortal → Affichage page login
└─> GET /login?redirect_app=easycheck
11. Utilisateur se reconnecte → LoginSubscriber détecte redirect_app=easycheck
└─> Redirection automatique vers EasyCheck /sso/login
```
### Flux de déconnexion depuis EasyPortal
```
1. Utilisateur → Clic "Déconnexion" sur EasyPortal
└─> GET /sso_logout
2. EasyPortal → Révocation des tokens OAuth2
└─> Tous les access_token de l'utilisateur sont révoqués
3. EasyPortal → Redirection vers /logout
└─> GET /logout
4. Symfony → Invalide la session EasyPortal
└─> Session détruite
5. LogoutSubscriber → Redirection vers EasyCheck
└─> GET https://check.../logout?from_portal=1
6. EasyCheck → Détecte from_portal=1
└─> Invalide la session EasyCheck
└─> Session détruite, cookies supprimés
7. EasyCheck → Redirection finale vers portail
└─> GET https://portail.../login
```
## Variables d'environnement
### EasyCheck (.env)
```bash
# URL du serveur SSO (EasyPortal)
SSO_URL='https://portail.solutions-easy.moi'
# Configuration OAuth2
OAUTH_CLIENT_ID='easycheck-client-id'
OAUTH_CLIENT_SECRET='secret-key'
```
### EasyPortal (.env)
```bash
# URL de l'application cliente (EasyCheck)
EASYCHECK_URL='https://check.solutions-easy.moi'
# Configuration OAuth2 Server
OAUTH_PRIVATE_KEY=%kernel.project_dir%/config/jwt/private.key
OAUTH_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.key
OAUTH_PASSPHRASE='passphrase'
OAUTH_ENCRYPTION_KEY='encryption-key'
```
## Système de redirection multi-applications
### Paramètre `redirect_app`
Pour gérer plusieurs applications SSO (EasyCheck, EasyAudit, EasyMaintenance, etc.), le système utilise un **paramètre URL `redirect_app`** au lieu de cookies.
**Avantages** :
- ✅ Fonctionne avec plusieurs onglets ouverts simultanément
- ✅ Chaque onglet garde son contexte de déconnexion
- ✅ Pas de conflit entre applications
- ✅ Facilement extensible pour de nouvelles applications
**Fonctionnement** :
1. Lors de la déconnexion depuis une application, elle envoie `redirect_app=nom_app`
2. Ce paramètre est propagé dans toutes les redirections de logout
3. Il arrive sur la page `/login?redirect_app=nom_app`
4. Après reconnexion, l'utilisateur est redirigé vers l'application d'origine
**Configuration dans LoginSubscriber** :
```php
$appUrls = [
'easycheck' => $easycheckUrl . '/sso/login',
'easyaudit' => $easyauditUrl . '/sso/login',
'easymaintenance' => $easymaintenanceUrl . '/sso/login',
];
```
## Endpoints
### EasyCheck - API Logout
**Route** : `POST /api/logout`
**Description** : Endpoint API pour invalider la session EasyCheck sans redirection (non utilisé actuellement, prévu pour usage futur).
**Réponse** :
```json
{
"success": true,
"message": "Session invalidated successfully"
}
```
## Points importants
### Sécurité
1. **CSRF désactivé sur logout** : Les routes de logout utilisent `enable_csrf: false` car ce sont des liens GET simples
2. **Tokens révoqués** : Lors du logout, tous les access_token de l'utilisateur sont révoqués côté portail
3. **Sessions invalidées** : Les sessions PHP sont complètement détruites des deux côtés
4. **Cookies supprimés** : Les cookies de session sont explicitement supprimés
### Architecture
1. **Pas de boucles infinies** : Utilisation du paramètre `from_portal` pour éviter les boucles de redirections
2. **Déconnexion bidirectionnelle** : Chaque application invalide la session de l'autre lors de la déconnexion
3. **Flux prévisible** : Chaque déconnexion suit un chemin clair avec des paramètres explicites
4. **Retour automatique** : Paramètre URL `redirect_app` pour rediriger l'utilisateur vers l'application d'origine après reconnexion
5. **Single Logout complet** : Toutes les sessions (portail + applications) sont toujours invalidées, quelle que soit l'origine de la déconnexion
6. **Multi-applications** : Support natif de plusieurs applications SSO sans conflit entre onglets

79
frankenphp/Caddyfile Normal file
View File

@ -0,0 +1,79 @@
{
# Global options
frankenphp {
# Number of workers for better performance
num_threads {$NUM_THREADS:4}
}
# Order directives properly
order mercure after encode
order php_server before file_server
}
# HTTP server - HTTPS is handled by caddy-proxy
{$SERVER_NAME:80} {
# Root directory
root * /app/public
# Enable compression
encode zstd gzip
# Mercure hub configuration (built-in)
mercure {
# Publisher JWT key
publisher_jwt {env.MERCURE_PUBLISHER_JWT_KEY} {
algorithm hs256
}
# Subscriber JWT key
subscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY} {
algorithm hs256
}
# Allow anonymous subscribers
anonymous
# CORS configuration
cors_origins *
}
# Client max body size (for uploads)
request_body {
max_size 20MB
}
# Security: Deny access to sensitive directories
@forbidden {
path /bin/* /config/* /src/* /templates/* /tests/* /translations/* /var/* /vendor/*
}
handle @forbidden {
respond "Access Denied" 404
}
# Security: Deny access to dot files (except .well-known for Mercure)
@dotfiles {
path */.*
not path /.well-known/*
}
handle @dotfiles {
respond "Access Denied" 404
}
# Cache static assets (30 days)
@static {
path *.jpg *.jpeg *.png *.gif *.ico *.css *.js *.svg *.woff *.woff2 *.ttf *.eot *.xlsx
}
handle @static {
header Cache-Control "public, max-age=2592000, no-transform"
file_server
}
# PHP FrankenPHP handler
php_server {
# Resolve symlinks
resolve_root_symlink
}
# Logging
log {
output file /var/log/caddy/access.log
format json
}
}

104
frankenphp/Caddyfile.prod Normal file
View File

@ -0,0 +1,104 @@
{
# Global options
frankenphp {
# Number of workers for better performance
num_threads {$NUM_THREADS:4}
}
# Order directives properly
order mercure after encode
order php_server before file_server
}
# HTTP - redirect to HTTPS
http://{$SERVER_NAME:localhost} {
redir https://{host}{uri} permanent
}
# HTTPS server
https://{$SERVER_NAME:localhost} {
# Root directory
root * /app/public
# TLS configuration - Caddy will automatically obtain and renew Let's Encrypt certificates
tls {
protocols tls1.2 tls1.3
}
# Enable compression
encode zstd gzip
# Mercure hub configuration (built-in)
mercure {
# Publisher JWT key
publisher_jwt {env.MERCURE_PUBLISHER_JWT_KEY} {
algorithm hs256
}
# Subscriber JWT key
subscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY} {
algorithm hs256
}
# Allow anonymous subscribers
anonymous
# Enable subscriptions
subscriptions
# CORS configuration
cors_origins *
}
# Client max body size (for uploads)
request_body {
max_size 20MB
}
# Security: Deny access to sensitive directories
@forbidden {
path /bin/* /config/* /src/* /templates/* /tests/* /translations/* /var/* /vendor/*
}
handle @forbidden {
respond "Access Denied" 404
}
# Security: Deny access to dot files (except .well-known for Mercure)
@dotfiles {
path */.*
not path /.well-known/*
}
handle @dotfiles {
respond "Access Denied" 404
}
# Cache static assets (30 days)
@static {
path *.jpg *.jpeg *.png *.gif *.ico *.css *.js *.svg *.woff *.woff2 *.ttf *.eot *.xlsx *.pdf
file
}
handle @static {
header Cache-Control "public, max-age=2592000, no-transform"
file_server
}
# Serve files from /assets directory
handle /assets/* {
root * /app/public
file_server
}
# PHP FrankenPHP handler
php_server {
# Resolve symlinks
resolve_root_symlink
}
# Logging
log {
output file /app/var/log/access.log
format json
# Redact sensitive data
format filter {
request>uri query {
replace authorization REDACTED
}
}
}
}

View File

@ -0,0 +1,13 @@
expose_php = 0
date.timezone = UTC
apc.enable_cli = 1
session.use_strict_mode = 1
zend.detect_unicode = 0
; https://symfony.com/doc/current/performance.html
realpath_cache_size = 4096K
realpath_cache_ttl = 600
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 20000
opcache.memory_consumption = 256
opcache.enable_file_override = 1

View File

@ -0,0 +1,5 @@
; See https://docs.docker.com/desktop/networking/#i-want-to-connect-from-a-container-to-a-service-on-the-host
; See https://github.com/docker/for-linux/issues/264
; The `client_host` below may optionally be replaced with `discover_client_host=yes`
; Add `start_with_request=yes` to start debug session on each request
xdebug.client_host = host.docker.internal

View File

@ -0,0 +1,5 @@
; https://symfony.com/doc/current/performance.html#use-the-opcache-class-preloading
opcache.preload_user = root
opcache.preload = /app/config/preload.php
; https://symfony.com/doc/current/performance.html#don-t-check-php-files-timestamps
opcache.validate_timestamps = 0

View File

@ -0,0 +1,66 @@
#!/bin/sh
set -e
if [ "$1" = 'frankenphp' ] || [ "$1" = 'php' ] || [ "$1" = 'bin/console' ]; then
# Install the project the first time PHP is started
# After the installation, the following block can be deleted
if [ ! -f composer.json ]; then
rm -Rf tmp/
composer create-project "symfony/skeleton $SYMFONY_VERSION" tmp --stability="$STABILITY" --prefer-dist --no-progress --no-interaction --no-install
cd tmp
cp -Rp . ..
cd -
rm -Rf tmp/
composer require "php:>=$PHP_VERSION" runtime/frankenphp-symfony
composer config --json extra.symfony.docker 'true'
if grep -q ^DATABASE_URL= .env; then
echo 'To finish the installation please press Ctrl+C to stop Docker Compose and run: docker compose up --build --wait'
sleep infinity
fi
fi
if [ -z "$(ls -A 'vendor/' 2>/dev/null)" ]; then
composer install --prefer-dist --no-progress --no-interaction
fi
# Display information about the current project
# Or about an error in project initialization
php bin/console -V
if grep -q ^DATABASE_URL= .env; then
echo 'Waiting for database to be ready...'
ATTEMPTS_LEFT_TO_REACH_DATABASE=60
until [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ] || DATABASE_ERROR=$(php bin/console dbal:run-sql -q "SELECT 1" 2>&1); do
if [ $? -eq 255 ]; then
# If the Doctrine command exits with 255, an unrecoverable error occurred
ATTEMPTS_LEFT_TO_REACH_DATABASE=0
break
fi
sleep 1
ATTEMPTS_LEFT_TO_REACH_DATABASE=$((ATTEMPTS_LEFT_TO_REACH_DATABASE - 1))
echo "Still waiting for database to be ready... Or maybe the database is not reachable. $ATTEMPTS_LEFT_TO_REACH_DATABASE attempts left."
done
if [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ]; then
echo 'The database is not up or not reachable:'
echo "$DATABASE_ERROR"
exit 1
else
echo 'The database is now ready and reachable'
fi
if [ "$( find ./migrations -iname '*.php' -print -quit )" ]; then
php bin/console doctrine:migrations:migrate --no-interaction --all-or-nothing
fi
fi
setfacl -R -m u:www-data:rwX -m u:"$(whoami)":rwX var
setfacl -dR -m u:www-data:rwX -m u:"$(whoami)":rwX var
echo 'PHP app ready!'
fi
exec docker-php-entrypoint "$@"

View File

@ -35,4 +35,42 @@ return [
'version' => '5.3.5', 'version' => '5.3.5',
'type' => 'css', 'type' => 'css',
], ],
'choices.js' => [
'version' => '11.1.0',
],
'choices.js/public/assets/styles/choices.min.css' => [
'version' => '11.1.0',
'type' => 'css',
],
'quill' => [
'version' => '2.0.3',
],
'lodash-es' => [
'version' => '4.17.21',
],
'parchment' => [
'version' => '3.0.0',
],
'quill-delta' => [
'version' => '5.1.0',
],
'eventemitter3' => [
'version' => '5.0.1',
],
'fast-diff' => [
'version' => '1.3.0',
],
'lodash.clonedeep' => [
'version' => '4.5.0',
],
'lodash.isequal' => [
'version' => '4.5.0',
],
'tabulator-tables' => [
'version' => '6.3.1',
],
'tabulator-tables/dist/css/tabulator.min.css' => [
'version' => '6.3.1',
'type' => 'css',
],
]; ];

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250709072959 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('DROP SEQUENCE subscriptions_id_seq CASCADE');
$this->addSql('ALTER TABLE subscriptions DROP CONSTRAINT fk_4778a0167b3b43d');
$this->addSql('DROP TABLE subscriptions');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('CREATE SEQUENCE subscriptions_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE subscriptions (id SERIAL NOT NULL, users_id INT NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX idx_4778a0167b3b43d ON subscriptions (users_id)');
$this->addSql('ALTER TABLE subscriptions ADD CONSTRAINT fk_4778a0167b3b43d FOREIGN KEY (users_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250709073312 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE subscriptions (id SERIAL NOT NULL, users_id INT NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_4778A0167B3B43D ON subscriptions (users_id)');
$this->addSql('ALTER TABLE subscriptions ADD CONSTRAINT FK_4778A0167B3B43D FOREIGN KEY (users_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE subscriptions DROP CONSTRAINT FK_4778A0167B3B43D');
$this->addSql('DROP TABLE subscriptions');
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250709073752 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE user_tab (id SERIAL NOT NULL, users_id INT NOT NULL, ip_address VARCHAR(255) NOT NULL, tab_id VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_98F5228767B3B43D ON user_tab (users_id)');
$this->addSql('ALTER TABLE user_tab ADD CONSTRAINT FK_98F5228767B3B43D FOREIGN KEY (users_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE user_tab DROP CONSTRAINT FK_98F5228767B3B43D');
$this->addSql('DROP TABLE user_tab');
}
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250709115309 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('DROP SEQUENCE subscriptions_id_seq CASCADE');
$this->addSql('ALTER TABLE subscriptions DROP CONSTRAINT fk_4778a0167b3b43d');
$this->addSql('DROP TABLE subscriptions');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('CREATE SEQUENCE subscriptions_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE subscriptions (id SERIAL NOT NULL, users_id INT NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX idx_4778a0167b3b43d ON subscriptions (users_id)');
$this->addSql('ALTER TABLE subscriptions ADD CONSTRAINT fk_4778a0167b3b43d FOREIGN KEY (users_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250709120951 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE organizations ADD name VARCHAR(255) NOT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE organizations DROP name');
}
}

View File

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

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250709141934 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE "user" ADD last_connection DATE DEFAULT NULL');
$this->addSql('COMMENT ON COLUMN "user".last_connection IS \'(DC2Type:date_immutable)\'');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE "user" DROP last_connection');
}
}

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250710070735 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE "user" ALTER last_connection TYPE DATE');
$this->addSql('COMMENT ON COLUMN "user".last_connection IS NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE "user" ALTER last_connection TYPE DATE');
$this->addSql('COMMENT ON COLUMN "user".last_connection IS \'(DC2Type:date_immutable)\'');
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250710071344 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE "user" ALTER last_connection TYPE TIMESTAMP(0) WITHOUT TIME ZONE');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE "user" ALTER last_connection TYPE DATE');
}
}

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