Compare commits

..

11 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
16 changed files with 85 additions and 64 deletions

View File

@ -19,6 +19,8 @@
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-client" /> <excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-client" />
<excludeFolder url="file://$MODULE_DIR$/vendor/ralouphie/getallheaders" /> <excludeFolder url="file://$MODULE_DIR$/vendor/ralouphie/getallheaders" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/rate-limiter" /> <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" />

View File

@ -181,6 +181,10 @@
<path value="$PROJECT_DIR$/vendor/twig/extra-bundle" /> <path value="$PROJECT_DIR$/vendor/twig/extra-bundle" />
<path value="$PROJECT_DIR$/vendor/staabm/side-effects-detector" /> <path value="$PROJECT_DIR$/vendor/staabm/side-effects-detector" />
<path value="$PROJECT_DIR$/vendor/symfony/rate-limiter" /> <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" />

View File

@ -171,7 +171,7 @@ export default class extends Controller {
this.modalTitleTarget.textContent = "Modifier l'organisation"; this.modalTitleTarget.textContent = "Modifier l'organisation";
try { try {
const response = await fetch(`/organization/${this.currentOrgId}`); const response = await fetch(`/organization/editModal/${this.currentOrgId}`);
const data = await response.json(); const data = await response.json();
// Fill targets // Fill targets

View File

@ -52,6 +52,7 @@
"symfony/ux-turbo": "^2.24", "symfony/ux-turbo": "^2.24",
"symfony/validator": "7.4.*", "symfony/validator": "7.4.*",
"symfony/web-link": "7.4.*", "symfony/web-link": "7.4.*",
"symfony/webhook": "7.4.*",
"symfony/yaml": "7.4.*", "symfony/yaml": "7.4.*",
"twig/twig": "^2.12|^3.0" "twig/twig": "^2.12|^3.0"
}, },

View File

@ -4,7 +4,7 @@ league_oauth2_server:
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: PT15M # 15 minutes access_token_ttl: PT15M # 15 minutes
refresh_token_ttl: PT7D # 7 days refresh_token_ttl: P7D # 7 days
auth_code_ttl: PT30M # 30 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:

View File

@ -676,7 +676,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* }>, * }>,
* }, * },
* webhook?: bool|array{ // Webhook configuration * webhook?: bool|array{ // Webhook configuration
* enabled?: bool|Param, // Default: false * enabled?: bool|Param, // Default: true
* message_bus?: scalar|Param|null, // The message bus to use. // Default: "messenger.default_bus" * message_bus?: scalar|Param|null, // The message bus to use. // Default: "messenger.default_bus"
* routing?: array<string, array{ // Default: [] * routing?: array<string, array{ // Default: []
* service: scalar|Param|null, * service: scalar|Param|null,
@ -684,7 +684,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* }>, * }>,
* }, * },
* remote-event?: bool|array{ // RemoteEvent configuration * remote-event?: bool|array{ // RemoteEvent configuration
* enabled?: bool|Param, // Default: false * enabled?: bool|Param, // Default: true
* }, * },
* json_streamer?: bool|array{ // JSON streamer configuration * json_streamer?: bool|array{ // JSON streamer configuration
* enabled?: bool|Param, // Default: false * enabled?: bool|Param, // Default: false

View File

@ -14,6 +14,7 @@ parameters:
oauth_sso_identifier: '%env(OAUTH_SSO_IDENTIFIER)%' oauth_sso_identifier: '%env(OAUTH_SSO_IDENTIFIER)%'
oauth_sso_identifier_login: '%env(OAUTH_SSO_IDENTIFIER_LOGIN)%' oauth_sso_identifier_login: '%env(OAUTH_SSO_IDENTIFIER_LOGIN)%'
easycheck_url: '%env(EASYCHECK_URL)%' 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
@ -58,12 +59,13 @@ services:
App\Command\CreateSuperAdminCommand: App\Command\CreateSuperAdminCommand:
arguments: arguments:
$environment: '%kernel.environment%' $environment: '%kernel.environment%'
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
App\EventListener\LogoutSubscriber: App\EventListener\LogoutSubscriber:
arguments: arguments:
$easycheckUrl: '%env(EASYCHECK_URL)%' $easycheckUrl: '%env(EASYCHECK_URL)%'
tags: tags:
- { name: kernel.event_subscriber } - { name: kernel.event_subscriber }
App\Webhook\OrganizationNotifier:
arguments:
$easycheckUrl: '%easycheck_url%'
$webhookSecret: '%webhook_secret%'

View File

@ -9,8 +9,8 @@ use App\Form\OrganizationForm;
use App\Repository\OrganizationsRepository; use App\Repository\OrganizationsRepository;
use App\Repository\UsersOrganizationsRepository; use App\Repository\UsersOrganizationsRepository;
use App\Service\ActionService; use App\Service\ActionService;
use App\Service\AwsService;
use App\Service\LoggerService; use App\Service\LoggerService;
use App\Webhook\OrganizationNotifier;
use App\Service\OrganizationsService; use App\Service\OrganizationsService;
use App\Service\UserOrganizationService; use App\Service\UserOrganizationService;
use App\Service\UserService; use App\Service\UserService;
@ -37,7 +37,9 @@ class OrganizationController extends AbstractController
private readonly ActionService $actionService, private readonly ActionService $actionService,
private readonly UserOrganizationService $userOrganizationService, private readonly UserOrganizationService $userOrganizationService,
private readonly OrganizationsRepository $organizationsRepository, private readonly OrganizationsRepository $organizationsRepository,
private readonly LoggerService $loggerService, private readonly UsersOrganizationsRepository $usersOrganizationsRepository) private readonly LoggerService $loggerService,
private readonly UsersOrganizationsRepository $usersOrganizationsRepository,
private readonly OrganizationNotifier $organizationNotifier)
{ {
} }
@ -111,6 +113,8 @@ class OrganizationController extends AbstractController
$this->entityManager->persist($organization); $this->entityManager->persist($organization);
$this->entityManager->flush(); $this->entityManager->flush();
//webhook notify
$this->organizationNotifier->notifyOrganizationCreated($organization);
// Loggers... // Loggers...
$this->loggerService->logOrganizationInformation($organization->getId(), $actingUser->getUserIdentifier(), "Organization Created via ajax"); $this->loggerService->logOrganizationInformation($organization->getId(), $actingUser->getUserIdentifier(), "Organization Created via ajax");
@ -375,7 +379,7 @@ class OrganizationController extends AbstractController
/* /*
* Path used to get data on an organization for the edit modal * Path used to get data on an organization for the edit modal
*/ */
#[Route(path: '/{id}', name: 'get', methods: ['GET'])] #[Route(path: '/editModal/{id}', name: 'get', methods: ['GET'])]
public function get(int $id): JsonResponse{ public function get(int $id): JsonResponse{
$this->denyAccessUnlessGranted('ROLE_USER'); $this->denyAccessUnlessGranted('ROLE_USER');
$actingUser = $this->getUser(); $actingUser = $this->getUser();

View File

@ -872,11 +872,12 @@ class UserController extends AbstractController
$email = $user->getEmail(); $email = $user->getEmail();
$existingUser = $this->userRepository->findOneBy(['email' => $email]); $existingUser = $this->userRepository->findOneBy(['email' => $email]);
if($this->userService->checkUserOrganizationLinkExists($existingUser, $org)){
return $this->json(['error' => "L'utilisateur existe déjà dans votre organisation"], 400);
}
// CASE A: User exists -> Add to org // CASE A: User exists -> Add to org
if ($existingUser) { if ($existingUser) {
if($this->userService->checkUserOrganizationLinkExists($existingUser, $org)){
return $this->json(['error' => "L'utilisateur existe déjà dans votre organisation"], 400);
}
// Check if already in org to avoid logic errors or duplicate logs // Check if already in org to avoid logic errors or duplicate logs
$this->userService->addExistingUserToOrganization($existingUser, $org, $selectedApps); $this->userService->addExistingUserToOrganization($existingUser, $org, $selectedApps);

View File

@ -43,7 +43,7 @@ class LogoutSubscriber implements EventSubscriberInterface
$params['redirect_app'] = $redirectApp; $params['redirect_app'] = $redirectApp;
} }
$easycheckLogoutUrl = $this->easycheckUrl . '/logout?' . http_build_query($params); $easycheckLogoutUrl = rtrim($this->easycheckUrl, '/'). '/logout?' . http_build_query($params);
$this->logger->info('Redirecting to EasyCheck logout', [ $this->logger->info('Redirecting to EasyCheck logout', [
'url' => $easycheckLogoutUrl, 'url' => $easycheckLogoutUrl,

View File

@ -59,7 +59,7 @@ class LoginSubscriber implements EventSubscriberInterface
$user->setLastConnection(new \DateTime('now', new \DateTimeZone('Europe/Paris'))); $user->setLastConnection(new \DateTime('now', new \DateTimeZone('Europe/Paris')));
$easySolution = $this->entityManager->getRepository(Client::class)->findOneBy(['identifier' => $this->clientIdentifier]); $easySolution = $this->entityManager->getRepository(Client::class)->findOneBy(['identifier' => $this->clientIdentifier]);
/*if ($easySolution) { if ($easySolution) {
$accessToken = new AccessToken( $accessToken = new AccessToken(
identifier: bin2hex(random_bytes(40)), identifier: bin2hex(random_bytes(40)),
expiry: new \DateTimeImmutable('+1 hour', new \DateTimeZone('Europe/Paris')), expiry: new \DateTimeImmutable('+1 hour', new \DateTimeZone('Europe/Paris')),
@ -70,7 +70,7 @@ class LoginSubscriber implements EventSubscriberInterface
$this->entityManager->persist($user); $this->entityManager->persist($user);
$this->entityManager->persist($accessToken); $this->entityManager->persist($accessToken);
$this->entityManager->flush(); $this->entityManager->flush();
}*/ }
} }
// Vérifier si un paramètre redirect_app est présent dans l'URL // Vérifier si un paramètre redirect_app est présent dans l'URL

View File

@ -181,12 +181,11 @@ readonly class LoggerService
])); ]));
} }
public function logUOALinkDeactivated(int $uoaId, int $appId, int $roleId): void public function logUOALinkDeactivated(int $uoaId, int $appId): void
{ {
$this->organizationManagementLogger->notice('UOA link deactivated', [ $this->organizationManagementLogger->notice('UOA link deactivated', [
'uoa_id' => $uoaId, 'uoa_id' => $uoaId,
'app_id' => $appId, 'app_id' => $appId,
'role_id' => $roleId,
'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown', 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown',
'timestamp' => $this->now(), 'timestamp' => $this->now(),
]); ]);

View File

@ -94,14 +94,13 @@ class UserOrganizationAppService
try{ try{
$uoa->setIsActive(false); $uoa->setIsActive(false);
$this->actionService->createAction("Deactivate UOA link", $userOrganization->getUsers(), $this->actionService->createAction("Deactivate UOA link", $userOrganization->getUsers(),
$userOrganization->getOrganization(), "App: " . $uoa->getApplication()->getName() . ", Role: " . $uoa->getRole()->getName()); $userOrganization->getOrganization(), "App: " . $uoa->getApplication()->getName());
$this->entityManager->persist($uoa); $this->entityManager->persist($uoa);
$this->loggerService->logUOALinkDeactivated($uoa->getId(), $uoa->getApplication()->getId(), $uoa->getRole()->getId()); $this->loggerService->logUOALinkDeactivated($uoa->getId(), $uoa->getApplication()->getId());
}catch (\Exception $exception){ }catch (\Exception $exception){
$this->loggerService->logCritical("Error deactivating UOA link", [ $this->loggerService->logCritical("Error deactivating UOA link", [
'uoa_id' => $uoa->getId(), 'uoa_id' => $uoa->getId(),
'app_id' => $uoa->getApplication()->getId(), 'app_id' => $uoa->getApplication()->getId(),
'role_id' => $uoa->getRole()->getId(),
'exception_message' => $exception->getMessage(), 'exception_message' => $exception->getMessage(),
]); ]);
} }

View File

@ -0,0 +1,45 @@
<?php
namespace App\Webhook;
use App\Entity\Organizations;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\RemoteEvent\RemoteEvent;
use Symfony\Component\Webhook\Messenger\SendWebhookMessage;
use Symfony\Component\Webhook\Subscriber;
class OrganizationNotifier
{
public function __construct(
private readonly MessageBusInterface $messageBus,
private readonly string $easycheckUrl,
private readonly string $webhookSecret
) {
}
public function notifyOrganizationCreated(Organizations $organizations): void
{
$subscriber = new Subscriber(
url: rtrim($this->easycheckUrl, '/'). '/webhook/organization_created',
secret: $this->webhookSecret,
);
$event = new RemoteEvent(
name: 'organization_created',
id: uniqid('', true),
payload: [
'orgId' => $organizations->getId(),
'orgName' => $organizations->getName(),
'orgEmail' => $organizations->getEmail(),
'orgNumber' => $organizations->getNumber(),
'orgAddress' => $organizations->getAddress(),
'orgLogo' => $organizations->getLogoUrl(),
'created_at' => time(),
]
);
$this->messageBus->dispatch(
new SendWebhookMessage($subscriber, $event)
);
}
}

View File

@ -414,5 +414,11 @@
"files": [ "files": [
"config/packages/messenger.yaml" "config/packages/messenger.yaml"
] ]
},
"symfony/webhook": {
"version": "7.4",
"recipe": {
"version": "7.3"
}
} }
} }

