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 = {
|
||||
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 = `<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\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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@
|
|||
{# Description (full) #}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">
|
||||
<h5>Description complète</h5>
|
||||
</div>
|
||||
<div id="toolbar-description" class="border-0">
|
||||
<button class="ql-bold"></button>
|
||||
<button class="ql-italic"></button>
|
||||
|
|
@ -53,6 +56,9 @@
|
|||
{# Description Small #}
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<div class="card-title">
|
||||
<h5>Description courte</h5>
|
||||
</div>
|
||||
<div id="toolbar-descriptionSmall" class="border-0">
|
||||
<button class="ql-bold"></button>
|
||||
<button class="ql-italic"></button>
|
||||
|
|
|
|||
|
|
@ -1,61 +1,42 @@
|
|||
<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">
|
||||
{% if is_granted("ROLE_SUPER_ADMIN") %}
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link" href="#">
|
||||
{# 2. Check if route is 'app_index' #}
|
||||
<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>
|
||||
<span class="menu-title">Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{# <!-- Example of a collapsible menu item -->#}
|
||||
{# <li class="nav-item">#}
|
||||
{# <a class="nav-link" data-bs-toggle="collapse" href="#ui-basic" aria-expanded="false"#}
|
||||
{# 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">
|
||||
|
||||
{# 3. Check if route starts with 'application_' (covers index, edit, show, etc.) #}
|
||||
<li class="nav-item {{ current_route starts with 'application_' ? 'active' : '' }}">
|
||||
<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>
|
||||
<span class="menu-title">Applications</span>
|
||||
</a>
|
||||
</li>
|
||||
{# if user is 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') }}">
|
||||
<i class="icon-grid menu-icon">{{ ux_icon('fa6-regular:circle-user', {height: '15px', width: '15px'}) }}</i>
|
||||
<span class="menu-title">Users</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="nav-item">
|
||||
|
||||
<li class="nav-item {{ current_route starts with 'organization_' ? 'active' : '' }}">
|
||||
{% if is_granted('ROLE_ADMIN') %}
|
||||
<a class="nav-link" href="{{ path('organization_index') }}">
|
||||
<i class="icon-grid menu-icon"> {{ ux_icon('bi:buildings', {height: '15px', width: '15px'}) }}
|
||||
</i>
|
||||
<span class="menu-title">
|
||||
Organizations</span>
|
||||
<i class="icon-grid menu-icon"> {{ ux_icon('bi:buildings', {height: '15px', width: '15px'}) }}</i>
|
||||
<span class="menu-title">Organizations</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
|
|
@ -72,6 +72,35 @@
|
|||
</div>
|
||||
</div>
|
||||
</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">
|
||||
<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">
|
||||
|
|
@ -85,25 +114,14 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
</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}) }}">
|
||||
<i class="me-2">{{ ux_icon('bi:gear', {height: '20px', width: '20px'}) }}</i>
|
||||
Profil
|
||||
</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') }}">
|
||||
<i class="me-2">{{ ux_icon('material-symbols:logout', {height: '20px', width: '20px'}) }}</i>
|
||||
Deconnexion
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@
|
|||
<div class="card h-100">
|
||||
<div class="card-header d-flex gap-2">
|
||||
{% 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;">
|
||||
{% endif %}
|
||||
<div class="card-title">
|
||||
|
|
|
|||
Loading…
Reference in New Issue