```
✨ feat(caddy): Améliore la sécurité avec CSP et headers standards ✨ feat(templates): Met à jour le logo sur la page d'inscription réussie ✨ feat(knp_paginator): Ajoute la configuration pour le style Tailwind ✨ feat(audit_logs): Crée la page de traçabilité des actions ✨ feat(logs): Ajoute le contrôleur pour gérer les logs d'audit ✨ feat(AppLogger): Enregistre l'user agent dans les logs d'audit ✨ feat(AccountController): Supprime l'appel inutile de l'EventAdminCreate ✨ feat(AuditLogRepository): Récupère les logs en excluant les ROOT ✨ feat(base): Ajoute la structure de base pour le dashboard ```
This commit is contained in:
@@ -10,17 +10,29 @@ intranet.ludikevent.fr, signature.ludikevent.fr {
|
|||||||
max_size 100MB
|
max_size 100MB
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
# This prevents search engines from indexing the site
|
# Empêche l'indexation
|
||||||
X-Robots-Tag "noindex, nofollow, nosnippet, noarchive"
|
X-Robots-Tag "noindex, nofollow, nosnippet, noarchive"
|
||||||
|
|
||||||
# Your existing Permissions-Policy
|
# Content Security Policy (CSP) Finale
|
||||||
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=()"
|
# auth.esy-web.dev ajouté dans :
|
||||||
|
# - script-src (si chargement de SDK auth)
|
||||||
|
# - connect-src (pour les requêtes d'authentification / token)
|
||||||
|
# - frame-src (si affichage d'une fenêtre de login en iframe)
|
||||||
|
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; \
|
||||||
|
connect-src 'self' https://sentry.esy-web.dev https://chat.esy-web.dev https://auth.esy-web.dev; \
|
||||||
|
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';"
|
||||||
|
|
||||||
# Recommended security headers for an intranet
|
# Headers de sécurité standards
|
||||||
X-Content-Type-Options "nosniff"
|
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-Frame-Options "DENY"
|
X-Content-Type-Options "nosniff"
|
||||||
Referrer-Policy "strict-origin-when-cross-origin"
|
X-Frame-Options "DENY"
|
||||||
|
Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
}
|
}
|
||||||
|
|
||||||
php_fastcgi unix//run/php/php8.3-fpm.sock {
|
php_fastcgi unix//run/php/php8.3-fpm.sock {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
"healey/robots": "^1.0.1",
|
"healey/robots": "^1.0.1",
|
||||||
"imagine/imagine": "^1.5.2",
|
"imagine/imagine": "^1.5.2",
|
||||||
"io-developer/php-whois": ">=4.1.10",
|
"io-developer/php-whois": ">=4.1.10",
|
||||||
"knplabs/knp-paginator-bundle": "^6.10.0",
|
"knplabs/knp-paginator-bundle": "^6.10",
|
||||||
"knpuniversity/oauth2-client-bundle": "^2.20",
|
"knpuniversity/oauth2-client-bundle": "^2.20",
|
||||||
"lasserafn/php-initial-avatar-generator": "^4.5",
|
"lasserafn/php-initial-avatar-generator": "^4.5",
|
||||||
"league/flysystem-aws-s3-v3": "^3.30.1",
|
"league/flysystem-aws-s3-v3": "^3.30.1",
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
"ovh/ovh": ">=3.5",
|
"ovh/ovh": ">=3.5",
|
||||||
"pear/net_dns2": ">=2.0.7",
|
"pear/net_dns2": ">=2.0.7",
|
||||||
"phpdocumentor/reflection-docblock": "^5.6.6",
|
"phpdocumentor/reflection-docblock": "^5.6.6",
|
||||||
"phpoffice/phpspreadsheet": ">=5.4",
|
"phpoffice/phpspreadsheet": "^5.4",
|
||||||
"phpstan/phpdoc-parser": "^2.3.1",
|
"phpstan/phpdoc-parser": "^2.3.1",
|
||||||
"presta/sitemap-bundle": "^4.2",
|
"presta/sitemap-bundle": "^4.2",
|
||||||
"scheb/2fa-bundle": "^7.13",
|
"scheb/2fa-bundle": "^7.13",
|
||||||
|
|||||||
2
composer.lock
generated
2
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "4bb28dc935f256091e8b6e96cd1fbfa3",
|
"content-hash": "45482c705146a5e69d39c6e43bf018b1",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "async-aws/core",
|
"name": "async-aws/core",
|
||||||
|
|||||||
3
config/packages/knp_paginator.yaml
Normal file
3
config/packages/knp_paginator.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
knp_paginator:
|
||||||
|
template:
|
||||||
|
pagination: '@KnpPaginator/Pagination/tailwindcss_pagination.html.twig'
|
||||||
38
migrations/Version20260115190139.php
Normal file
38
migrations/Version20260115190139.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260115190139 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE audit_log ADD user_agent VARCHAR(255) NOT NULL');
|
||||||
|
$this->addSql('ALTER TABLE audit_log ALTER account_id SET NOT NULL');
|
||||||
|
$this->addSql('ALTER TABLE audit_log ALTER type TYPE VARCHAR(50)');
|
||||||
|
$this->addSql('ALTER TABLE audit_log ALTER message TYPE TEXT');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
$this->addSql('ALTER TABLE audit_log DROP user_agent');
|
||||||
|
$this->addSql('ALTER TABLE audit_log ALTER account_id DROP NOT NULL');
|
||||||
|
$this->addSql('ALTER TABLE audit_log ALTER type TYPE VARCHAR(255)');
|
||||||
|
$this->addSql('ALTER TABLE audit_log ALTER message TYPE VARCHAR(255)');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,7 +27,6 @@ class AccountController extends AbstractController
|
|||||||
{
|
{
|
||||||
// Audit Log : On trace la consultation de la liste
|
// Audit Log : On trace la consultation de la liste
|
||||||
$appLogger->record('VIEW', 'Consultation de la liste des administrateurs');
|
$appLogger->record('VIEW', 'Consultation de la liste des administrateurs');
|
||||||
$eventDispatcher->dispatch(new EventAdminCreate($accountRepository->findAdmin()[0], $this->getUser()));
|
|
||||||
return $this->render('dashboard/administrateur.twig', [
|
return $this->render('dashboard/administrateur.twig', [
|
||||||
'admins' => $accountRepository->findAdmin(),
|
'admins' => $accountRepository->findAdmin(),
|
||||||
]);
|
]);
|
||||||
|
|||||||
76
src/Controller/Dashboard/LogsController.php
Normal file
76
src/Controller/Dashboard/LogsController.php
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller\Dashboard;
|
||||||
|
|
||||||
|
use App\Entity\AuditLog;
|
||||||
|
use App\Repository\AuditLogRepository;
|
||||||
|
use Knp\Component\Pager\PaginatorInterface;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
class LogsController extends AbstractController
|
||||||
|
{
|
||||||
|
|
||||||
|
#[Route(path: '/crm/logs', name: 'app_crm_audit_logs', methods: ['GET'])]
|
||||||
|
public function crmLogs(
|
||||||
|
AuditLogRepository $auditLogRepository,
|
||||||
|
PaginatorInterface $paginator,
|
||||||
|
Request $request
|
||||||
|
): Response {
|
||||||
|
$query = $auditLogRepository->getQueryForUser($this->getUser());
|
||||||
|
if ($request->query->get('extract')) {
|
||||||
|
return $this->generateExcelExport($query->getResult());
|
||||||
|
}
|
||||||
|
$pagination = $paginator->paginate(
|
||||||
|
$query, /* la requête */
|
||||||
|
$request->query->getInt('page', 1), /* numéro de page */
|
||||||
|
25 /* nombre d'éléments par page */
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->render('dashboard/audit_logs.twig', [
|
||||||
|
'logs' => $pagination // On passe l'objet pagination à la place de l'array
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateExcelExport(array $logs): StreamedResponse
|
||||||
|
{
|
||||||
|
$spreadsheet = new Spreadsheet();
|
||||||
|
$sheet = $spreadsheet->getActiveSheet();
|
||||||
|
$sheet->setTitle('Logs Intranet Ludikevent');
|
||||||
|
|
||||||
|
// En-têtes stylisés
|
||||||
|
$headers = ['Date', 'Heure', 'Admin', 'Email', 'Action', 'Message', 'URL', 'Navigateur/OS'];
|
||||||
|
foreach (range('A', 'G') as $i => $column) {
|
||||||
|
$sheet->setCellValue($column . '1', $headers[$i]);
|
||||||
|
$sheet->getStyle($column . '1')->getFont()->setBold(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$row = 2;
|
||||||
|
/** @var AuditLog $log */
|
||||||
|
foreach ($logs as $log) {
|
||||||
|
$sheet->setCellValue('A' . $row, $log->getActionAt()->format('d/m/Y'));
|
||||||
|
$sheet->setCellValue('B' . $row, $log->getActionAt()->format('H:i:s'));
|
||||||
|
$sheet->setCellValue('C' . $row, $log->getAccount()->getFirstName() . ' ' . $log->getAccount()->getName());
|
||||||
|
$sheet->setCellValue('D' . $row, $log->getAccount()->getEmail());
|
||||||
|
$sheet->setCellValue('E' . $row, $log->getType());
|
||||||
|
$sheet->setCellValue('F' . $row, $log->getMessage());
|
||||||
|
$sheet->setCellValue('G' . $row, $log->getPath());
|
||||||
|
$sheet->setCellValue('H' . $row, $log->getUserAgent());
|
||||||
|
$row++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$writer = new Xlsx($spreadsheet);
|
||||||
|
$response = new StreamedResponse(fn() => $writer->save('php://output'));
|
||||||
|
|
||||||
|
$fileName = 'export_logs_' . date('d_m_Y') . '.xlsx';
|
||||||
|
$response->headers->set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||||
|
$response->headers->set('Content-Disposition', 'attachment;filename="' . $fileName . '"');
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,15 +29,19 @@ class AuditLog
|
|||||||
|
|
||||||
#[ORM\Column(length: 255)]
|
#[ORM\Column(length: 255)]
|
||||||
private ?string $path = null;
|
private ?string $path = null;
|
||||||
|
#[ORM\Column(length: 255)]
|
||||||
|
private ?string $userAgent = null;
|
||||||
|
|
||||||
|
|
||||||
// Le constructeur force le remplissage des données dès le départ
|
// Le constructeur force le remplissage des données dès le départ
|
||||||
public function __construct(Account $account, string $type, string $message, string $path)
|
public function __construct(Account $account, string $type, string $message, string $path,string $userAgent)
|
||||||
{
|
{
|
||||||
$this->account = $account;
|
$this->account = $account;
|
||||||
$this->type = $type;
|
$this->type = $type;
|
||||||
$this->message = $message;
|
$this->message = $message;
|
||||||
$this->path = $path;
|
$this->path = $path;
|
||||||
$this->actionAt = new \DateTimeImmutable();
|
$this->actionAt = new \DateTimeImmutable();
|
||||||
|
$this->userAgent = $userAgent;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Uniquement des Getters (Pas de Setters = Pas de modification possible en PHP)
|
// Uniquement des Getters (Pas de Setters = Pas de modification possible en PHP)
|
||||||
@@ -47,4 +51,5 @@ class AuditLog
|
|||||||
public function getType(): ?string { return $this->type; }
|
public function getType(): ?string { return $this->type; }
|
||||||
public function getMessage(): ?string { return $this->message; }
|
public function getMessage(): ?string { return $this->message; }
|
||||||
public function getPath(): ?string { return $this->path; }
|
public function getPath(): ?string { return $this->path; }
|
||||||
|
public function getUserAgent(): ?string{ return $this->userAgent;}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class AppLogger
|
|||||||
$path = $request ? $request->getRequestUri() : 'CLI/Internal';
|
$path = $request ? $request->getRequestUri() : 'CLI/Internal';
|
||||||
|
|
||||||
// Création de l'objet immuable via le constructeur
|
// Création de l'objet immuable via le constructeur
|
||||||
$log = new AuditLog($user, $type, $message, $path);
|
$log = new AuditLog($user, $type, $message, $path,$request->headers->get('user-agent'));
|
||||||
|
|
||||||
$this->entityManager->persist($log);
|
$this->entityManager->persist($log);
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ namespace App\Repository;
|
|||||||
use App\Entity\AuditLog;
|
use App\Entity\AuditLog;
|
||||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
use Doctrine\Persistence\ManagerRegistry;
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
|
||||||
/**
|
|
||||||
* @extends ServiceEntityRepository<AuditLog>
|
|
||||||
*/
|
|
||||||
class AuditLogRepository extends ServiceEntityRepository
|
class AuditLogRepository extends ServiceEntityRepository
|
||||||
{
|
{
|
||||||
public function __construct(ManagerRegistry $registry)
|
public function __construct(ManagerRegistry $registry)
|
||||||
@@ -16,28 +14,44 @@ class AuditLogRepository extends ServiceEntityRepository
|
|||||||
parent::__construct($registry, AuditLog::class);
|
parent::__construct($registry, AuditLog::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
// /**
|
/**
|
||||||
// * @return AuditLog[] Returns an array of AuditLog objects
|
* Récupère les logs en excluant les comptes ROLE_ROOT
|
||||||
// */
|
*/
|
||||||
// public function findByExampleField($value): array
|
// src/Repository/AuditLogRepository.php
|
||||||
// {
|
|
||||||
// return $this->createQueryBuilder('a')
|
|
||||||
// ->andWhere('a.exampleField = :val')
|
|
||||||
// ->setParameter('val', $value)
|
|
||||||
// ->orderBy('a.id', 'ASC')
|
|
||||||
// ->setMaxResults(10)
|
|
||||||
// ->getQuery()
|
|
||||||
// ->getResult()
|
|
||||||
// ;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public function findOneBySomeField($value): ?AuditLog
|
|
||||||
// {
|
|
||||||
// return $this->createQueryBuilder('a')
|
public function findAllForUser(?UserInterface $user): array
|
||||||
// ->andWhere('a.exampleField = :val')
|
{
|
||||||
// ->setParameter('val', $value)
|
$qb = $this->createQueryBuilder('l')
|
||||||
// ->getQuery()
|
->innerJoin('l.account', 'a')
|
||||||
// ->getOneOrNullResult()
|
->orderBy('l.actionAt', 'DESC')
|
||||||
// ;
|
->setMaxResults(100);
|
||||||
// }
|
|
||||||
|
// Si l'utilisateur n'est pas ROOT, on cache les actions des ROOT
|
||||||
|
if (!$user || !in_array('ROLE_ROOT', $user->getRoles())) {
|
||||||
|
// Astuce PostgreSQL pour DQL : on utilise une expression de comparaison
|
||||||
|
// qui force la lecture en texte sans utiliser le mot-clé CAST qui plante
|
||||||
|
$qb->andWhere('a.roles NOT LIKE :role')
|
||||||
|
->setParameter('role', '%ROLE_ROOT%');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $qb->getQuery()->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/Repository/AuditLogRepository.php
|
||||||
|
|
||||||
|
public function getQueryForUser(?UserInterface $user)
|
||||||
|
{
|
||||||
|
$qb = $this->createQueryBuilder('l')
|
||||||
|
->innerJoin('l.account', 'a')
|
||||||
|
->orderBy('l.actionAt', 'DESC');
|
||||||
|
|
||||||
|
if (!$user || !in_array('ROLE_ROOT', $user->getRoles())) {
|
||||||
|
$qb->andWhere('a.roles NOT LIKE :role')
|
||||||
|
->setParameter('role', '%ROLE_ROOT%');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $qb->getQuery();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
139
templates/dashboard/audit_logs.twig
Normal file
139
templates/dashboard/audit_logs.twig
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
{% extends 'dashboard/base.twig' %}
|
||||||
|
|
||||||
|
{% block title %}Traçabilité des actions{% endblock %}
|
||||||
|
|
||||||
|
{# BOUTON EXTRACTION DANS LE HEADER #}
|
||||||
|
{% block actions %}
|
||||||
|
<a href="{{ path('app_crm_audit_logs', {extract: true}) }}" target="_blank" class="flex items-center space-x-2 px-5 py-2.5 bg-emerald-600 hover:bg-emerald-700 text-white text-sm font-bold rounded-xl transition-all shadow-lg shadow-emerald-500/20 group">
|
||||||
|
<svg class="w-5 h-5 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||||
|
</svg>
|
||||||
|
<span>Exporter XLSX</span>
|
||||||
|
</a>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div class="w-full bg-white dark:bg-[#1e293b] rounded-2xl border border-slate-200 dark:border-slate-800 shadow-sm overflow-hidden">
|
||||||
|
|
||||||
|
{# Header du tableau avec statistiques rapides #}
|
||||||
|
<div class="px-8 py-6 border-b border-slate-100 dark:border-slate-800 flex items-center justify-between bg-slate-50/30 dark:bg-slate-800/30">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-bold text-slate-800 dark:text-white tracking-tight">Journal d'Audit</h2>
|
||||||
|
<p class="text-xs text-slate-500 dark:text-slate-400 mt-1">Historique complet des interactions sur l'Intranet</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="px-4 py-1.5 bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 text-[10px] font-black uppercase rounded-lg border border-blue-100 dark:border-blue-800/50">
|
||||||
|
{{ logs.getTotalItemCount }} ENREGISTREMENTS
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto custom-scrollbar">
|
||||||
|
<table class="w-full text-left border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-slate-50/50 dark:bg-slate-900/40 border-b border-slate-100 dark:border-slate-800">
|
||||||
|
<th class="px-8 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-[0.2em]">Horodatage</th>
|
||||||
|
<th class="px-8 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-[0.2em]">Administrateur & Appareil</th>
|
||||||
|
<th class="px-8 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-[0.2em] text-center">Action</th>
|
||||||
|
<th class="px-8 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-[0.2em]">Détails de l'activité</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-100 dark:divide-slate-800">
|
||||||
|
{% for log in logs %}
|
||||||
|
<tr class="hover:bg-slate-50/50 dark:hover:bg-slate-800/30 transition-colors duration-150">
|
||||||
|
|
||||||
|
{# 1. DATE & HEURE #}
|
||||||
|
<td class="px-8 py-4 whitespace-nowrap align-top">
|
||||||
|
<div class="text-sm font-bold text-slate-700 dark:text-slate-200">{{ log.actionAt|date('d/m/Y') }}</div>
|
||||||
|
<div class="text-[10px] text-slate-400 font-mono mt-0.5 tracking-wider">{{ log.actionAt|date('H:i:s') }}</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{# 2. ADMINISTRATEUR + USER AGENT #}
|
||||||
|
<td class="px-8 py-4 whitespace-nowrap align-top">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<div class="h-10 w-10 rounded-xl bg-gradient-to-br from-blue-500 to-indigo-600 flex flex-shrink-0 items-center justify-center text-white font-bold text-xs shadow-md mt-0.5">
|
||||||
|
{{ log.account.firstName|first|upper }}{{ log.account.name|first|upper }}
|
||||||
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<div class="text-sm font-bold text-slate-800 dark:text-white flex items-center mb-0.5">
|
||||||
|
{{ log.account.firstName }} {{ log.account.name }}
|
||||||
|
{% if 'ROLE_ROOT' in log.account.roles %}
|
||||||
|
<span class="ml-2 px-1.5 py-0.5 rounded text-[8px] bg-red-600/10 text-red-600 dark:bg-red-500/20 dark:text-red-400 font-black uppercase tracking-tighter border border-red-200 dark:border-red-900/50">Root</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="text-[11px] text-slate-400 dark:text-slate-500 mb-2 font-medium">{{ log.account.email }}</div>
|
||||||
|
|
||||||
|
{# Bloc User Agent avec icône SVG dynamique #}
|
||||||
|
{% if log.userAgent %}
|
||||||
|
{% set ua = log.userAgent|lower %}
|
||||||
|
<div class="flex items-center text-[10px] text-slate-500 dark:text-slate-400 bg-slate-100/50 dark:bg-slate-900/50 px-2 py-1.5 rounded-lg border border-slate-200/50 dark:border-slate-800 max-w-[260px] group/ua">
|
||||||
|
<span class="mr-2 flex-shrink-0">
|
||||||
|
{% if 'firefox' in ua %}
|
||||||
|
<svg class="w-3.5 h-3.5 text-orange-500" fill="currentColor" viewBox="0 0 24 24"><path d="M23.9 12c0 6.6-5.4 12-11.9 12C5.4 24 0 18.6 0 12S5.4 0 12 0c6.5 0 11.9 5.4 11.9 12zM10.8 4.7c-.5.1-1.3.4-1.3.4s.8-.2 1.3-.3c1.5-.4 3.1-.2 4.4.6 1.3.7 2.2 2 2.5 3.4.1.7.1 1.5-.1 2.2-.2 1-1.2 2.2-1.2 2.2s.8-.9 1-1.8c.3-1.4-.1-2.9-1.2-4-1.1-1.1-2.6-1.6-4.2-1.4-1.4.1-2.1.8-2.6 1.4-.5.6-.7 1.4-.6 2.1.1.7.5 1.4 1.1 1.8.6.4 1.3.5 2 .4.7-.1 1.3-.5 1.7-1.1.4-.6.5-1.3.3-2-.1-.7-.5-1.3-1-1.7s-1.2-.5-1.9-.4c-.7.1-1.3.5-1.7 1.1-.3.5-.4 1.1-.3 1.7.1.5.3.9.7 1.2.4.3.8.4 1.3.3.5-.1.9-.3 1.2-.7.2-.4.3-.8.2-1.3-.1-.4-.3-.7-.6-.9-.3-.2-.6-.3-1-.2-.3 0-.6.1-.8.4-.2.2-.3.5-.2.8.1.3.3.5.6.6.3.1.6 0 .8-.2.2-.2.3-.4.2-.7 0-.3-.2-.5-.5-.6-.2-.1-.5 0-.7.2s-.3.4-.2.7z"/></svg>
|
||||||
|
{% elseif 'edg/' in ua %}
|
||||||
|
<svg class="w-3.5 h-3.5 text-blue-500" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm0 18.75c-3.728 0-6.75-3.022-6.75-6.75s3.022-6.75 6.75-6.75 6.75 3.022 6.75 6.75-3.022 6.75-6.75 6.75z"/></svg>
|
||||||
|
{% elseif 'chrome' in ua %}
|
||||||
|
<svg class="w-3.5 h-3.5 text-blue-400" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0C8.21 0 4.83 1.75 2.64 4.5l3.96 6.84A6.004 6.004 0 0 1 12 6h10.36A12.012 12.012 0 0 0 12 0zm-1.04 13.5l-5.12-8.88A11.936 11.936 0 0 0 0 12c0 6.07 4.51 11.08 10.36 11.92l3.96-6.84a6.012 6.012 0 0 1-3.36-3.58zm12.4-7.5H12a6.002 6.002 0 0 1 3.36 9.42l-5.12 8.88C10.74 23.9 11.36 24 12 24c6.63 0 12-5.37 12-12 0-2.12-.55-4.12-1.52-5.88z"/></svg>
|
||||||
|
{% elseif 'safari' in ua and 'chrome' not in ua %}
|
||||||
|
<svg class="w-3.5 h-3.5 text-sky-600" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z"/><path d="M14.5 9.5L9.5 14.5M14.5 9.5L12 3M14.5 9.5L21 12M9.5 14.5L12 21M9.5 14.5L3 12"/></svg>
|
||||||
|
{% else %}
|
||||||
|
<svg class="w-3.5 h-3.5 text-slate-400" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
<span class="truncate font-mono opacity-80" title="{{ log.userAgent }}">
|
||||||
|
{{ log.userAgent }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{# 3. BADGE ACTION #}
|
||||||
|
<td class="px-8 py-4 whitespace-nowrap text-center align-top">
|
||||||
|
{% set typeStyles = {
|
||||||
|
'CREATE': 'bg-emerald-500/10 text-emerald-600 border-emerald-500/20',
|
||||||
|
'DELETE': 'bg-rose-500/10 text-rose-600 border-rose-500/20',
|
||||||
|
'VIEW': 'bg-sky-500/10 text-sky-600 border-sky-500/20',
|
||||||
|
'AUTH': 'bg-amber-500/10 text-amber-600 border-amber-500/20'
|
||||||
|
} %}
|
||||||
|
<span class="px-3 py-1.5 rounded-lg text-[10px] font-black border uppercase tracking-widest {{ typeStyles[log.type] ?? 'bg-slate-500/10 text-slate-500 border-slate-500/20' }}">
|
||||||
|
{{ log.type }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{# 4. MESSAGE ET CHEMIN #}
|
||||||
|
<td class="px-8 py-4 align-top">
|
||||||
|
<div class="text-sm text-slate-600 dark:text-slate-300 font-medium leading-relaxed max-w-2xl">{{ log.message }}</div>
|
||||||
|
<div class="mt-2 flex items-center">
|
||||||
|
<span class="text-[9px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-tighter mr-2">URL :</span>
|
||||||
|
<code class="text-[10px] bg-slate-100 dark:bg-slate-900 px-2 py-0.5 rounded text-blue-500 dark:text-blue-400 font-mono">
|
||||||
|
{{ log.path }}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% else %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="px-8 py-12 text-center">
|
||||||
|
<div class="text-slate-400 dark:text-slate-600 italic">Aucun log trouvé dans cette période.</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# FOOTER & PAGINATION #}
|
||||||
|
<div class="px-8 py-6 bg-slate-50/50 dark:bg-slate-900/40 border-t border-slate-100 dark:border-slate-800">
|
||||||
|
<div class="flex flex-col md:flex-row items-center justify-between gap-6">
|
||||||
|
<div class="text-xs font-semibold text-slate-400 uppercase tracking-widest">
|
||||||
|
Page {{ logs.getCurrentPageNumber }} — Affichage de {{ logs|length }} logs
|
||||||
|
</div>
|
||||||
|
<div class="navigation shadow-sm rounded-xl overflow-hidden">
|
||||||
|
{{ knp_pagination_render(logs) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -3,110 +3,123 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{% block title %}Administration{% endblock %} - LudikEvent</title>
|
<title>{% block title %}Administration{% endblock %} — Intranet Ludikevent</title>
|
||||||
{{ vite_asset('admin.js', {}) }}
|
{{ vite_asset('admin.js', {}) }}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.custom-scrollbar::-webkit-scrollbar { width: 5px; height: 5px; }
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
|
||||||
|
.dark .custom-scrollbar::-webkit-scrollbar-thumb { background: #334155; }
|
||||||
|
|
||||||
|
.page-transition { animation: fadeIn 0.3s ease-out; }
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(4px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100 antialiased">
|
<body class="bg-slate-50 dark:bg-[#0f172a] text-slate-900 dark:text-slate-200 antialiased overflow-hidden font-sans">
|
||||||
|
|
||||||
<div class="flex h-screen overflow-hidden">
|
<div class="flex h-screen overflow-hidden">
|
||||||
|
|
||||||
{# Overlay pour mobile #}
|
{# SIDEBAR MODERNE #}
|
||||||
<div id="sidebar-overlay" class="fixed inset-0 z-20 bg-black/50 lg:hidden hidden"></div>
|
<aside id="sidebar" class="fixed inset-y-0 left-0 z-40 w-72 bg-white dark:bg-[#1e293b] border-r border-slate-200 dark:border-slate-800 transform -translate-x-full lg:translate-x-0 transition-all duration-300 ease-in-out shadow-xl lg:shadow-none">
|
||||||
|
|
||||||
{# SIDEBAR #}
|
<div class="flex items-center px-8 h-20 border-b border-slate-100 dark:border-slate-800">
|
||||||
<aside id="sidebar" class="fixed inset-y-0 left-0 z-30 w-64 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 transform -translate-x-full lg:translate-x-0 transition-transform duration-300 ease-in-out">
|
<div class="flex items-center space-x-3">
|
||||||
<div class="flex items-center justify-between px-6 h-16 border-b border-gray-200 dark:border-gray-700">
|
<div class="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center shadow-lg shadow-blue-500/30">
|
||||||
<span class="text-xl font-bold bg-gradient-to-r from-blue-600 to-indigo-500 bg-clip-text text-transparent">LudikEvent CRM</span>
|
<span class="text-white font-bold text-xl font-serif">L</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-lg font-bold tracking-tight text-slate-800 dark:text-white uppercase italic">Intranet <span class="text-blue-600 not-italic">Ludikevent</span></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="flex flex-col p-4 space-y-1 h-[calc(100vh-64px)] overflow-y-auto">
|
<nav class="flex flex-col p-6 space-y-8 h-[calc(100vh-80px)] overflow-y-auto custom-scrollbar">
|
||||||
{% macro nav_link(path, label, icon_svg, current_route) %}
|
<div>
|
||||||
{% set isActive = app.request.get('_route') == current_route %}
|
<p class="px-4 mb-4 text-[10px] font-semibold text-slate-400 dark:text-slate-500 uppercase tracking-[0.2em]">Menu Principal</p>
|
||||||
<a href="{{ path }}" class="flex items-center space-x-3 p-3 rounded-lg transition-all duration-200 {{ isActive ? 'bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400' : 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700' }}">
|
<div class="space-y-1">
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">{{ icon_svg|raw }}</svg>
|
{% macro nav_link(path, label, icon_svg, current_route) %}
|
||||||
<span class="font-medium text-sm">{{ label }}</span>
|
{% set isActive = app.request.get('_route') == current_route %}
|
||||||
</a>
|
<a href="{{ path }}" class="flex items-center space-x-3 px-4 py-3 rounded-xl transition-all duration-200 group {{ isActive ? 'bg-blue-600 text-white shadow-lg shadow-blue-500/30' : 'hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-600 dark:text-slate-400' }}">
|
||||||
{% endmacro %}
|
<svg class="w-5 h-5 {{ isActive ? 'text-white' : 'text-slate-400 group-hover:text-blue-500' }}" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">{{ icon_svg|raw }}</svg>
|
||||||
|
<span class="font-semibold text-sm">{{ label }}</span>
|
||||||
{% import _self as menu %}
|
|
||||||
|
|
||||||
{{ menu.nav_link(path('app_crm'), 'Tableau de bord', '<path d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"></path>', 'app_crm') }}
|
|
||||||
{{ menu.nav_link('#', 'Clients', '<path d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path>', 'app_clients') }}
|
|
||||||
{{ menu.nav_link('#', 'Articles', '<path d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"></path>', 'app_articles') }}
|
|
||||||
|
|
||||||
{# Groupe Paramètres #}
|
|
||||||
<div class="pt-2">
|
|
||||||
{% set isAdminRoute = app.request.get('_route') matches '/^app_crm_administrateur/' %}
|
|
||||||
<button id="settings-toggle" class="w-full flex items-center justify-between p-3 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition duration-150">
|
|
||||||
<div class="flex items-center space-x-3">
|
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37a1.724 1.724 0 002.572-1.065z"></path></svg>
|
|
||||||
<span class="font-medium text-sm">Paramètres</span>
|
|
||||||
</div>
|
|
||||||
<svg id="settings-chevron" class="w-4 h-4 transition-transform duration-200 {{ isAdminRoute ? 'rotate-180' }}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M19 9l-7 7-7-7"></path></svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<ul id="settings-submenu" class="ml-9 mt-1 space-y-1 {{ isAdminRoute ? '' : 'hidden' }}">
|
|
||||||
<li>
|
|
||||||
<a href="{{ path('app_crm_administrateur') }}" class="block p-2 rounded-lg text-sm {{ isAdminRoute ? 'text-blue-600 font-bold dark:text-blue-400' : 'text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white' }}">
|
|
||||||
Administrateurs
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
{% endmacro %}
|
||||||
<li>
|
{% import _self as menu %}
|
||||||
<a href="#" class="block p-2 rounded-lg text-sm text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white">Services</a>
|
|
||||||
</li>
|
{{ menu.nav_link(path('app_crm'), 'Dashboard', '<path d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>', 'app_crm') }}
|
||||||
</ul>
|
{{ menu.nav_link('#', 'Clients', '<path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>', 'app_clients') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p class="px-4 mb-4 text-[10px] font-semibold text-slate-400 dark:text-slate-500 uppercase tracking-[0.2em]">Configuration</p>
|
||||||
|
<div class="space-y-1">
|
||||||
|
{% set isAdminActive = app.request.get('_route') matches '/^app_crm_administrateur/' %}
|
||||||
|
{% set isLogsActive = app.request.get('_route') == 'app_crm_audit_logs' %}
|
||||||
|
{% set isOpen = isAdminActive or isLogsActive %}
|
||||||
|
|
||||||
|
<button id="settings-toggle" class="w-full flex items-center justify-between px-4 py-3 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-600 dark:text-slate-400 transition-all duration-200 group">
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<svg class="w-5 h-5 text-slate-400 group-hover:text-blue-500" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37a1.724 1.724 0 002.572-1.065z"></path></svg>
|
||||||
|
<span class="font-semibold text-sm">Paramètres</span>
|
||||||
|
</div>
|
||||||
|
<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' }}">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{# CONTENU PRINCIPAL #}
|
{# CONTENU FULL WIDTH #}
|
||||||
<main class="flex-1 flex flex-col min-w-0 lg:ml-64 bg-gray-50 dark:bg-gray-900">
|
<main class="flex-1 flex flex-col min-w-0 lg:ml-72 bg-slate-50 dark:bg-[#0f172a] h-screen overflow-y-auto custom-scrollbar relative">
|
||||||
|
|
||||||
{# Header #}
|
{# Header Glassmorphism #}
|
||||||
<header class="h-16 flex items-center justify-between px-6 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 sticky top-0 z-20">
|
<header class="h-20 flex items-center justify-between px-8 bg-white/80 dark:bg-[#1e293b]/80 backdrop-blur-md border-b border-slate-200 dark:border-slate-800 sticky top-0 z-30">
|
||||||
<button id="sidebar-toggle" class="lg:hidden text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-700 p-2 rounded-md transition-colors">
|
<button id="sidebar-toggle" class="lg:hidden p-2 rounded-lg bg-slate-100 dark:bg-slate-800 text-slate-600">
|
||||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h16"></path></svg>
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7"></path></svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="flex items-center space-x-4 ml-auto">
|
<div class="flex items-center space-x-6 ml-auto">
|
||||||
<div class="text-right hidden sm:block">
|
<div class="flex items-center space-x-3 px-4 py-2 bg-slate-100 dark:bg-slate-800 rounded-2xl border border-slate-200 dark:border-slate-700">
|
||||||
<p class="text-xs font-medium text-gray-900 dark:text-white">{{ app.user.username|default('Admin') }}</p>
|
<div class="w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center text-blue-600 font-bold text-xs">
|
||||||
<a href="{{ path('app_logout') }}" class="text-[10px] text-red-500 hover:underline">Déconnexion</a>
|
{{ app.user.firstName|first|upper }}
|
||||||
|
</div>
|
||||||
|
<div class="text-left hidden sm:block">
|
||||||
|
<p class="text-xs font-bold text-slate-800 dark:text-white">{{ app.user.firstName }} {{ app.user.name }}</p>
|
||||||
|
<a href="{{ path('app_logout') }}" class="text-[10px] text-red-500 font-semibold hover:text-red-600 uppercase tracking-tighter">Déconnexion</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{# Zone Flash Messages #}
|
{# Zone de contenu 100% #}
|
||||||
<div id="flash-container" class="fixed top-20 right-6 z-50 flex flex-col gap-3 w-80">
|
<div class="p-6 md:p-8 lg:p-10 page-transition w-full">
|
||||||
{% for label, messages in app.flashes %}
|
<div class="flex flex-col md:flex-row md:items-end justify-between mb-10 gap-4 border-b border-slate-200/60 dark:border-slate-800 pb-8">
|
||||||
{% for message in messages %}
|
<div>
|
||||||
<div class="flash-message transform transition-all duration-500 p-4 rounded-lg shadow-lg border flex items-center justify-between {{ label == 'success' ? 'bg-green-100 border-green-200 text-green-800 dark:bg-green-900/80 dark:text-green-300 dark:border-green-800' : 'bg-red-100 border-red-200 text-red-800 dark:bg-red-900/80 dark:text-red-300 dark:border-red-800' }}">
|
<p class="text-blue-600 font-bold text-[10px] uppercase tracking-[0.4em] mb-2">Espace Administration</p>
|
||||||
<p class="text-sm font-medium">{{ message }}</p>
|
<h1 class="text-4xl font-extrabold text-slate-900 dark:text-white tracking-tight">
|
||||||
<button type="button" onclick="this.parentElement.remove()" class="ml-4 opacity-50 hover:opacity-100">×</button>
|
{% block title_header %}{{ block('title') }}{% endblock %}
|
||||||
</div>
|
</h1>
|
||||||
{% endfor %}
|
</div>
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{# Zone Contenu #}
|
|
||||||
<div class="p-6 md:p-8">
|
|
||||||
<div class="flex flex-col md:flex-row md:items-center justify-between mb-8 gap-4">
|
|
||||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
|
|
||||||
{% block title_header %}{{ block('title') }}{% endblock %}
|
|
||||||
</h1>
|
|
||||||
<div class="flex items-center space-x-3">
|
<div class="flex items-center space-x-3">
|
||||||
{% block actions %}{% endblock %}
|
{% block actions %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="animate-fadeIn w-full">
|
<div class="w-full">
|
||||||
{% block body %}{% endblock %}
|
{% block body %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
{# Logo #}
|
{# Logo #}
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<img class="h-16 w-auto" src="{{ asset('assets/images/logo.png') }}" alt="Ludikevent Logo"/>
|
<img class="h-50 w-auto" src="{{ asset('assets/images/logo.png') }}" alt="Ludikevent Logo"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Icône de succès #}
|
{# Icône de succès #}
|
||||||
|
|||||||
Reference in New Issue
Block a user