Set up api calls

This commit is contained in:
Charles 2026-02-18 11:35:44 +01:00
parent b9b0efd6c6
commit e50bb0402a
10 changed files with 122 additions and 17 deletions

1
.env
View File

@ -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 ###

View File

@ -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) {

View File

@ -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:

View File

@ -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']

View File

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

View File

@ -0,0 +1,22 @@
<?php
namespace App\Controller\api\Security;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;
class SecurityController extends AbstractController{
#[Route('/api/validate-token', name: 'api_validate_token')]
public function validate(): JsonResponse
{
$user = $this->getUser();
return $this->json([
'valid' => true,
'email' => ($user instanceof \App\Entity\User) ? $user->getUserIdentifier() : null,
'scopes' => $this->container->get('security.token_storage')->getToken()->getScopes(),
]);
}
}

View File

@ -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;
}
}

View File

@ -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);

View File

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

View File

@ -0,0 +1,49 @@
<?php
namespace App\Service\SSO;
use App\Entity\Project;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class ProjectService
{
public function __construct(private readonly HttpClientInterface $httpClient,
private string $appUrl,
private string $clientSecret)
{
}
// Inside your SSO Server Service
public function createRemoteProject(string $clientAppUrl, Project $project): void
{
// 1. Get a token for "ourselves"
$tokenResponse = $this->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
]);
}
}