View File

@ -26,49 +26,7 @@
{% endif %} {% endif %}
{# New organization modal #} {# New organization modal #}
<div class="modal fade" id="createOrganizationModal" tabindex="-1" aria-labelledby="createOrganizationModalLabel" {{ include('organization/organizationModal.html.twig') }}
aria-hidden="true"
data-organization-target="modal">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="createOrganizationModalLabel">Créer une organisation</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form data-action="submit->organization#createOrganization">
<div class="mb-3">
<label for="organizationName" class="form-label">Nom de l'organisation</label>
<input type="text" class="form-control" id="organizationName" name="name" required>
</div>
<div class="mb-3">
<label for="organizationEmail" class="form-label">Email</label>
<input class="form-control" id="organizationEmail" type="email" name="email" required>
</div>
<div class="mb-3">
<label for="organizationPhone" class="form-label">Téléphone</label>
<input class="form-control" type="number" id="organizationPhone" name="phone">
</div>
<div class="mb-3">
<label for="organizationAddress" class="form-label">Adresse</label>
<input class="form-control" id="organizationAddress" name="address">
</div>
<div class="mb-3">
<label class="form-label" for="organizationLogo">Logo de l'organisation</label>
<input type="file" name="logoUrl" id="organizationLogo" class="form-control"
accept="image/*">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Annuler
</button>
<button type="submit" class="btn btn-primary">Créer l'organisation</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div> </div>
<div class="card-body"> <div class="card-body">
{% if is_granted('ROLE_SUPER_ADMIN') and not hasOrganizations %} {% if is_granted('ROLE_SUPER_ADMIN') and not hasOrganizations %}