```
✨ feat(ansible/caddy): Supprime CSP statique et Permissions-Policy obsolète 🐛 fix(assets/admin): Corrige la gestion du menu admin et des flashs ✨ feat(Twig/ViteAssetExtension): Ajoute CSP nonce et gère les favicons 🐛 fix(Entity/AuditLog): Corrige la relation ManyToOne avec Account ➕ feat: Ajoute NelmioSecurityBundle pour gérer la sécurité CSP ```
This commit is contained in:
@@ -13,10 +13,6 @@ intranet.ludikevent.fr, signature.ludikevent.fr {
|
||||
# --- BLOC HEADER AVEC CSP ---
|
||||
header {
|
||||
X-Robots-Tag "noindex, nofollow, nosnippet, noarchive"
|
||||
|
||||
# CSP sur une seule ligne pour éviter tout problème d'interprétation par Caddy
|
||||
Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://sentry.esy-web.dev https://chat.esy-web.dev https://auth.esy-web.dev https://static.cloudflareinsights.com; connect-src 'self' https://sentry.esy-web.dev https://chat.esy-web.dev https://auth.esy-web.dev https://cloudflareinsights.com; frame-src 'self' https://chat.esy-web.dev https://auth.esy-web.dev; style-src 'self' 'unsafe-inline' https://chat.esy-web.dev; img-src 'self' data: https://chat.esy-web.dev; font-src 'self' data:; frame-ancestors 'none';"
|
||||
|
||||
Permissions-Policy "accelerometer=(), autoplay=(), camera=(), clipboard-write=(), encrypted-media=(), fullscreen=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), usb=(), vr=(), screen-wake-lock=(), xr-spatial-tracking=(), bluetooth=(), ambient-light-sensor=(), battery=(), gamepad=(), notifications=(), push=()"
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "DENY"
|
||||
|
||||
113
assets/admin.js
113
assets/admin.js
@@ -1,76 +1,121 @@
|
||||
import './admin.scss'
|
||||
import * as Turbo from "@hotwired/turbo"
|
||||
|
||||
// Cette fonction initialise tous les écouteurs d'événements
|
||||
/**
|
||||
* Initialise les composants de l'interface d'administration.
|
||||
* Cette fonction est appelée à chaque chargement de page par Turbo.
|
||||
*/
|
||||
function initAdminLayout() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const overlay = document.getElementById('sidebar-overlay');
|
||||
const toggleBtn = document.getElementById('sidebar-toggle');
|
||||
|
||||
const settingsToggle = document.getElementById('settings-toggle');
|
||||
const settingsSubmenu = document.getElementById('settings-submenu');
|
||||
const settingsChevron = document.getElementById('settings-chevron');
|
||||
|
||||
// --- TOGGLE SIDEBAR MOBILE ---
|
||||
// --- 1. GESTION DE LA SIDEBAR (MOBILE) ---
|
||||
if (toggleBtn && sidebar && overlay) {
|
||||
// On clone pour éviter de doubler les events avec Turbo
|
||||
toggleBtn.replaceWith(toggleBtn.cloneNode(true));
|
||||
const newToggleBtn = document.getElementById('sidebar-toggle');
|
||||
|
||||
newToggleBtn.addEventListener('click', () => {
|
||||
toggleBtn.onclick = () => {
|
||||
sidebar.classList.toggle('-translate-x-full');
|
||||
overlay.classList.toggle('hidden');
|
||||
});
|
||||
};
|
||||
|
||||
overlay.addEventListener('click', () => {
|
||||
overlay.onclick = () => {
|
||||
sidebar.classList.add('-translate-x-full');
|
||||
overlay.classList.add('hidden');
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// --- GESTION SOUS-MENU PARAMÈTRES ---
|
||||
if (settingsToggle) {
|
||||
settingsToggle.replaceWith(settingsToggle.cloneNode(true));
|
||||
const newSettingsToggle = document.getElementById('settings-toggle');
|
||||
// --- 2. GESTION DU DROPDOWN (PARAMÈTRES) ---
|
||||
if (settingsToggle && settingsSubmenu) {
|
||||
const settingsChevron = settingsToggle.querySelector('svg:last-child');
|
||||
|
||||
newSettingsToggle.addEventListener('click', (e) => {
|
||||
/**
|
||||
* Alterne l'état du dropdown avec une animation de glissement.
|
||||
* @param {boolean} show - Forcer l'ouverture ou la fermeture
|
||||
* @param {boolean} animate - Activer ou non la transition CSS
|
||||
*/
|
||||
const toggleDropdown = (show, animate = true) => {
|
||||
if (!animate) settingsSubmenu.style.transition = 'none';
|
||||
|
||||
if (show) {
|
||||
settingsSubmenu.classList.remove('hidden');
|
||||
// scrollHeight permet de calculer la hauteur réelle du contenu
|
||||
settingsSubmenu.style.maxHeight = settingsSubmenu.scrollHeight + "px";
|
||||
settingsChevron?.classList.add('rotate-180');
|
||||
localStorage.setItem('admin_settings_open', 'true');
|
||||
} else {
|
||||
settingsSubmenu.style.maxHeight = "0px";
|
||||
settingsChevron?.classList.remove('rotate-180');
|
||||
localStorage.setItem('admin_settings_open', 'false');
|
||||
|
||||
// On cache l'élément après l'animation pour l'accessibilité
|
||||
if (animate) {
|
||||
setTimeout(() => {
|
||||
if (settingsSubmenu.style.maxHeight === "0px") {
|
||||
settingsSubmenu.classList.add('hidden');
|
||||
}
|
||||
}, 300);
|
||||
} else {
|
||||
settingsSubmenu.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
if (!animate) {
|
||||
// Forcer un recalcul pour réactiver la transition proprement
|
||||
settingsSubmenu.offsetHeight;
|
||||
settingsSubmenu.style.transition = '';
|
||||
}
|
||||
};
|
||||
|
||||
// Événement de clic
|
||||
settingsToggle.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
if (settingsSubmenu) settingsSubmenu.classList.toggle('hidden');
|
||||
if (settingsChevron) settingsChevron.classList.toggle('rotate-180');
|
||||
});
|
||||
const isClosed = settingsSubmenu.style.maxHeight === "0px" || settingsSubmenu.classList.contains('hidden');
|
||||
toggleDropdown(isClosed);
|
||||
};
|
||||
|
||||
// Persistance : Garder ouvert si on est dans une sous-route admin
|
||||
if (window.location.pathname.includes('administrateur')) {
|
||||
if (settingsSubmenu) settingsSubmenu.classList.remove('hidden');
|
||||
if (settingsChevron) settingsChevron.classList.add('rotate-180');
|
||||
// --- PERSISTANCE ---
|
||||
// On vérifie si on est sur une page appartenant au menu ou si l'utilisateur l'avait laissé ouvert
|
||||
const isSettingsRoute = window.location.pathname.includes('/crm/administrateur') ||
|
||||
window.location.pathname.includes('/crm/logs');
|
||||
const wasOpen = localStorage.getItem('admin_settings_open') === 'true';
|
||||
|
||||
if (isSettingsRoute || wasOpen) {
|
||||
toggleDropdown(true, false); // Ouverture immédiate sans animation
|
||||
}
|
||||
|
||||
// --- HIGHLIGHT DU LIEN ACTIF ---
|
||||
settingsSubmenu.querySelectorAll('a').forEach(link => {
|
||||
if (window.location.pathname === link.getAttribute('href')) {
|
||||
link.classList.add('text-blue-600', 'dark:text-blue-400', 'font-semibold');
|
||||
link.classList.remove('text-slate-500');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --- GESTION DES MESSAGES FLASH (Auto-suppression 10s) ---
|
||||
const flashes = document.querySelectorAll('.flash-message');
|
||||
flashes.forEach((flash) => {
|
||||
// Supprime le message après 10 secondes
|
||||
// --- 3. GESTION DES MESSAGES FLASH (Auto-suppression) ---
|
||||
document.querySelectorAll('.flash-message').forEach((flash) => {
|
||||
setTimeout(() => {
|
||||
// Animation de sortie
|
||||
flash.classList.add('opacity-0', 'translate-x-10');
|
||||
// Retrait du DOM après l'animation
|
||||
setTimeout(() => flash.remove(), 500);
|
||||
}, 10000);
|
||||
}, 8000);
|
||||
});
|
||||
}
|
||||
|
||||
// --- CORRECTIF DATA-TURBO-CONFIRM ---
|
||||
// Force l'affichage de la confirmation native sur les liens avec data-turbo-confirm
|
||||
// Turbo 7+ intercepte les clics, on réimplémente une confirmation native simple
|
||||
document.addEventListener("turbo:click", (event) => {
|
||||
const message = event.target.getAttribute("data-turbo-confirm");
|
||||
const message = event.target.closest("[data-turbo-confirm]")?.getAttribute("data-turbo-confirm");
|
||||
if (message && !confirm(message)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// S'exécute au premier chargement ET à chaque navigation Turbo
|
||||
// Exécution au chargement initial et à chaque navigation Turbo
|
||||
document.addEventListener('turbo:load', initAdminLayout);
|
||||
|
||||
// Fermer la sidebar mobile avant que Turbo ne mette en cache la page
|
||||
// Nettoyage avant la mise en cache de Turbo (évite les bugs visuels au retour arrière)
|
||||
document.addEventListener('turbo:before-cache', () => {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const overlay = document.getElementById('sidebar-overlay');
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"mittwald/vault-php": "^3.0.2",
|
||||
"mobiledetect/mobiledetectlib": "^4.8.10",
|
||||
"nelmio/cors-bundle": "^2.6.1",
|
||||
"nelmio/security-bundle": "^3.8",
|
||||
"ovh/ovh": ">=3.5",
|
||||
"pear/net_dns2": ">=2.0.7",
|
||||
"phpdocumentor/reflection-docblock": "^5.6.6",
|
||||
|
||||
211
composer.lock
generated
211
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "45482c705146a5e69d39c6e43bf018b1",
|
||||
"content-hash": "4ce617f198e010903ec5351925259b10",
|
||||
"packages": [
|
||||
{
|
||||
"name": "async-aws/core",
|
||||
@@ -699,6 +699,78 @@
|
||||
},
|
||||
"time": "2025-11-27T18:57:36+00:00"
|
||||
},
|
||||
{
|
||||
"name": "composer/ca-bundle",
|
||||
"version": "1.5.10",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/composer/ca-bundle.git",
|
||||
"reference": "961a5e4056dd2e4a2eedcac7576075947c28bf63"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/composer/ca-bundle/zipball/961a5e4056dd2e4a2eedcac7576075947c28bf63",
|
||||
"reference": "961a5e4056dd2e4a2eedcac7576075947c28bf63",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-openssl": "*",
|
||||
"ext-pcre": "*",
|
||||
"php": "^7.2 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^1.10",
|
||||
"phpunit/phpunit": "^8 || ^9",
|
||||
"psr/log": "^1.0 || ^2.0 || ^3.0",
|
||||
"symfony/process": "^4.0 || ^5.0 || ^6.0 || ^7.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Composer\\CaBundle\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jordi Boggiano",
|
||||
"email": "j.boggiano@seld.be",
|
||||
"homepage": "http://seld.be"
|
||||
}
|
||||
],
|
||||
"description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.",
|
||||
"keywords": [
|
||||
"cabundle",
|
||||
"cacert",
|
||||
"certificate",
|
||||
"ssl",
|
||||
"tls"
|
||||
],
|
||||
"support": {
|
||||
"irc": "irc://irc.freenode.org/composer",
|
||||
"issues": "https://github.com/composer/ca-bundle/issues",
|
||||
"source": "https://github.com/composer/ca-bundle/tree/1.5.10"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://packagist.com",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/composer",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-12-08T15:06:51+00:00"
|
||||
},
|
||||
{
|
||||
"name": "composer/pcre",
|
||||
"version": "3.3.2",
|
||||
@@ -6163,6 +6235,80 @@
|
||||
},
|
||||
"time": "2026-01-12T15:59:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "nelmio/security-bundle",
|
||||
"version": "v3.8.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/nelmio/NelmioSecurityBundle.git",
|
||||
"reference": "2fafee1cdda1d5952554c44eef4c3c8566d56f40"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/nelmio/NelmioSecurityBundle/zipball/2fafee1cdda1d5952554c44eef4c3c8566d56f40",
|
||||
"reference": "2fafee1cdda1d5952554c44eef4c3c8566d56f40",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.4 || ^8.0",
|
||||
"symfony/deprecation-contracts": "^2.5 || ^3",
|
||||
"symfony/framework-bundle": "^5.4 || ^6.3 || ^7.0 || ^8.0",
|
||||
"symfony/http-kernel": "^5.4 || ^6.3 || ^7.0 || ^8.0",
|
||||
"symfony/security-core": "^5.4 || ^6.3 || ^7.0 || ^8.0",
|
||||
"symfony/security-csrf": "^5.4 || ^6.3 || ^7.0 || ^8.0",
|
||||
"symfony/security-http": "^5.4 || ^6.3 || ^7.0 || ^8.0",
|
||||
"symfony/yaml": "^5.4 || ^6.3 || ^7.0 || ^8.0",
|
||||
"ua-parser/uap-php": "^3.4.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^2.0",
|
||||
"phpstan/phpstan-deprecation-rules": "^2.0",
|
||||
"phpstan/phpstan-phpunit": "^2.0",
|
||||
"phpstan/phpstan-strict-rules": "^2.0",
|
||||
"phpstan/phpstan-symfony": "^2.0",
|
||||
"phpunit/phpunit": "^9.5 || ^10.1 || ^11.0",
|
||||
"psr/cache": "^1.0 || ^2.0 || ^3.0",
|
||||
"symfony/browser-kit": "^5.4 || ^6.3 || ^7.0 || ^8.0",
|
||||
"symfony/cache": "^5.4 || ^6.3 || ^7.0 || ^8.0",
|
||||
"symfony/phpunit-bridge": "^6.3 || ^7.0 || ^8.0",
|
||||
"symfony/twig-bundle": "^5.4 || ^6.3 || ^7.0 || ^8.0",
|
||||
"twig/twig": "^2.10 || ^3.0"
|
||||
},
|
||||
"type": "symfony-bundle",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "3.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Nelmio\\SecurityBundle\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nelmio",
|
||||
"homepage": "http://nelm.io"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://github.com/nelmio/NelmioSecurityBundle/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Extra security-related features for Symfony: signed/encrypted cookies, HTTPS/SSL/HSTS handling, cookie session storage, ...",
|
||||
"keywords": [
|
||||
"security"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/nelmio/NelmioSecurityBundle/issues",
|
||||
"source": "https://github.com/nelmio/NelmioSecurityBundle/tree/v3.8.0"
|
||||
},
|
||||
"time": "2026-01-14T19:38:55+00:00"
|
||||
},
|
||||
{
|
||||
"name": "nesbot/carbon",
|
||||
"version": "3.11.0",
|
||||
@@ -14995,6 +15141,69 @@
|
||||
],
|
||||
"time": "2025-12-14T11:28:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "ua-parser/uap-php",
|
||||
"version": "v3.10.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ua-parser/uap-php.git",
|
||||
"reference": "f44bdd1b38198801cf60b0681d2d842980e47af5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/ua-parser/uap-php/zipball/f44bdd1b38198801cf60b0681d2d842980e47af5",
|
||||
"reference": "f44bdd1b38198801cf60b0681d2d842980e47af5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"composer/ca-bundle": "^1.1",
|
||||
"php": "^7.2 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^0.12.33",
|
||||
"phpunit/phpunit": "^8 || ^9",
|
||||
"symfony/console": "^3.4 || ^4.2 || ^4.3 || ^5.0",
|
||||
"symfony/filesystem": "^3.4 || ^4.2 || ^4.3 || ^5.0",
|
||||
"symfony/finder": "^3.4 || ^4.2 || ^4.3 || ^5.0",
|
||||
"symfony/yaml": "^3.4 || ^4.2 || ^4.3 || ^5.0",
|
||||
"vimeo/psalm": "^3.12"
|
||||
},
|
||||
"suggest": {
|
||||
"symfony/console": "Required for CLI usage - ^3.4 || ^4.3 || ^5.0",
|
||||
"symfony/filesystem": "Required for CLI usage - ^3.4 || ^4.3 || ^5.0",
|
||||
"symfony/finder": "Required for CLI usage - ^3.4 || ^4.3 || ^5.0",
|
||||
"symfony/yaml": "Required for CLI usage - ^3.4 || ^4.3 || ^5.0"
|
||||
},
|
||||
"bin": [
|
||||
"bin/uaparser"
|
||||
],
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"UAParser\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Dave Olsen",
|
||||
"email": "dmolsen@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Lars Strojny",
|
||||
"email": "lars@strojny.net"
|
||||
}
|
||||
],
|
||||
"description": "A multi-language port of Browserscope's user agent parser.",
|
||||
"support": {
|
||||
"issues": "https://github.com/ua-parser/uap-php/issues",
|
||||
"source": "https://github.com/ua-parser/uap-php/tree/v3.10.0"
|
||||
},
|
||||
"time": "2025-07-17T15:43:24+00:00"
|
||||
},
|
||||
{
|
||||
"name": "vich/uploader-bundle",
|
||||
"version": "v2.9.1",
|
||||
|
||||
@@ -20,4 +20,5 @@ return [
|
||||
Vich\UploaderBundle\VichUploaderBundle::class => ['all' => true],
|
||||
KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle::class => ['all' => true],
|
||||
Scheb\TwoFactorBundle\SchebTwoFactorBundle::class => ['all' => true],
|
||||
Nelmio\SecurityBundle\NelmioSecurityBundle::class => ['all' => true],
|
||||
];
|
||||
|
||||
12
config/packages/dev/nelmio_security.yaml
Normal file
12
config/packages/dev/nelmio_security.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
# config/packages/dev/nelmio_security.yaml
|
||||
nelmio_security:
|
||||
csp:
|
||||
enforce:
|
||||
connect-src:
|
||||
- "'self'"
|
||||
- "ws://localhost:5173" # Autorise le WebSocket de Vite
|
||||
- "http://localhost:5173" # Autorise les assets de Vite
|
||||
- "https://sentry.esy-web.dev"
|
||||
- "https://chat.esy-web.dev"
|
||||
- "https://auth.esy-web.dev"
|
||||
- "https://cloudflareinsights.com"
|
||||
37
config/packages/nelmio_security.yaml
Normal file
37
config/packages/nelmio_security.yaml
Normal file
@@ -0,0 +1,37 @@
|
||||
nelmio_security:
|
||||
# Content Security Policy (CSP)
|
||||
csp:
|
||||
enforce:
|
||||
default-src: ["'self'"]
|
||||
script-src:
|
||||
- "'self'"
|
||||
- "nonce"
|
||||
- "https://sentry.esy-web.dev"
|
||||
- "https://chat.esy-web.dev"
|
||||
- "https://auth.esy-web.dev"
|
||||
- "https://static.cloudflareinsights.com"
|
||||
- "'strict-dynamic'"
|
||||
connect-src:
|
||||
- "'self'"
|
||||
- "https://sentry.esy-web.dev"
|
||||
- "https://chat.esy-web.dev"
|
||||
- "https://auth.esy-web.dev"
|
||||
- "https://cloudflareinsights.com"
|
||||
frame-src:
|
||||
- "'self'"
|
||||
- "https://chat.esy-web.dev"
|
||||
- "https://auth.esy-web.dev"
|
||||
style-src:
|
||||
- "'self'"
|
||||
- "'unsafe-inline'"
|
||||
- "https://chat.esy-web.dev"
|
||||
img-src:
|
||||
- "'self'"
|
||||
- "data:"
|
||||
- "https://chat.esy-web.dev"
|
||||
font-src:
|
||||
- "'self'"
|
||||
- "data:"
|
||||
frame-ancestors: ["'none'"]
|
||||
# Optionnel : forcer le passage en HTTPS
|
||||
upgrade-insecure-requests: false
|
||||
@@ -16,7 +16,10 @@ services:
|
||||
App\:
|
||||
resource: '../src/'
|
||||
|
||||
|
||||
App\Twig\ViteAssetExtension:
|
||||
arguments:
|
||||
$manifest: '%kernel.project_dir%/public/build/.vite/manifest.json'
|
||||
$cache: '@vite_cache_pool'
|
||||
# Utilisation du listener de Nelmio (identifiant officiel)
|
||||
$cspListener: '@nelmio_security.csp_listener'
|
||||
|
||||
@@ -14,7 +14,7 @@ class AuditLog
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\ManyToOne(targetEntity: Account::class, inversedBy: 'auditLogs')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?Account $account = null;
|
||||
|
||||
|
||||
@@ -6,24 +6,20 @@ use Detection\MobileDetect;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFunction;
|
||||
use Nelmio\SecurityBundle\EventListener\ContentSecurityPolicyListener;
|
||||
|
||||
class ViteAssetExtension extends AbstractExtension
|
||||
{
|
||||
// Clé réservée dans le manifest Vite pour le HTML généré des favicons.
|
||||
const FAVICON_MANIFEST_KEY = '_FAVICONS_HTML_';
|
||||
|
||||
private ?array $manifestData = null;
|
||||
|
||||
const CACHE_KEY = 'vite_manifest';
|
||||
|
||||
private ?array $manifestData = null;
|
||||
private readonly bool $isDev;
|
||||
|
||||
public function __construct(
|
||||
private readonly string $manifest,
|
||||
private readonly CacheItemPoolInterface $cache,
|
||||
private readonly ContentSecurityPolicyListener $cspListener,
|
||||
) {
|
||||
// Respecte la logique existante : VITE_LOAD == "0" est considéré comme DEV.
|
||||
$this->isDev = $_ENV['VITE_LOAD'] == "0";
|
||||
$this->isDev = $_ENV['VITE_LOAD'] === "0";
|
||||
}
|
||||
|
||||
public function getFunctions(): array
|
||||
@@ -31,20 +27,25 @@ class ViteAssetExtension extends AbstractExtension
|
||||
return [
|
||||
new TwigFunction('vite_asset', $this->asset(...), ['is_safe' => ['html']]),
|
||||
new TwigFunction('isMobile', $this->isMobile(...), ['is_safe' => ['html']]),
|
||||
// Nouvelle fonction Twig pour inclure les liens de favicons
|
||||
new TwigFunction('vite_favicons', $this->favicons(...), ['is_safe' => ['html']])
|
||||
];
|
||||
}
|
||||
|
||||
public function isMobile()
|
||||
/**
|
||||
* Récupère le nonce pour les scripts via le Listener de Nelmio
|
||||
*/
|
||||
private function getNonce(): string
|
||||
{
|
||||
// Dans la v3.8, on utilise getNonce('script') sur le listener
|
||||
return $this->cspListener->getNonce('script');
|
||||
}
|
||||
|
||||
public function isMobile(): bool
|
||||
{
|
||||
$detect = new MobileDetect();
|
||||
return $detect->isMobile() || $detect->isTablet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge le manifeste s'il n'est pas déjà chargé et met en cache.
|
||||
*/
|
||||
private function loadManifest(): void
|
||||
{
|
||||
if ($this->manifestData === null) {
|
||||
@@ -53,106 +54,61 @@ class ViteAssetExtension extends AbstractExtension
|
||||
$this->manifestData = $item->get();
|
||||
} else {
|
||||
if (!file_exists($this->manifest)) {
|
||||
// En cas d'erreur de fichier, initialise à un tableau vide
|
||||
$this->manifestData = [];
|
||||
return;
|
||||
}
|
||||
|
||||
$this->manifestData = json_decode((string)file_get_contents($this->manifest), true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
$this->manifestData = [];
|
||||
}
|
||||
|
||||
$item->set($this->manifestData);
|
||||
$this->cache->save($item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Gestion des assets JS/CSS (non modifiée) ---
|
||||
|
||||
public function asset(string $entry, array $deps): string
|
||||
public function asset(string $entry, array $deps = []): string
|
||||
{
|
||||
if ($this->isDev) {
|
||||
return $this->assetDev($entry, $deps);
|
||||
}
|
||||
|
||||
return $this->assetProd($entry);
|
||||
return $this->isDev ? $this->assetDev($entry, $deps) : $this->assetProd($entry);
|
||||
}
|
||||
|
||||
public function assetDev(string $entry, array $deps): string
|
||||
{
|
||||
$html = <<<HTML
|
||||
<script type="module" src="http://localhost:5173/assets/@vite/client"></script>
|
||||
HTML;
|
||||
return $html . <<<HTML
|
||||
<script type="module" src="http://localhost:5173/assets/{$entry}" defer></script>
|
||||
HTML;
|
||||
$nonce = $this->getNonce();
|
||||
return <<<HTML
|
||||
<script type="module" src="http://localhost:5173/assets/@vite/client" nonce="{$nonce}"></script>
|
||||
<script type="module" src="http://localhost:5173/assets/{$entry}" nonce="{$nonce}" defer></script>
|
||||
HTML;
|
||||
}
|
||||
|
||||
public function assetProd(string $entry): string
|
||||
{
|
||||
$this->loadManifest();
|
||||
$nonce = $this->getNonce();
|
||||
|
||||
$file = $this->manifestData[$entry]['file'] ?? '';
|
||||
$css = $this->manifestData[$entry]['css'] ?? [];
|
||||
$imports = $this->manifestData[$entry]['imports'] ?? [];
|
||||
|
||||
$html = <<<HTML
|
||||
<script type="module" src="/build/{$file}" crossorigin="anonymous" defer></script>
|
||||
HTML;
|
||||
<script type="module" src="/build/{$file}" crossorigin="anonymous" nonce="{$nonce}" defer></script>
|
||||
HTML;
|
||||
|
||||
foreach ($css as $cssFile) {
|
||||
$html .= <<<HTML
|
||||
<link rel="stylesheet" rel="preload" media="screen" href="/build/{$cssFile}" crossorigin="anonymous"/>
|
||||
HTML;
|
||||
}
|
||||
|
||||
foreach ($imports as $import) {
|
||||
$import = str_replace("_vendor","vendor",$import);
|
||||
$import = str_replace("_turbo","turbo",$import);
|
||||
$html .= <<<HTML
|
||||
<link rel="modulepreload" href="/build/{$import}" crossorigin="anonymous"/>
|
||||
HTML;
|
||||
$html .= '<link rel="stylesheet" href="/build/'.$cssFile.'" crossorigin="anonymous"/>';
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
// --- Nouvelle Gestion des Favicons ---
|
||||
|
||||
public function favicons(): string
|
||||
{
|
||||
if ($this->isDev) {
|
||||
return $this->faviconsDev();
|
||||
}
|
||||
|
||||
return $this->faviconsProd();
|
||||
return $this->isDev ? '<link rel="icon" href="/favicon.ico">' : $this->faviconsProd();
|
||||
}
|
||||
|
||||
public function faviconsDev(): string
|
||||
{
|
||||
// En mode dev, on assume qu'un fichier favicon.ico ou favicon.png
|
||||
// standard est présent dans le répertoire public.
|
||||
return <<<HTML
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||
HTML;
|
||||
}
|
||||
|
||||
public function faviconsProd(): string
|
||||
private function faviconsProd(): string
|
||||
{
|
||||
$this->loadManifest();
|
||||
|
||||
// Récupère le bloc HTML complet généré par le plugin dans le manifest.
|
||||
// On suppose que l'entrée est un tableau associatif avec la clé 'html'.
|
||||
$faviconData = $this->manifestData;
|
||||
$faviconHtml = "";
|
||||
foreach ($faviconData as $key =>$favicon) {
|
||||
if(!str_contains($key,".js")) {
|
||||
$faviconHtml .= <<<HTML
|
||||
<link rel="icon" href="/build/{$favicon['file']}" type="image/x-icon">
|
||||
HTML;
|
||||
foreach ($this->manifestData as $key => $favicon) {
|
||||
if(!str_contains($key, ".js") && isset($favicon['file'])) {
|
||||
$faviconHtml .= '<link rel="icon" href="/build/'.$favicon['file'].'" type="image/x-icon">';
|
||||
}
|
||||
}
|
||||
return $faviconHtml;
|
||||
|
||||
12
symfony.lock
12
symfony.lock
@@ -100,6 +100,18 @@
|
||||
"config/packages/nelmio_cors.yaml"
|
||||
]
|
||||
},
|
||||
"nelmio/security-bundle": {
|
||||
"version": "3.8",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "2.4",
|
||||
"ref": "71045833e4f882ad9de8c95fe47efb99a1eec2f7"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/nelmio_security.yaml"
|
||||
]
|
||||
},
|
||||
"phpstan/phpstan": {
|
||||
"version": "2.1",
|
||||
"recipe": {
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
<svg class="w-4 h-4 transition-transform duration-300 {{ isOpen ? 'rotate-180' }}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
|
||||
</button>
|
||||
|
||||
<div id="settings-submenu" class="mt-2 space-y-1 overflow-hidden transition-all duration-300 {{ isOpen ? 'max-h-40' : 'max-h-0' }}">
|
||||
<div id="settings-submenu" class="mt-2 space-y-1 overflow-hidden transition-all duration-300 max-h-0 hidden">
|
||||
<a href="{{ path('app_crm_administrateur') }}" class="block px-12 py-2 text-sm {{ isAdminActive ? 'text-blue-600 font-bold' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}">Gestion Admins</a>
|
||||
<a href="{{ path('app_crm_audit_logs') }}" class="block px-12 py-2 text-sm {{ isLogsActive ? 'text-blue-600 font-bold' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}">Traçabilité (Logs)</a>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user