diff --git a/assets/controllers/application_controller.js b/assets/controllers/application_controller.js
index 5aba951..ebc6e66 100644
--- a/assets/controllers/application_controller.js
+++ b/assets/controllers/application_controller.js
@@ -5,8 +5,9 @@ export default class extends Controller {
static values = {
application: String,
organization: String,
+ user: Number,
}
- static targets = ['hidden', 'submitBtn']
+ static targets = ['hidden', 'submitBtn', 'appList']
connect() {
// Map each editor to its toolbar and hidden field
@@ -40,6 +41,9 @@ export default class extends Controller {
hiddenTarget.value = quill.root.innerHTML
})
}
+ if(this.userValue){
+ this.loadApplications();
+ }
}
handleAuthorizeSubmit(event) {
@@ -107,4 +111,59 @@ export default class extends Controller {
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 = `Erreur`;
+ }
+ }
+
+ renderApps(apps) {
+ if (apps.length === 0) {
+ // Span 2 columns if empty so the message is centered
+ this.appListTarget.innerHTML = `Aucune application`;
+ return;
+ }
+
+ const html = apps.map(app => {
+ const url = `https://${app.subDomain}.solutions-easy.moi`;
+
+ // Check for logo string vs object
+ const logoSrc = (typeof app.logoMiniUrl === 'string') ? app.logoMiniUrl : '';
+
+ // Render Icon (Image or Fallback)
+ const iconHtml = logoSrc
+ ? `
`
+ : ``;
+
+ // Return a Card-like block
+ return `
+
+
+ ${iconHtml}
+
+
+ ${app.name}
+
+
+ `;
+ }).join('');
+
+ this.appListTarget.innerHTML = html;
+ }
}
\ No newline at end of file
diff --git a/docs/Client_Setup.md b/docs/Client_Setup.md
new file mode 100644
index 0000000..51917c0
--- /dev/null
+++ b/docs/Client_Setup.md
@@ -0,0 +1,327 @@
+# 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:
+ }
+ use_state: false
+```
+
+### .env
+```dotenv
+# .env
+# CORS
+CORS_ALLOW_ORIGIN=http://*.your domain/*'
+# OAUTH2
+OAUTH2_CLIENT_ID=
+OAUTH2_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
+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($sudalysSsoUser->getEmail());
+ $user->setName($sudalysSsoUser->getName());
+ $user->setSurname($sudalysSsoUser->getSurname());
+ $user->setSsoId($sudalysSsoUser->getId());
+ $this->em->persist($user);
+ }else{
+ // On met a jour l'utilisateur
+ $user->setEmail($sudalysSsoUser->getEmail());
+ $user->setName($sudalysSsoUser->getName());
+ $user->setSurname($sudalysSsoUser->getSurname());
+ $this->em->persist($user);
+ }
+ $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
+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 --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 can be found in the database oauth2_client table
+The 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
+```
diff --git a/src/Controller/ApplicationController.php b/src/Controller/ApplicationController.php
index e883449..b7aa207 100644
--- a/src/Controller/ApplicationController.php
+++ b/src/Controller/ApplicationController.php
@@ -4,26 +4,32 @@ namespace App\Controller;
use App\Entity\Apps;
use App\Entity\Organizations;
+use App\Repository\UserRepository;
use App\Service\ActionService;
use App\Service\ApplicationService;
use App\Service\LoggerService;
+use App\Service\UserOrganizationAppService;
use App\Service\UserService;
use Doctrine\ORM\EntityManagerInterface;
+use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
-use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
-
+use Symfony\Component\Asset\Packages;
#[Route(path: '/application', name: 'application_')]
class ApplicationController extends AbstractController
{
public function __construct(private readonly EntityManagerInterface $entityManager,
- private readonly UserService $userService,
- private readonly ActionService $actionService,
- private readonly LoggerService $loggerService,
- private readonly ApplicationService $applicationService)
+ private readonly UserService $userService,
+ private readonly ActionService $actionService,
+ private readonly LoggerService $loggerService,
+ private readonly ApplicationService $applicationService,
+ private readonly UserRepository $userRepository,
+ private Packages $assetsManager,
+ private readonly UserOrganizationAppService $userOrganizationAppService)
{
}
@@ -172,4 +178,30 @@ class ApplicationController extends AbstractController
return new Response('', Response::HTTP_OK);
}
+
+ #[Route(path:'/user/{id}', name: 'user', methods: ['GET'])]
+ public function getApplicationUsers(int $id): JSONResponse
+ {
+ $user = $this->userRepository->find($id);
+ $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
+ if (!$user) {
+ $this->loggerService->logEntityNotFound('User', ['message'=> 'User not found for application list'], $actingUser->getId());
+ return new JsonResponse(['error' => 'User not found'], Response::HTTP_NOT_FOUND);
+ }
+ if ($this->isGranted('ROLE_SUPER_ADMIN')) {
+ $applications = $this->entityManager->getRepository(Apps::class)->findAll();
+ }else{
+ $applications = $this->userOrganizationAppService->getUserApplications($user);
+
+ }
+ $data = array_map(function($app) {
+ return [
+ 'name' => $app->getName(),
+ 'subDomain' => $app->getSubDomain(),
+ 'logoMiniUrl' => $this->assetsManager->getUrl($app->getLogoMiniUrl()),
+ ];
+ }, $applications);
+
+ return new JsonResponse($data, Response::HTTP_OK);
+ }
}
diff --git a/src/Service/UserOrganizationAppService.php b/src/Service/UserOrganizationAppService.php
index bf2615f..53fc759 100644
--- a/src/Service/UserOrganizationAppService.php
+++ b/src/Service/UserOrganizationAppService.php
@@ -248,4 +248,26 @@ class UserOrganizationAppService
$uoaAdmin->setIsActive(true);
}
}
+
+ /**
+ * Get users applications links for a given user
+ *
+ * @param User $user
+ * @return Apps[]
+ */
+ public function getUserApplications(User $user): array
+ {
+ $uos = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['users' => $user]);
+ $apps = [];
+ foreach ($uos as $uo) {
+ $uoas = $this->entityManager->getRepository(UserOrganizatonApp::class)->findBy(['userOrganization' => $uo, 'isActive' => true]);
+ foreach ($uoas as $uoa) {
+ $app = $uoa->getApplication();
+ if (!in_array($app, $apps, true)) {
+ $apps[] = $app;
+ }
+ }
+ }
+ return $apps;
+ }
}
diff --git a/templates/application/edit.html.twig b/templates/application/edit.html.twig
index d694a73..7c89081 100644
--- a/templates/application/edit.html.twig
+++ b/templates/application/edit.html.twig
@@ -14,6 +14,9 @@
{# Description (full) #}