Merge branch 'docs' into 'develop'
Docs See merge request easy-solutions/apps/easyportal!25
This commit is contained in:
commit
8317617288
|
|
@ -5,8 +5,9 @@ export default class extends Controller {
|
||||||
static values = {
|
static values = {
|
||||||
application: String,
|
application: String,
|
||||||
organization: String,
|
organization: String,
|
||||||
|
user: Number,
|
||||||
}
|
}
|
||||||
static targets = ['hidden', 'submitBtn']
|
static targets = ['hidden', 'submitBtn', 'appList']
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
// Map each editor to its toolbar and hidden field
|
// Map each editor to its toolbar and hidden field
|
||||||
|
|
@ -40,6 +41,9 @@ export default class extends Controller {
|
||||||
hiddenTarget.value = quill.root.innerHTML
|
hiddenTarget.value = quill.root.innerHTML
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if(this.userValue){
|
||||||
|
this.loadApplications();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAuthorizeSubmit(event) {
|
handleAuthorizeSubmit(event) {
|
||||||
|
|
@ -107,4 +111,59 @@ export default class extends Controller {
|
||||||
alert('Erreur lors de l\'action');
|
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.moi`;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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: <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($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
|
||||||
|
<?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
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
@ -4,26 +4,32 @@ namespace App\Controller;
|
||||||
|
|
||||||
use App\Entity\Apps;
|
use App\Entity\Apps;
|
||||||
use App\Entity\Organizations;
|
use App\Entity\Organizations;
|
||||||
|
use App\Repository\UserRepository;
|
||||||
use App\Service\ActionService;
|
use App\Service\ActionService;
|
||||||
use App\Service\ApplicationService;
|
use App\Service\ApplicationService;
|
||||||
use App\Service\LoggerService;
|
use App\Service\LoggerService;
|
||||||
|
use App\Service\UserOrganizationAppService;
|
||||||
use App\Service\UserService;
|
use App\Service\UserService;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
|
use Symfony\Component\Asset\Packages;
|
||||||
|
|
||||||
#[Route(path: '/application', name: 'application_')]
|
#[Route(path: '/application', name: 'application_')]
|
||||||
|
|
||||||
class ApplicationController extends AbstractController
|
class ApplicationController extends AbstractController
|
||||||
{
|
{
|
||||||
public function __construct(private readonly EntityManagerInterface $entityManager,
|
public function __construct(private readonly EntityManagerInterface $entityManager,
|
||||||
private readonly UserService $userService,
|
private readonly UserService $userService,
|
||||||
private readonly ActionService $actionService,
|
private readonly ActionService $actionService,
|
||||||
private readonly LoggerService $loggerService,
|
private readonly LoggerService $loggerService,
|
||||||
private readonly ApplicationService $applicationService)
|
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);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -248,4 +248,26 @@ class UserOrganizationAppService
|
||||||
$uoaAdmin->setIsActive(true);
|
$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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,9 @@
|
||||||
{# Description (full) #}
|
{# Description (full) #}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<h5>Description complète</h5>
|
||||||
|
</div>
|
||||||
<div id="toolbar-description" class="border-0">
|
<div id="toolbar-description" class="border-0">
|
||||||
<button class="ql-bold"></button>
|
<button class="ql-bold"></button>
|
||||||
<button class="ql-italic"></button>
|
<button class="ql-italic"></button>
|
||||||
|
|
@ -53,6 +56,9 @@
|
||||||
{# Description Small #}
|
{# Description Small #}
|
||||||
<div class="card mt-3">
|
<div class="card mt-3">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<h5>Description courte</h5>
|
||||||
|
</div>
|
||||||
<div id="toolbar-descriptionSmall" class="border-0">
|
<div id="toolbar-descriptionSmall" class="border-0">
|
||||||
<button class="ql-bold"></button>
|
<button class="ql-bold"></button>
|
||||||
<button class="ql-italic"></button>
|
<button class="ql-italic"></button>
|
||||||
|
|
|
||||||
|
|
@ -1,61 +1,42 @@
|
||||||
<nav class="sidebar sidebar-offcanvas" id="sidebar">
|
<nav class="sidebar sidebar-offcanvas" id="sidebar">
|
||||||
|
{# 1. Get the current route name into a variable for easier use #}
|
||||||
|
{% set current_route = app.request.attributes.get('_route') %}
|
||||||
|
|
||||||
<ul class="nav">
|
<ul class="nav">
|
||||||
{% if is_granted("ROLE_SUPER_ADMIN") %}
|
{% if is_granted("ROLE_SUPER_ADMIN") %}
|
||||||
<li class="nav-item active">
|
{# 2. Check if route is 'app_index' #}
|
||||||
<a class="nav-link" href="#">
|
<li class="nav-item {{ current_route == 'app_index' ? 'active' : '' }}">
|
||||||
|
<a class="nav-link" href="{{ path('app_index') }}">
|
||||||
<i class="icon-grid menu-icon">{{ ux_icon('material-symbols:dashboard-outline-rounded', {height: '16px', width: '16px'}) }}</i>
|
<i class="icon-grid menu-icon">{{ ux_icon('material-symbols:dashboard-outline-rounded', {height: '16px', width: '16px'}) }}</i>
|
||||||
<span class="menu-title">Dashboard</span>
|
<span class="menu-title">Dashboard</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{# <!-- Example of a collapsible menu item -->#}
|
|
||||||
{# <li class="nav-item">#}
|
{# 3. Check if route starts with 'application_' (covers index, edit, show, etc.) #}
|
||||||
{# <a class="nav-link" data-bs-toggle="collapse" href="#ui-basic" aria-expanded="false"#}
|
<li class="nav-item {{ current_route starts with 'application_' ? 'active' : '' }}">
|
||||||
{# aria-controls="ui-basic">#}
|
|
||||||
{# <i class="icon-layout menu-icon">{{ ux_icon('bi:menu-up', {height: '16px', width: '16px'}) }}</i>#}
|
|
||||||
{# <span class="menu-title">Menu</span>#}
|
|
||||||
{# <i class="menu-arrow">{{ ux_icon('bi:chevron-right', {height: '16px', width: '16px'}) }}</i>#}
|
|
||||||
{# </a>#}
|
|
||||||
{# <div class="collapse" id="ui-basic">#}
|
|
||||||
{# <ul class="nav sub-menu flex-column">#}
|
|
||||||
{# <li class="nav-item">{{ ux_icon('material-symbols-light:play-arrow-outline', {height: '16px', width: '16px'}) }}#}
|
|
||||||
{# <a class="nav-link" href="#">Accordions</a></li>#}
|
|
||||||
{# <li class="nav-item">{{ ux_icon('material-symbols-light:play-arrow-outline', {height: '16px', width: '16px'}) }}#}
|
|
||||||
{# <a class="nav-link" href="#">Buttons</a></li>#}
|
|
||||||
{# <li class="nav-item">{{ ux_icon('material-symbols-light:play-arrow-outline', {height: '16px', width: '16px'}) }}#}
|
|
||||||
{# <a class="nav-link" href="#">Badges</a></li>#}
|
|
||||||
{# <li class="nav-item">{{ ux_icon('material-symbols-light:play-arrow-outline', {height: '16px', width: '16px'}) }}#}
|
|
||||||
{# <a class="nav-link" href="#">Breadcrumbs</a></li>#}
|
|
||||||
{# <li class="nav-item">{{ ux_icon('material-symbols-light:play-arrow-outline', {height: '16px', width: '16px'}) }}#}
|
|
||||||
{# <a class="nav-link" href="#">Dropdowns</a></li>#}
|
|
||||||
{# </ul>#}
|
|
||||||
{# </div>#}
|
|
||||||
{# </li>#}
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="{{ path('application_index') }}">
|
<a class="nav-link" href="{{ path('application_index') }}">
|
||||||
<i class="icon-grid menu-icon">{{ ux_icon('material-symbols:settings-applications-outline', {height: '15px', width: '15px'}) }}</i>
|
<i class="icon-grid menu-icon">{{ ux_icon('material-symbols:settings-applications-outline', {height: '15px', width: '15px'}) }}</i>
|
||||||
<span class="menu-title">Applications</span>
|
<span class="menu-title">Applications</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{# if user is Super Admin #}
|
|
||||||
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
||||||
<li class="nav-item">
|
<li class="nav-item {{ current_route starts with 'user_' ? 'active' : '' }}">
|
||||||
<a class="nav-link" href="{{ path('user_index') }}">
|
<a class="nav-link" href="{{ path('user_index') }}">
|
||||||
<i class="icon-grid menu-icon">{{ ux_icon('fa6-regular:circle-user', {height: '15px', width: '15px'}) }}</i>
|
<i class="icon-grid menu-icon">{{ ux_icon('fa6-regular:circle-user', {height: '15px', width: '15px'}) }}</i>
|
||||||
<span class="menu-title">Users</span>
|
<span class="menu-title">Users</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li class="nav-item">
|
|
||||||
|
<li class="nav-item {{ current_route starts with 'organization_' ? 'active' : '' }}">
|
||||||
{% if is_granted('ROLE_ADMIN') %}
|
{% if is_granted('ROLE_ADMIN') %}
|
||||||
<a class="nav-link" href="{{ path('organization_index') }}">
|
<a class="nav-link" href="{{ path('organization_index') }}">
|
||||||
<i class="icon-grid menu-icon"> {{ ux_icon('bi:buildings', {height: '15px', width: '15px'}) }}
|
<i class="icon-grid menu-icon"> {{ ux_icon('bi:buildings', {height: '15px', width: '15px'}) }}</i>
|
||||||
</i>
|
<span class="menu-title">Organizations</span>
|
||||||
<span class="menu-title">
|
|
||||||
Organizations</span>
|
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
@ -72,6 +72,35 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item dropdown nav-notif"
|
||||||
|
data-controller="application"
|
||||||
|
data-application-user-value="{{ app.user.id }}"
|
||||||
|
>
|
||||||
|
<a id="applicationDropdown"
|
||||||
|
class="nav-link count-indicator dropdown-toggle m-auto"
|
||||||
|
href="#"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
data-action="click->application#loadApps">
|
||||||
|
<i class="mx-0">{{ ux_icon('bi:grid-3x3-gap', {height: '20px', width: '20px'}) }}</i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="dropdown-menu dropdown-menu-right navbar-dropdown preview-list"
|
||||||
|
aria-labelledby="applicationDropdown"
|
||||||
|
style="max-height: 400px; overflow-y: auto; min-width: 320px;">
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center px-3 py-2 border-bottom">
|
||||||
|
<p class="mb-0 font-weight-normal dropdown-header text-bold">Applications</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-3 py-2">
|
||||||
|
<div data-application-target="appList"
|
||||||
|
style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
||||||
|
<span class="text-muted small">Chargement...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
<li class="nav-item dropdown nav-profile">
|
<li class="nav-item dropdown nav-profile">
|
||||||
<a id="profileDropdown" class="nav-link count-indicator dropdown-toggle m-auto" href="#" data-bs-toggle="dropdown">
|
<a id="profileDropdown" class="nav-link count-indicator dropdown-toggle m-auto" href="#" data-bs-toggle="dropdown">
|
||||||
<div id="profil" class="rounded-circle bg-secondary d-flex">
|
<div id="profil" class="rounded-circle bg-secondary d-flex">
|
||||||
|
|
@ -85,25 +114,14 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<div class="dropdown-menu dropdown-menu-right navbar-dropdown px-2" aria-labelledby="profileDropdown" data-bs-popper="static">
|
<div class="dropdown-menu dropdown-menu-right navbar-dropdown px-2"
|
||||||
|
aria-labelledby="profileDropdown"
|
||||||
|
data-bs-popper="static">
|
||||||
|
|
||||||
<a class="dropdown-item border-bottom" style="padding-left: 8px;" href="{{ path('user_show', {'id': app.user.id}) }}">
|
<a class="dropdown-item border-bottom" style="padding-left: 8px;" href="{{ path('user_show', {'id': app.user.id}) }}">
|
||||||
<i class="me-2">{{ ux_icon('bi:gear', {height: '20px', width: '20px'}) }}</i>
|
<i class="me-2">{{ ux_icon('bi:gear', {height: '20px', width: '20px'}) }}</i>
|
||||||
Profil
|
Profil
|
||||||
</a>
|
</a>
|
||||||
<div style="padding:8px 0;" class="row border-bottom">
|
|
||||||
<div class="col-2 m-auto">
|
|
||||||
<i >{{ ux_icon('bi:menu-up', {height: '20px', width: '20px'}) }}</i>
|
|
||||||
</div>
|
|
||||||
<div class="col-9">
|
|
||||||
<a href="http://client.solutions-easy.moi"> Client </a>
|
|
||||||
{# <select class="form-control">
|
|
||||||
<option>Exploit</options>
|
|
||||||
<option>Monithor</options>
|
|
||||||
<option>Check</options>
|
|
||||||
<option>Access</options>
|
|
||||||
</select> #}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a class="dropdown-item" style="padding-left: 8px;" href="{{ path('sso_logout') }}">
|
<a class="dropdown-item" style="padding-left: 8px;" href="{{ path('sso_logout') }}">
|
||||||
<i class="me-2">{{ ux_icon('material-symbols:logout', {height: '20px', width: '20px'}) }}</i>
|
<i class="me-2">{{ ux_icon('material-symbols:logout', {height: '20px', width: '20px'}) }}</i>
|
||||||
Deconnexion
|
Deconnexion
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@
|
||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
<div class="card-header d-flex gap-2">
|
<div class="card-header d-flex gap-2">
|
||||||
{% if app.logoMiniUrl %}
|
{% if app.logoMiniUrl %}
|
||||||
<img src="{{ aws_url ~ app.logoMiniUrl }}" alt="Logo {{ app.name }}"
|
<img src="{{ asset(application.entity.logoMiniUrl) }}" alt="Logo {{ app.name }}"
|
||||||
class="rounded-circle" style="width:50px; height:50px;">
|
class="rounded-circle" style="width:50px; height:50px;">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue