From e50bb0402a38428c3f3f17d18c28a645d228b759 Mon Sep 17 00:00:00 2001 From: Charles Date: Wed, 18 Feb 2026 11:35:44 +0100 Subject: [PATCH] Set up api calls --- .env | 1 + assets/controllers/project_controller.js | 11 ++--- config/packages/security.yaml | 7 ++- config/services.yaml | 5 ++ src/Controller/ProjectController.php | 6 ++- .../api/Security/SecurityController.php | 22 +++++++++ src/Entity/AccessToken.php | 5 ++ src/EventSubscriber/LoginSubscriber.php | 30 +++++++++--- src/Repository/AccessTokenRepository.php | 3 +- src/Service/SSO/ProjectService.php | 49 +++++++++++++++++++ 10 files changed, 122 insertions(+), 17 deletions(-) create mode 100644 src/Controller/api/Security/SecurityController.php create mode 100644 src/Service/SSO/ProjectService.php diff --git a/.env b/.env index 625e7ee..87ddb29 100644 --- a/.env +++ b/.env @@ -48,6 +48,7 @@ OAUTH_PRIVATE_KEY=%kernel.project_dir%/config/jwt/private.key OAUTH_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.key OAUTH_PASSPHRASE=8170ea18d2e3e05b5c7ae0672a754bf4 OAUTH_ENCRYPTION_KEY=f1b7c279f7992205a0df45e295d07066 +OAUTH_SSO_SECRET='sso-own-secret' ###< league/oauth2-server-bundle ### ###> nelmio/cors-bundle ### diff --git a/assets/controllers/project_controller.js b/assets/controllers/project_controller.js index e7bf00f..bcf2e12 100644 --- a/assets/controllers/project_controller.js +++ b/assets/controllers/project_controller.js @@ -2,10 +2,9 @@ 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"; -import base_controller from "./base_controller.js"; -export default class extends base_controller { +export default class extends Controller { static values = { listProject : Boolean, orgId: Number, @@ -176,13 +175,13 @@ export default class extends base_controller { this.currentProjectId = projectId; this.modal.show(); - this.nameInputTarget.disabled = true; this.formTitleTarget.textContent = "Modifier le projet"; try { // 1. Ensure checkboxes are loaded first - const apps = await this.fetchAndRenderApplications(this.appListTarget); + await this.loadApplications(); + // 2. Fetch the project data const response = await fetch(`/project/data/${projectId}`); const project = await response.json(); @@ -204,13 +203,13 @@ export default class extends base_controller { } } // Update your openCreateModal to reset the state - async openCreateModal() { + openCreateModal() { this.currentProjectId = null; this.modal.show(); this.nameInputTarget.disabled = false; this.nameInputTarget.value = ""; this.formTitleTarget.textContent = "Nouveau Projet"; - await this.fetchAndRenderApplications(); + this.loadApplications(); } async deleteProject(event) { diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 58afd30..8dae615 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -19,6 +19,10 @@ security: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false + api_token_validation: + pattern: ^/api/validate-token + stateless: true + oauth2: true oauth_userinfo: pattern: ^/oauth2/userinfo stateless: true @@ -65,6 +69,7 @@ security: # Note: Only the *first* access control that matches will be used access_control: - { 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 } @@ -76,8 +81,6 @@ security: - { path: ^/oauth2/userinfo, roles: IS_AUTHENTICATED_FULLY } - { path: ^/, roles: ROLE_USER } - - when@test: security: password_hashers: diff --git a/config/services.yaml b/config/services.yaml index b6113ee..7d3bb06 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -10,6 +10,7 @@ parameters: app_url: '%env(APP_URL)%' mercure_secret: '%env(MERCURE_JWT_SECRET)%' logos_directory: '%kernel.project_dir%/public/uploads/logos' + oauth_sso_secret: '%env(OAUTH_SSO_SECRET)%' services: # default configuration for services in *this* file @@ -28,6 +29,10 @@ services: App\MessageHandler\NotificationMessageHandler: arguments: $appUrl: '%app_url%' + App\Service\SSO\ProjectService: + arguments: + $appUrl: '%app_url%' + $clientSecret: '%oauth_sso_secret%' App\EventSubscriber\: resource: '../src/EventSubscriber/' tags: ['kernel.event_subscriber'] diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php index f6511b0..f1a9fa8 100644 --- a/src/Controller/ProjectController.php +++ b/src/Controller/ProjectController.php @@ -8,6 +8,7 @@ use App\Repository\AppsRepository; use App\Repository\OrganizationsRepository; use App\Repository\ProjectRepository; use App\Service\ProjectService; +use App\Service\SSO\ProjectService as SSOProjectService; use App\Service\UserService; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -25,7 +26,9 @@ final class ProjectController extends AbstractController private readonly OrganizationsRepository $organizationsRepository, private readonly ProjectRepository $projectRepository, private readonly ProjectService $projectService, - private readonly UserService $userService, private readonly AppsRepository $appsRepository) + private readonly AppsRepository $appsRepository, + private readonly SSOProjectService $SSOProjectService, + ) { } @@ -61,6 +64,7 @@ final class ProjectController extends AbstractController $project->setOrganization($org); $project->setApplications($data['applications']); $this->entityManager->persist($project); + $this->SSOProjectService->createRemoteProject('http://api.solutions-easy.moi', $project); $this->entityManager->flush(); return new JsonResponse(['message' => 'Project created successfully'], Response::HTTP_CREATED); } diff --git a/src/Controller/api/Security/SecurityController.php b/src/Controller/api/Security/SecurityController.php new file mode 100644 index 0000000..ed95b28 --- /dev/null +++ b/src/Controller/api/Security/SecurityController.php @@ -0,0 +1,22 @@ +getUser(); + + return $this->json([ + 'valid' => true, + 'email' => ($user instanceof \App\Entity\User) ? $user->getUserIdentifier() : null, + 'scopes' => $this->container->get('security.token_storage')->getToken()->getScopes(), + ]); + } +} \ No newline at end of file diff --git a/src/Entity/AccessToken.php b/src/Entity/AccessToken.php index ff51556..5936145 100644 --- a/src/Entity/AccessToken.php +++ b/src/Entity/AccessToken.php @@ -32,4 +32,9 @@ final class AccessToken implements AccessTokenEntityInterface ->getToken($this->jwtConfiguration->signer(), $this->jwtConfiguration->signingKey()); } + public function setUserIdentifier(?string $userIdentifier): void + { + $this->userIdentifier = $userIdentifier; + } + } \ No newline at end of file diff --git a/src/EventSubscriber/LoginSubscriber.php b/src/EventSubscriber/LoginSubscriber.php index 5438de6..3c3febc 100644 --- a/src/EventSubscriber/LoginSubscriber.php +++ b/src/EventSubscriber/LoginSubscriber.php @@ -28,19 +28,37 @@ class LoginSubscriber implements EventSubscriberInterface public function onLoginSuccess(LoginSuccessEvent $event): void { - $user = $event->getUser(); - if($user) { - $user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $user->getUserIdentifier()]); + $passportUser = $event->getUser(); + + // 1. Check if we have a user at all + if (!$passportUser) { + return; + } + + // 2. IMPORTANT: Check if this is a real User entity from your DB. + // If it's a Machine/Client login, it will be an instance of + // League\Bundle\OAuth2ServerBundle\Security\User\ClientCredentialsUser + if (!$passportUser instanceof \App\Entity\User) { + // It's a machine (M2M), so we don't track "last connection" or create manual tokens + return; + } + + // Now we know it's a real human user + $user = $this->entityManager->getRepository(User::class)->findOneBy([ + 'email' => $passportUser->getUserIdentifier() + ]); + + if ($user) { $user->setLastConnection(new \DateTime('now', new \DateTimeZone('Europe/Paris'))); $easySolution = $this->entityManager->getRepository(Client::class)->findOneBy(['name' => 'EasySolution']); - if($easySolution) { + if ($easySolution) { $accessToken = new AccessToken( - identifier: bin2hex(random_bytes(40)), // Generate unique identifier + identifier: bin2hex(random_bytes(40)), expiry: new \DateTimeImmutable('+1 hour', new \DateTimeZone('Europe/Paris')), client: $easySolution, userIdentifier: $user->getUserIdentifier(), - scopes: ['email profile openid apps:easySolutions'] // Empty array if no specific scopes needed + scopes: ['email', 'profile', 'openid', 'apps:easySolutions'] ); $this->entityManager->persist($user); $this->entityManager->persist($accessToken); diff --git a/src/Repository/AccessTokenRepository.php b/src/Repository/AccessTokenRepository.php index 6ac6134..103262b 100644 --- a/src/Repository/AccessTokenRepository.php +++ b/src/Repository/AccessTokenRepository.php @@ -25,8 +25,7 @@ final class AccessTokenRepository implements AccessTokenRepositoryInterface /** @var int|string|null $userIdentifier */ $accessToken = new AccessTokenEntity(); $accessToken->setClient($clientEntity); - $accessToken->setUserIdentifier($userIdentifier); - + $accessToken->setUserIdentifier($userIdentifier ?? $clientEntity->getIdentifier()); foreach ($scopes as $scope) { $accessToken->addScope($scope); } diff --git a/src/Service/SSO/ProjectService.php b/src/Service/SSO/ProjectService.php new file mode 100644 index 0000000..60586ed --- /dev/null +++ b/src/Service/SSO/ProjectService.php @@ -0,0 +1,49 @@ +httpClient->request('POST', $this->appUrl . 'token', [ + 'auth_basic' => ['afc7b28b95b61aeeeae8eaed94c5cfe1', $this->clientSecret], // ID and Secret go here + 'body' => [ + 'grant_type' => 'client_credentials', +// 'scope' => 'project_sync' + ], + ]); +// if (400 === $tokenResponse->getStatusCode() || 500 === $tokenResponse->getStatusCode()) { +// // This will print the actual OAuth2 error (e.g., "invalid_scope" or "unsupported_grant_type") +// dd($tokenResponse->getContent(false)); +// } + $accessToken = $tokenResponse->toArray()['access_token']; +// data must match easy check database + $projectJson = [ + 'id' => $project->getId(), + 'projet' => $project->getName(), + 'entity_id' => 3, + 'bdd' => $project->getBddName(), + 'isactive' => $project->isActive(), + ]; + + // 2. Call the Client Application's Webhook/API + $this->httpClient->request('POST', $clientAppUrl . '/api/v1/project/create', [ + 'headers' => ['Authorization' => 'Bearer ' . $accessToken], + 'json' => $projectJson + ]); + } + +} \ No newline at end of file