diff --git a/.htaccess b/.htaccess
index 5af7133..350430a 100644
--- a/.htaccess
+++ b/.htaccess
@@ -1,4 +1,227 @@
-# --- Sécurité : en-têtes globaux ---
+# BEGIN Security - Shared rules for all sites
+# Last updated: 2026-05-22
+
+# Block known attacker ranges (VPS/hosting providers used for brute force)
+
+RewriteEngine On
+# FranTech / PonyNET / BuyVM
+RewriteCond %{REMOTE_ADDR} ^107\.189\.
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^209\.141\.
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^104\.244\.73\.
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^198\.98\.
+RewriteRule .* - [F,L]
+# WatchComm / FNI
+RewriteCond %{REMOTE_ADDR} ^205\.185\.
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^45\.61\.18[4-8]\.
+RewriteRule .* - [F,L]
+# AltunHOST / hosting providers
+RewriteCond %{REMOTE_ADDR} ^5\.83\.135\.
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^192\.109\.200\.
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^103\.60\.12\.
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^185\.198\.240\.
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^173\.239\.240\.
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^104\.234\.53\.
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^104\.238\.222\.
+RewriteRule .* - [F,L]
+# TECHOFF SRV LIMITED (scraper)
+RewriteCond %{REMOTE_ADDR} ^195\.178\.110\.
+RewriteRule .* - [F,L]
+# Brute force attackers (May 11)
+RewriteCond %{REMOTE_ADDR} ^103\.59\.161\.
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^185\.153\.151\.
+RewriteRule .* - [F,L]
+# F.N.S. HOLDINGS LIMITED
+RewriteCond %{REMOTE_ADDR} ^216\.73\.160\.
+RewriteRule .* - [F,L]
+# DigitalOcean brute force
+RewriteCond %{REMOTE_ADDR} ^143\.198\.
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^146\.190\.
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^142\.93\.
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^170\.64\.
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^134\.209\.
+RewriteRule .* - [F,L]
+# Cipher Operations (Serbia)
+RewriteCond %{REMOTE_ADDR} ^62\.60\.130\.
+RewriteRule .* - [F,L]
+# UK Dedicated Servers
+RewriteCond %{REMOTE_ADDR} ^185\.192\.7[0-1]\.
+RewriteRule .* - [F,L]
+# GSL Networks
+RewriteCond %{REMOTE_ADDR} ^85\.203\.2[1-3]\.
+RewriteRule .* - [F,L]
+# Argon Data Communication
+RewriteCond %{REMOTE_ADDR} ^157\.66\.56\.
+RewriteRule .* - [F,L]
+# Pfcloud UG
+RewriteCond %{REMOTE_ADDR} ^217\.60\.241\.
+RewriteRule .* - [F,L]
+# Omegatech LTD
+RewriteCond %{REMOTE_ADDR} ^158\.94\.210\.
+RewriteRule .* - [F,L]
+# Individual attackers
+RewriteCond %{REMOTE_ADDR} ^94\.26\.106\.111$
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^176\.65\.132\.166$
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^37\.120\.217\.182$
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^8\.210\.170\.233$
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^120\.24\.67\.156$
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^140\.245\.111\.5$
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^20\.9\.82\.33$
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^45\.156\.87\.222$
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^31\.57\.184\.107$
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^92\.205\.109\.21$
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^72\.167\.150\.128$
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^165\.154\.38\.157$
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^50\.6\.207\.27$
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^141\.98\.11\.117$
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^136\.243\.176\.166$
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^123\.30\.233\.1[2-3]$
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^91\.227\.37\.60$
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^78\.142\.18\.172$
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^66\.116\.235\.214$
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^149\.88\.110\.49$
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^35\.227\.109\.86$
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^103\.253\.27\.44$
+RewriteRule .* - [F,L]
+# Contabo (individual IP)
+RewriteCond %{REMOTE_ADDR} ^85\.239\.235\.108$
+RewriteRule .* - [F,L]
+# xmlrpc + brute force attackers (May 22)
+RewriteCond %{REMOTE_ADDR} ^105\.184\.79\.191$
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^45\.94\.31\.119$
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^138\.2\.98\.238$
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^20\.89\.64\.143$
+RewriteRule .* - [F,L]
+# Vulnerability scanners (May 22-23)
+RewriteCond %{REMOTE_ADDR} ^143\.92\.32\.30$
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^194\.26\.192\.219$
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^38\.99\.81\.38$
+RewriteRule .* - [F,L]
+# Massive brute force (May 23)
+RewriteCond %{REMOTE_ADDR} ^193\.26\.115\.
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^51\.79\.177\.162$
+RewriteRule .* - [F,L]
+# ALFA shell scanners / 2ipcore crawler
+RewriteCond %{REMOTE_ADDR} ^185\.213\.154\.185$
+RewriteRule .* - [F,L]
+# Vulnerability scanners (May 23 evening)
+# Microsoft Azure — shell/backdoor scanner
+RewriteCond %{REMOTE_ADDR} ^20\.151\.117\.104$
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^4\.228\.100\.214$
+RewriteRule .* - [F,L]
+# 2ipcore ALFA shell scanners (expand to /24)
+RewriteCond %{REMOTE_ADDR} ^2\.58\.56\.
+RewriteRule .* - [F,L]
+# DigitalOcean scanners
+RewriteCond %{REMOTE_ADDR} ^157\.230\.
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^139\.59\.
+RewriteRule .* - [F,L]
+# Directory enumeration scanner
+RewriteCond %{REMOTE_ADDR} ^91\.199\.118\.57$
+RewriteRule .* - [F,L]
+# ALFA shell scanners (expand to /24)
+RewriteCond %{REMOTE_ADDR} ^45\.92\.1\.
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^45\.154\.98\.
+RewriteRule .* - [F,L]
+# Google Cloud spammer
+RewriteCond %{REMOTE_ADDR} ^34\.42\.34\.237$
+RewriteRule .* - [F,L]
+# Azure vulnerability scanners (May 24)
+RewriteCond %{REMOTE_ADDR} ^20\.48\.233\.234$
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^20\.63\.100\.211$
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^20\.226\.23\.23$
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^20\.9\.81\.163$
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^20\.63\.46\.176$
+RewriteRule .* - [F,L]
+# Persistent brute force (Hetzner)
+RewriteCond %{REMOTE_ADDR} ^168\.119\.26\.89$
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^173\.249\.4\.57$
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^216\.251\.35\.201$
+RewriteRule .* - [F,L]
+# File enumeration scanner (May 25)
+RewriteCond %{REMOTE_ADDR} ^158\.158\.44\.35$
+RewriteRule .* - [F,L]
+# Reconnaissance bots / Tor (May 25)
+RewriteCond %{REMOTE_ADDR} ^149\.102\.230\.133$
+RewriteRule .* - [F,L]
+RewriteCond %{REMOTE_ADDR} ^82\.102\.18\.116$
+RewriteRule .* - [F,L]
+# Directory scanner (May 25)
+RewriteCond %{REMOTE_ADDR} ^195\.3\.220\.7$
+RewriteRule .* - [F,L]
+# Azure shell scanner (May 25)
+RewriteCond %{REMOTE_ADDR} ^20\.226\.59\.228$
+RewriteRule .* - [F,L]
+# WP installer scanner (May 26)
+RewriteCond %{REMOTE_ADDR} ^194\.26\.192\.75$
+RewriteRule .* - [F,L]
+# Azure webshell scanner (May 26)
+RewriteCond %{REMOTE_ADDR} ^104\.43\.242\.179$
+RewriteRule .* - [F,L]
+# Block non-existent plugin/theme probes (saves PHP workers)
+RewriteRule ^ALFA_DATA/ - [F,L]
+RewriteRule ^alfacgiapi/ - [F,L]
+
+
+# Block xmlrpc.php
+
+
+ Require all denied
+
+
+# END Security
+
+# --- Security headers ---
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "DENY"
@@ -6,60 +229,65 @@
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
- # CSP en observation (ne bloque rien pour l’instant)
+ # CSP en observation (ne bloque rien pour l'instant)
# --- CSP (global) : autorise Matomo + CDNs ---
Header always set Content-Security-Policy-Report-Only "default-src 'self'; img-src 'self' data: https: https://staging.uslugi.fr/matomo/; script-src 'self' 'unsafe-inline' https://staging.uslugi.fr/matomo/ https://cdn.jsdelivr.net https://www.googletagmanager.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https:; frame-ancestors 'none'"
-# Réduire surface d’attaque
+# Reduce attack surface
Options -Indexes -MultiViews
-# --- Réécriture & HTTPS ---
+# --- Rewrite & HTTPS ---
RewriteEngine On
-# Forcer HTTPS (OK derrière proxy/Varnish)
+# Force HTTPS (behind proxy/Varnish)
RewriteCond %{HTTPS} !=on
RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
-# Laisser passer Let's Encrypt
+# Let's Encrypt
RewriteRule ^\.well-known/acme-challenge/ - [L]
-# --- Protection chemins & fichiers sensibles ---
-# Bloquer VCS
+# --- Sensitive paths & files ---
+# Block VCS
RewriteRule ^(\.git|\.svn|\.hg)(/|$) - [F]
-# Bloquer dotfiles (sauf .well-known)
+# Block dotfiles (except .well-known)
RewriteRule ^\.(?!well-known/) - [F]
-# Bloquer extensions sensibles
+# Block sensitive extensions
RewriteRule \.(env|ini|log|bak|old|sql|zip|tar|gz|7z)$ - [F]
-# Bloquer fichiers sauvegardes *~
+# Block backup files *~
RewriteRule ^(.*/)?[^/]+~$ - [F]
-# Bloquer certains noms explicites
+# Block specific filenames
Require all denied
-# Ceinture+bretelles : jamais de scripts dans /uploads
+# No scripts in /uploads
RewriteRule ^(?:public/)?uploads/.*\.(php|phtml|phar|phps|cgi|pl|py|sh|rb)$ - [F,NC]
-# --- Routage applicatif ---
-# 0) Servir les fichiers/dirs existants
+# --- Application routing ---
+# 0) Serve existing files/dirs
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
-# 1) (optionnel) Normaliser la barre finale (sauf /)
+# 1) Normalize trailing slash (except /)
RewriteCond %{REQUEST_URI} !^/$
RewriteCond %{REQUEST_URI} (.+)/$
RewriteRule ^ %1 [R=301,L]
-# 2) URL "propres" sans extension -> router.php
+# 2) Clean URLs -> router.php
RewriteRule ^([^.]+)$ router.php?slug=$1 [L,QSA]
-# 3) Fallback général
+# 2bis) Unknown PHP files => 404
+RewriteCond %{REQUEST_URI} \.php$ [NC]
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteRule ^ - [R=404,L]
+
+# 3) General fallback
RewriteRule ^ index.php [L,QSA]
-# --- PWA / Types & cache SW ---
+# --- PWA / Types & SW cache ---
AddType application/manifest+json .webmanifest
@@ -68,7 +296,7 @@ AddType application/manifest+json .webmanifest
Header set Expires "0"
-# Activer des en-têtes différents uniquement pour /visio/join.php
+# Visio-specific headers for /visio/join.php
SetEnvIf Request_URI "^/visio/join\.php$" IS_VISIO=1
@@ -81,5 +309,3 @@ SetEnvIf Request_URI "^/visio/join\.php$" IS_VISIO=1
DirectoryIndex index.php index.html
-
-
diff --git a/admin/index.php b/admin/index.php
index 7a6066c..9e704bf 100644
--- a/admin/index.php
+++ b/admin/index.php
@@ -1,7 +1,4 @@
-
@@ -197,7 +230,37 @@
?>
+
diff --git a/admin/tools/connecter_en_admin.php b/admin/tools/connecter_en_admin.php
index fd44203..ef84a55 100644
--- a/admin/tools/connecter_en_admin.php
+++ b/admin/tools/connecter_en_admin.php
@@ -1,49 +1,51 @@
'.htmlspecialchars(($translations['access_denied'] ?? 'Доступ запрещён.'), ENT_QUOTES, 'UTF-8').'
';
+ echo '' . htmlspecialchars(($translations['access_denied'] ?? 'Доступ запрещён.'), ENT_QUOTES, 'UTF-8') . '
';
exit;
-}
-// reauth (10 min)
-$mustReauth=function(int $ttl=600){$t=(int)($_SESSION['admin_reauth_at']??0);return ($t===0||(time()-$t)>$ttl);};
-if ($mustReauth()){
- $qs=http_build_query(['page'=>'reauth','redir'=>($_SERVER['REQUEST_URI']??'index.php?page=maintenance')]);
- header('Location:?'.$qs);
+ }
+ // reauth (10 min)
+ $mustReauth = function (int $ttl = 600) {$t = (int) ($_SESSION['admin_reauth_at'] ?? 0);return ($t === 0 || (time() - $t) > $ttl);};
+ if ($mustReauth()) {
+ $qs = http_build_query(['page' => 'reauth', 'redir' => ($_SERVER['REQUEST_URI'] ?? 'index.php?page=maintenance')]);
+ header('Location:?' . $qs);
exit;
-}
+ }
-// CSRF
-if (!isset($_SESSION['csrf_token'])) {
+ // CSRF
+ if (! isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
-}
-$CSRF = $_SESSION['csrf_token'];
+ }
+ $CSRF = $_SESSION['csrf_token'];
-function e($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
+ function e($s)
+ {return htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8');}
-// Helpers DB : test existence de la table de log
-function tableExists(PDO $pdo, string $table): bool {
+ // Helpers DB : test existence de la table de log
+ function tableExists(PDO $pdo, string $table): bool
+ {
$sql = "SELECT 1 FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? LIMIT 1";
$st = $pdo->prepare($sql);
$st->execute([$table]);
- return (bool)$st->fetchColumn();
-}
+ return (bool) $st->fetchColumn();
+ }
-// ------------ Traitement POST ------------
-$flash = null;
+ // ------------ Traitement POST ------------
+ $flash = null;
-if (($_POST['do'] ?? '') === 'impersonate') {
+ if (($_POST['do'] ?? '') === 'impersonate') {
try {
- if (!hash_equals($_SESSION['csrf_token'] ?? '', $_POST['csrf'] ?? '')) {
+ if (! hash_equals($_SESSION['csrf_token'] ?? '', $_POST['csrf'] ?? '')) {
throw new Exception('CSRF');
}
- $email = trim((string)($_POST['email'] ?? ''));
+ $email = trim((string) ($_POST['email'] ?? ''));
if ($email === '') {
throw new Exception('NO_EMAIL');
}
@@ -57,37 +59,37 @@ if (($_POST['do'] ?? '') === 'impersonate') {
$st->execute([$email]);
$user = $st->fetch(PDO::FETCH_ASSOC);
- if (!$user) {
+ if (! $user) {
throw new Exception('NOT_FOUND');
}
- $role = (string)($user['role'] ?? '');
+ $role = (string) ($user['role'] ?? '');
// On autorise seulement certains rôles à être "impersonnés"
$allowedRoles = ['praticien', 'client'];
- if (!in_array($role, $allowedRoles, true)) {
+ if (! in_array($role, $allowedRoles, true)) {
throw new Exception('ROLE_FORBIDDEN');
}
// Sauvegarder l’admin actuel si ce n’est pas déjà fait
- if (empty($_SESSION['admin_impersonate']) || !is_array($_SESSION['admin_impersonate'])) {
+ if (empty($_SESSION['admin_impersonate']) || ! is_array($_SESSION['admin_impersonate'])) {
$_SESSION['admin_impersonate'] = [
- 'admin_user_id' => $_SESSION['user_id'] ?? null,
+ 'admin_compte_id' => $_SESSION['compte_id'] ?? null,
'admin_reference_id' => $_SESSION['reference_id'] ?? null,
- 'admin_role' => $_SESSION['role'] ?? null,
+ 'admin_role' => $_SESSION['role'] ?? null,
'started_at' => time(),
];
}
// Log de l’action si la table existe
if (tableExists($pdo, 'admin_impersonation_log')) {
- $ip = $_SERVER['REMOTE_ADDR'] ?? null;
+ $ip = $_SERVER['REMOTE_ADDR'] ?? null;
$ua = $_SERVER['HTTP_USER_AGENT'] ?? null;
- $adminUid = (int)($_SESSION['user_id'] ?? 0);
- $adminRef = (int)($_SESSION['reference_id'] ?? 0);
- $targetUid = (int)($user['id'] ?? 0);
- $targetRef = (int)($user['reference_id'] ?? 0);
- $emailLog = (string)($user['email'] ?? '');
+ $adminUid = (int) ($_SESSION['compte_id'] ?? 0);
+ $adminRef = (int) ($_SESSION['reference_id'] ?? 0);
+ $targetUid = (int) ($user['id'] ?? 0);
+ $targetRef = (int) ($user['reference_id'] ?? 0);
+ $emailLog = (string) ($user['email'] ?? '');
$logSql = "INSERT INTO admin_impersonation_log
(admin_user_id, admin_reference_id, target_user_id, target_reference_id, target_role, email, ip, user_agent)
@@ -105,15 +107,34 @@ if (($_POST['do'] ?? '') === 'impersonate') {
}
// Basculer la session sur l’utilisateur cible
- $_SESSION['user_id'] = (int)$user['id'];
- $_SESSION['role'] = $role;
- $_SESSION['reference_id'] = (int)($user['reference_id'] ?? 0);
+ session_regenerate_id(true);
+
+ unset(
+ $_SESSION['client_id'],
+ $_SESSION['praticien_id'],
+ $_SESSION['secretaire_id'],
+ $_SESSION['admin_id'],
+ $_SESSION['compte_id'],
+ $_SESSION['reference_id'],
+ $_SESSION['role']
+ );
+
+ $_SESSION['compte_id'] = (int) $user['id'];
+ $_SESSION['role'] = $role;
+ $_SESSION['reference_id'] = (int) ($user['reference_id'] ?? 0);
+ $_SESSION['last_activity'] = time();
+
+ if ($role === 'praticien') {
+ $_SESSION['praticien_id'] = (int) $user['reference_id'];
+ } elseif ($role === 'client') {
+ $_SESSION['client_id'] = (int) $user['reference_id'];
+ }
// Redirection selon le rôle
if ($role === 'praticien') {
- header('Location: /public/espace_praticien/index.php');
+ header('Location: /public/praticien/agenda.php');
} elseif ($role === 'client') {
- header('Location: /public/espace_client/index.php');
+ header('Location: /public/client/mes_rendez_vous.php');
} else {
// fallback
header('Location: /');
@@ -124,56 +145,67 @@ if (($_POST['do'] ?? '') === 'impersonate') {
$code = $e->getMessage();
$msg = ($translations['admin_impersonate_err_default'] ?? 'Ошибка при подключении.');
- if ($code === 'CSRF') $msg = ($translations['admin_impersonate_err_csrf'] ?? 'Ошибка безопасности. Обновите страницу.');
- if ($code === 'NO_EMAIL') $msg = ($translations['admin_impersonate_err_no_email'] ?? 'Укажите e-mail практикующего или клиента.');
- if ($code === 'NOT_FOUND') $msg = ($translations['admin_impersonate_err_not_found'] ?? 'Пользователь с таким e-mail не найден.');
- if ($code === 'ROLE_FORBIDDEN') $msg = ($translations['admin_impersonate_err_role_forbidden'] ?? 'Невозможно подключиться под этим типом пользователя.');
+ if ($code === 'CSRF') {
+ $msg = ($translations['admin_impersonate_err_csrf'] ?? 'Ошибка безопасности. Обновите страницу.');
+ }
- $flash = ['type'=>'danger','msg'=>$msg];
+ if ($code === 'NO_EMAIL') {
+ $msg = ($translations['admin_impersonate_err_no_email'] ?? 'Укажите e-mail практикующего или клиента.');
+ }
+
+ if ($code === 'NOT_FOUND') {
+ $msg = ($translations['admin_impersonate_err_not_found'] ?? 'Пользователь с таким e-mail не найден.');
+ }
+
+ if ($code === 'ROLE_FORBIDDEN') {
+ $msg = ($translations['admin_impersonate_err_role_forbidden'] ?? 'Невозможно подключиться под этим типом пользователя.');
+ }
+
+ $flash = ['type' => 'danger', 'msg' => $msg];
+ }
}
-}
-// Indicateur si on est déjà en mode impersonation (admin_impersonate présent)
-$isImpersonating = !empty($_SESSION['admin_impersonate']) && is_array($_SESSION['admin_impersonate']);
+ // Indicateur si on est déjà en mode impersonation (admin_impersonate présent)
+ $isImpersonating = ! empty($_SESSION['admin_impersonate']) && is_array($_SESSION['admin_impersonate']);
?>
-
= e($translations['admin_impersonate_title'] ?? 'Подключиться как пользователь') ?>
+
-
= e($flash['msg'] ?? '') ?>
+
- = e($translations['admin_impersonate_footer'] ?? 'Все действия должны соответствовать политике конфиденциальности. Доступ к этой функции есть только у супер-администратора.') ?>
+
diff --git a/admin/tools/retour_admin.php b/admin/tools/retour_admin.php
index e0a173e..3e51513 100644
--- a/admin/tools/retour_admin.php
+++ b/admin/tools/retour_admin.php
@@ -1,26 +1,38 @@
li,
+.menu-panel .dropdown-item {
+ font-size: 16px;
+ line-height: 1.25;
+ white-space: nowrap;
+ cursor: pointer;
+ transition: background 0.2s ease;
+}
+
+/* séparateurs entre blocs du menu principal */
+.menu-panel > li + li,
+.menu-panel .dropdown-item + .dropdown-item {
+ border-top: 1px solid #f1f1f1;
+}
+
+.menu-panel > li:hover,
+.menu-panel .dropdown-item:hover {
+ background: #f8f9fa;
+}
+
+.menu-panel a {
+ color: inherit;
+ text-decoration: none;
+ display: block;
+ cursor: pointer;
+}
+
+/* ==== MENU CUSTOM ==== */
.menu-wrap {
position: relative;
- display: inline-block;
}
+
.dropdown-menu-custom {
position: absolute;
top: calc(100% + 8px);
right: 0;
- background: #fff;
- border: 1px solid #dee2e6;
- border-radius: 8px;
list-style: none;
- padding: 0;
margin: 0;
- width: 230px;
- box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
display: none;
z-index: 1100;
}
-.dropdown-menu-custom li {
+
+.dropdown-menu-custom.is-open {
+ display: block;
+}
+
+/* ==== Sous-menu interne ==== */
+.has-submenu {
+ background: #fff;
+}
+
+.has-submenu .submenu-toggle {
+ width: 100%;
+ text-align: left;
+ background: none;
+ border: 0;
padding: 12px 18px;
- border-bottom: 1px solid #f1f1f1;
+ font-size: 16px;
+ line-height: 1.25;
+ font-weight: 400;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
cursor: pointer;
- font-size: 15px;
+ transition: background 0.2s ease;
}
-.dropdown-menu-custom li:last-child {
- border-bottom: none;
-}
-.dropdown-menu-custom li:hover {
+
+.has-submenu .submenu-toggle:hover {
background: #f8f9fa;
}
+.submenu-arrow {
+ font-size: 14px;
+ opacity: 0.7;
+ transition: transform 0.2s ease;
+}
+
+.has-submenu.open .submenu-arrow {
+ transform: rotate(180deg);
+}
+
+.submenu {
+ list-style: none;
+ margin: 0;
+ padding: 0 0 8px 0;
+ background: #fff;
+}
+
+.submenu > li {
+ padding: 10px 18px 10px 32px;
+ font-size: 15px;
+ line-height: 1.25;
+ cursor: pointer;
+ transition: background 0.2s ease;
+ white-space: nowrap;
+}
+
+.submenu > li:hover {
+ background: #f8f9fa;
+}
+
+.submenu > li + li {
+ border-top: 1px solid #f7f7f7;
+}
+
+.submenu[hidden] {
+ display: none;
+}
+
+/* bouton logout */
+.menu-item-form {
+ padding: 0;
+}
+
+.menu-logout-form {
+ margin: 0;
+}
+
+.menu-logout-btn {
+ width: 100%;
+ text-align: left;
+ background: none;
+ border: 0;
+ padding: 12px 18px;
+ font-size: 16px;
+ line-height: 1.25;
+ cursor: pointer;
+}
+
+.menu-logout-btn:hover {
+ background: #f8f9fa;
+}
+
+/* sécurité overflow */
+.site-header,
+.header-actions,
+.menu-wrap,
+.dropdown {
+ overflow: visible;
+}
+
+/* bouton avatar */
+#btnMenu {
+ background: none;
+ border: none;
+ padding: 0;
+ cursor: pointer;
+ -webkit-tap-highlight-color: transparent;
+ padding-left: 10px;
+}
+
+#btnMenu:focus,
+#btnMenu:active {
+ outline: none;
+ box-shadow: none;
+}
+
/* ======================== Mobile-first (≤ 956px) ======================== */
@media (max-width: 956px) {
/* Garde-fous */
@@ -398,3 +534,34 @@
background: #38378b; /* bleu foncé */
color: #fff;
}
+
+.menu-item-form {
+ padding: 0 !important;
+ list-style: none;
+}
+
+.menu-logout-form {
+ margin: 0;
+ width: 100%;
+}
+
+.menu-logout-btn {
+ all: unset;
+ box-sizing: border-box;
+ display: block;
+ width: 100%;
+ padding: 12px 16px;
+ cursor: pointer;
+ font: inherit;
+ color: inherit;
+ text-align: left;
+}
+
+.menu-logout-btn:hover {
+ background: #f3f4f6;
+}
+
+.menu-logout-btn:focus-visible {
+ outline: 2px solid #94a3b8;
+ outline-offset: -2px;
+}
diff --git a/includes/avatar_initiales.php b/includes/avatar_initiales.php
index 170fc49..79cacf0 100644
--- a/includes/avatar_initiales.php
+++ b/includes/avatar_initiales.php
@@ -7,65 +7,87 @@
/* =========================
Helpers
========================= */
-function is_logged_in(): bool {
- $role = (string)($_SESSION['role'] ?? ($_SESSION['user_type'] ?? ''));
- if (!empty($_SESSION['reference_id']) && $role !== '') return true;
- if (!empty($_SESSION['user_id'])) return true;
- if (!empty($_SESSION['client_id'])) return true;
- if (!empty($_SESSION['praticien_id'])) return true;
- return false;
+function is_header_logged_in(): bool
+{
+ $role = (string) ($_SESSION['role'] ?? ($_SESSION['user_type'] ?? ''));
+ if (! empty($_SESSION['reference_id']) && $role !== '') {
+ return true;
+ }
+
+ if (! empty($_SESSION['user_id'])) {
+ return true;
+ }
+
+ if (! empty($_SESSION['client_id'])) {
+ return true;
+ }
+
+ if (! empty($_SESSION['praticien_id'])) {
+ return true;
+ }
+
+ return false;
}
/**
* Inclut /public/includes/header.php sans DOCTYPE/HTML/HEAD/BODY et sans recharger bootstrap/css.
*/
-function include_header_clean(string $absPath): void {
- global $translations, $lang;
- ob_start();
- include $absPath;
- $buf = ob_get_clean() ?: '';
+function include_header_clean(string $absPath): void
+{
+ global $translations, $lang;
+ require_once APP_HTDOCS . '/public/includes/securite.php';
+ ob_start();
+ include $absPath; // /public/includes/header.php
+ $buf = ob_get_clean() ?: '';
- $buf = preg_replace('~]*>~i', '', $buf);
- $buf = preg_replace('~?html\b[^>]*>~i', '', $buf);
- $buf = preg_replace('~]*>.*?~is', '', $buf);
- $buf = preg_replace('~?body\b[^>]*>~i', '', $buf);
+ // 0) DOCTYPE + …
+ $buf = preg_replace('~]*>~i', '', $buf);
+ $buf = preg_replace('~?html\b[^>]*>~i', '', $buf);
- // éviter d’écraser la home
- $buf = preg_replace('~ ]+href="[^"]*(bootstrap[^"]*\.css|mobil_style\.css|site_style\.css|fonts\.googleapis)[^"]*"[^>]*>~i', '', $buf);
- $buf = preg_replace('~~i', '', $buf);
+ // 1) retirer … et ouverts/fermés
+ $buf = preg_replace('~]*>.*?~is', '', $buf);
+ $buf = preg_replace('~?body\b[^>]*>~i', '', $buf);
- echo $buf;
+ // 2) retirer les CSS “globaux” pour éviter d’écraser la home
+ $buf = preg_replace('~ ]+href="[^"]*(bootstrap[^"]*\.css|mobil_style\.css|site_style\.css|fonts\.googleapis)[^"]*"[^>]*>~i', '', $buf);
+
+ // 3) retirer le ~i', '', $buf);
+
+ echo $buf; // garde + tes scripts inline utiles
}
-function prepare_secure_header_context(): void {
- $pdo = $GLOBALS['pdo'] ?? null;
- if (!($pdo instanceof PDO)) return;
+function prepare_secure_header_context(): void
+{
+ $pdo = $GLOBALS['pdo'] ?? null;
+ if (! ($pdo instanceof PDO)) {
+ return;
+ }
- $role = strtolower((string)($_SESSION['role'] ?? ''));
- $refId = (int)($_SESSION['reference_id'] ?? 0);
+ $role = strtolower((string) ($_SESSION['role'] ?? ''));
+ $refId = (int) ($_SESSION['reference_id'] ?? 0);
-
-
- // Initiales client/praticien
- if ($refId > 0 && ($role === 'client' || $role === 'praticien')) {
- try {
- $table = ($role === 'client') ? 'client' : 'praticien';
- $st = $pdo->prepare("SELECT nom, prenom FROM {$table} WHERE id = ? LIMIT 1");
- $st->execute([$refId]);
- $u = $st->fetch(PDO::FETCH_ASSOC) ?: [];
- $nom = trim((string)($u['nom'] ?? ''));
- $prenom = trim((string)($u['prenom'] ?? ''));
- $ini = mb_strtoupper(
- mb_substr($prenom, 0, 1, 'UTF-8') . mb_substr($nom, 0, 1, 'UTF-8'),
- 'UTF-8'
- );
- $GLOBALS['initiales'] = $ini !== '' ? $ini : 'U';
- } catch (Throwable $e) {
- $GLOBALS['initiales'] = 'U';
+ // Initiales client/praticien
+ if ($refId > 0 && ($role === 'client' || $role === 'praticien')) {
+ try {
+ $table = ($role === 'client') ? 'client' : 'praticien';
+ $st = $pdo->prepare("SELECT nom, prenom FROM {$table} WHERE id = ? LIMIT 1");
+ $st->execute([$refId]);
+ $u = $st->fetch(PDO::FETCH_ASSOC) ?: [];
+ $nom = trim((string) ($u['nom'] ?? ''));
+ $prenom = trim((string) ($u['prenom'] ?? ''));
+ $ini = mb_strtoupper(
+ mb_substr($prenom, 0, 1, 'UTF-8') . mb_substr($nom, 0, 1, 'UTF-8'),
+ 'UTF-8'
+ );
+ $GLOBALS['initiales'] = $ini !== '' ? $ini : 'U';
+ } catch (Throwable $e) {
+ $GLOBALS['initiales'] = 'U';
+ }
}
- }
}
// Si connecté -> préparer le contexte header sécurisé (sans redirection)
-$logged = is_logged_in();
-if ($logged) prepare_secure_header_context();
-
+$logged = is_header_logged_in();
+if ($logged) {
+ prepare_secure_header_context();
+}
diff --git a/index.php b/index.php
index ce3eb11..a13d619 100644
--- a/index.php
+++ b/index.php
@@ -1,6 +1,17 @@
+