Merge branch 'docs' into 'develop'

Docs

See merge request easy-solutions/apps/easyportal!25
This commit is contained in:
Charles-Edouard MARGUERITE 2026-02-02 15:08:00 +00:00
commit 8317617288
8 changed files with 502 additions and 57 deletions

View File

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

327
docs/Client_Setup.md Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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