Init Git Prod Mars 2026

This commit is contained in:
Oleg Barykin committed 2026-03-05 10:25:39 +01:00
commit d81efe93b1
362 files changed
+86627

No files matched your search

+12
View File
@@ -0,0 +1,12 @@
# Interdit l'accès HTTP aux fichiers sensibles
<FilesMatch "\.(php|inc|phtml|phar|env|ini|json|ya?ml|sql|bak|zip)$">
Require all denied
</FilesMatch>
# Interdit l'accès aux fichiers cachés (.env, .git, etc.)
<FilesMatch "^\.">
Require all denied
</FilesMatch>
# Pas de listing de répertoire
Options -Indexes
+58
View File
@@ -0,0 +1,58 @@
<?php
// Si quelqu'un essaie d'appeler directement /includes/db.php dans le navigateur, on coupe
if (isset($_SERVER['SCRIPT_FILENAME']) && realpath(__FILE__) === realpath($_SERVER['SCRIPT_FILENAME'])) {
http_response_code(403);
exit('
<!doctype html>
<html lang="ru"><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>403 — Accès refusé</title>
<style>
html,body{height:100%;margin:0}
body{display:flex;align-items:center;justify-content:center;background:#f7f7f9;font-family:system-ui,Segoe UI,Roboto,Arial}
.card{max-width:560px;width:92%;background:#fff;border:1px solid #e5e7eb;border-radius:14px;box-shadow:0 8px 24px rgba(0,0,0,.08);padding:28px;text-align:center}
.emoji{font-size:48px;margin-bottom:8px}
h1{margin:6px 0 10px;font-size:22px}
p{margin:0 0 18px;color:#6b7280}
.row{display:flex;gap:10px;justify-content:center;flex-wrap:wrap}
.btn{padding:10px 14px;border-radius:999px;text-decoration:none;display:inline-block}
.btn-primary{background:#E03C3F;color:#fff}
.btn-outline{border:1px solid #d1d5db;color:#fff;background:#38378B}
</style>
</head><body>
<div class="card">
<div class="emoji"></div>
<h1>Accès refusé</h1>
<p>Vous n’êtes pas autorisé à consulter cette page.</p>
<div class="row">
<a class="btn btn-primary" href="/connexion.php">Se connecter</a>
<a class="btn btn-outline" href="/">Retour à l’accueil</a>
</div>
</div>
</body></html>');
}
$host = getenv('APP_DB_HOST') ?: 'localhost';
$dbname = getenv('APP_DB_NAME') ?: '';
$user = getenv('APP_DB_USER') ?: '';
$password = getenv('APP_DB_PASS') ?: '';
$charset = getenv('APP_DB_CHARSET') ?: 'utf8mb4';
try {
$pdo = new PDO(
"mysql:host=$host;dbname=$dbname;charset=$charset",
$user,
$password,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]
);
} catch (PDOException $e) {
error_log('DB connect error: ' . $e->getMessage());
http_response_code(500);
exit('Erreur de connexion à la base de données public.');
}
?>
+616
View File
@@ -0,0 +1,616 @@
<?php
$lang = $_SESSION['lang'] ?? 'fr';
$PAGE_TITLE = $PAGE_TITLE ?? ($translations['site_name'] ?? 'uslugi.fr');
$HEAD_EXTRA = $HEAD_EXTRA ?? '';
$initiales = $initiales ?? ($GLOBALS['initiales'] ?? 'U');
$avatarSvg = $avatar_svg ?? ($GLOBALS['avatar_svg'] ?? null);
$pushCfg = $pushCfg ?? ($GLOBALS['pushCfg'] ?? []);
$alerte_abonnement = $alerte_abonnement ?? ($GLOBALS['alerte_abonnement'] ?? null);
require_once __DIR__ . '/../contact_admin.php';
// =====================================================
// [RETOUR / REFERER SÉCURISÉ]
// - Autorise le retour seulement si le referer est sur le même host
// =====================================================
$ref = $_SERVER['HTTP_REFERER'] ?? '';
$host = $_SERVER['HTTP_HOST'] ?? '';
$safeRef = '';
if ($ref) {
$u = parse_url($ref);
if (!empty($u['host']) && $u['host'] === $host) {
$safeRef = $ref;
}
}
$fallbackBack = $safeRef ?: '/';
// =====================================================
// [DETECTION MOBILE]
// - Très simple détection UA pour le logo adaptatif
// =====================================================
$is_mobile = preg_match('/Mobile|Android|iPhone|iPad/i', $_SERVER['HTTP_USER_AGENT']);
?>
<!doctype html>
<html lang="fr">
<head>
<title><?= htmlspecialchars($PAGE_TITLE, ENT_QUOTES, 'UTF-8') ?></title>
<?= $HEAD_EXTRA ?>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<!-- Police Montserrat -->
<!-- Bootstrap -->
<link href="/css/bootstrap.min.css" rel="stylesheet">
<script defer src="/js/bootstrap.bundle.min.js"></script>
<!-- Styles personnalisés -->
<link rel="stylesheet" href="<?= asset('/includes/site_style.css') ?>">
<link rel="stylesheet" href="<?= asset('/includes/custom_style.css') ?>"> <!-- ta home -->
<link rel="stylesheet" href="<?= asset('/includes/mobil_style.css') ?>">
</head>
<body>
<?php
// Pour n'afficher qu'une fois par compte (PWA)
$pushOnboardKey = 'uslugi_push_onboard_v1_' . (int)($_SESSION['compte_id'] ?? 0);
// helper traduction (si tu n'as pas déjà $t ici)
$t = function(string $k, string $fallback = '') use (&$translations): string {
$val = $translations[$k] ?? null;
return ($val === null || $val === '') ? $fallback : (string)$val;
};
?>
<div class="modal fade" id="pushOnboardModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<?= htmlspecialchars($t('push_onboard_title', 'Activer les notifications ?'), ENT_QUOTES, 'UTF-8') ?>
</h5>
<button type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="<?= htmlspecialchars($t('push_onboard_close_aria', 'Fermer'), ENT_QUOTES, 'UTF-8') ?>">
</button>
</div>
<div class="modal-body">
<?= htmlspecialchars($t('push_onboard_body', 'Vous recevrez des alertes pour les nouveaux messages et rendez-vous.'), ENT_QUOTES, 'UTF-8') ?>
</div>
<div class="modal-footer">
<button type="button"
class="btn btn-outline-secondary"
data-bs-dismiss="modal"
id="pushLaterBtn">
<?= htmlspecialchars($t('push_onboard_later', 'Plus tard'), ENT_QUOTES, 'UTF-8') ?>
</button>
<button type="button"
class="btn btn-primary"
id="pushEnableBtn">
<?= htmlspecialchars($t('push_onboard_allow', 'Autoriser'), ENT_QUOTES, 'UTF-8') ?>
</button>
</div>
</div>
</div>
</div>
<!-- ================================================
[HEADER / BARRE DE NAVIGATION]
- Bouton retour sécurisé vers $fallbackBack
- Logo adaptatif mobile/desktop
- Icône messages avec badge non lus
- Menu principal + sous-menu Admin
- Menu "?" compact (aide)
================================================ -->
<header class="site-header py-2 border-bottom">
<?php if (!empty($_SESSION['admin_impersonate'])): ?>
<div class="alert alert-warning text-center mb-0 rounded-0">
Вы вошли как другой пользователь.
<a href="/admin/tools/retour_admin.php"
class="btn btn-sm btn-danger ms-2"
style="border-radius:999px;">
Вернуться в режим администратора
</a>
</div>
<?php endif; ?>
<?php $alerte_abonnement = $GLOBALS['alerte_abonnement'] ?? null; ?>
<?php if (!empty($alerte_abonnement)): ?>
<div class="alert alert-warning m-0 py-1 px-2 rounded"
role="alert"
style="background:#F1F1F1; color:#E03C3F; font-size:14px; text-align:center;">
<?= htmlspecialchars((string)$alerte_abonnement, ENT_QUOTES, 'UTF-8') ?>
</div>
<?php endif; ?>
<div class="container d-flex justify-content-between align-items-center">
<!-- Marque + Bouton retour + Logo -->
<div class="header-left">
<div class="brand">
<!-- Bouton retour -->
<a
href="<?= htmlspecialchars($fallbackBack, ENT_QUOTES, 'UTF-8') ?>"
id="btnBack"
class="btn btn-light btn-circle me-2"
style="border-radius:5px; box-shadow:0 1px 2px rgba(0,0,0,.08);"
aria-label="<?= htmlspecialchars($translations['retour'] ?? 'Retour', ENT_QUOTES, 'UTF-8') ?>"
>
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" aria-hidden="true">
<path
d="M15 6l-6 6 6 6"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</a>
<!-- Logo -->
<a href="/index.php">
<img src="/public/uploads/img/logo_u.png"
alt="uslugi.fr"
class="logo-mobile">
<img src="/public/uploads/img/logo.png"
alt="uslugi.fr"
class="logo-desktop">
</a>
</div>
<div class="lang-switch">
<?php require __DIR__ . '/../../includes/menu_langue.php'; ?>
</div>
</div>
<!-- Actions header (avatar, messages, menu, aide) -->
<nav class="header-actions d-flex align-items-center gap-2 position-relative">
<!-- RDV (+ badge non lus) -->
<a
id="btn-rdv"
href="<?= ($_SESSION['role'] ?? '') === 'praticien'
? '/public/praticien/rendez_vous.php'
: '/public/client/mes_rendez_vous.php' ?>"
class="nav-rdv position-relative"
>
<img src="/public/uploads/img/bell-on.png" width="22" alt="Rendez-vous">
<span
id="rdv-badge"
style="position:absolute; top:-6px; right:-8px; background:#e11d48; color:#fff;
font-size:12px; min-width:18px; height:18px; line-height:18px;
text-align:center; border-radius:999px; padding:0 5px; display:none;"
>0</span>
</a>
<!-- Messages (+ badge non lus) -->
<a href="/messages/" class="nav-msg position-relative">
<img src="/public/uploads/img/message.png" width="20" alt="Messages">
<span
id="msg-badge"
style="position:absolute; top:-6px; right:-8px; background:#e11d48; color:#fff;
font-size:12px; min-width:18px; height:18px; line-height:18px;
text-align:center; border-radius:999px; padding:0 5px; display:none;"
>0</span>
</a>
<!-- Menu principal -->
<div class="menu-wrap">
<button type="button" id="btnMenu">
<div class="avatar"><?= $initiales ?></div>
</button>
<!-- Menu principal (liste) -->
<ul class="dropdown-menu-custom menu-panel" id="menu">
<?php if ($_SESSION['role'] === 'client'): ?>
<li onclick="location.href='/public/client/mes_rendez_vous.php'">
<?= htmlspecialchars($translations['menu_mes_rendez_vous']) ?>
</li>
<li onclick="location.href='/public/client/list_praticien.php'">
<?= htmlspecialchars($translations['menu_liste_specialistes']) ?>
</li>
<li onclick="location.href='/public/client/mes_favori.php'">
<?= htmlspecialchars($translations['menu_mes_favoris']) ?>
</li>
<?php else: ?>
<li onclick="location.href='/public/praticien/rendez_vous.php'">
<?= htmlspecialchars($translations['menu_mes_rendez_vous']) ?>
</li>
<li onclick="location.href='/public/praticien/agenda.php'">
<?= htmlspecialchars($translations['menu_mon_agenda']) ?>
</li>
<li onclick="location.href='/public/praticien/disponibilites.php'">
<?= htmlspecialchars($translations['menu_mes_disponibilites']) ?>
</li>
<li onclick="location.href='/public/praticien/synchronisation.php'">
<?= htmlspecialchars($translations['menu_sync_agendas']) ?>
</li>
<!--<li onclick="location.href='/public/praticien/clients/index.php'">
<?= htmlspecialchars($translations['menu_mes_clients'] ?? 'Mes clients') ?>
</li>-->
<?php endif; ?>
<!-- Sous-menu Admin -->
<li class="has-submenu">
<button
type="button"
class="submenu-toggle"
aria-expanded="false"
aria-controls="submenu-admin"
>
<?= htmlspecialchars($translations['menu_admin']) ?>
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
<path d="M6 9l6 6 6-6" fill="none" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
<ul id="submenu-admin" class="submenu" hidden>
<?php if ($_SESSION['role'] === 'praticien'): ?>
<li onclick="location.href='/public/praticien/resiliation.php'">
<?= htmlspecialchars($translations['menu_gerer_abonnement']) ?>
</li>
<li onclick="location.href='/public/praticien/factures.php'">
<?= htmlspecialchars($translations['menu_mes_factures']) ?>
</li>
<li onclick="location.href='/public/cabinet/invitations.php'">
<?= htmlspecialchars($translations['secretariat_inviter_secretaire']) ?>
</li>
<li onclick="location.href='/public/praticien/clients/index.php'">
<?= htmlspecialchars($translations['menu_mes_clients'] ?? 'Mes clients') ?>
</li>
<?php endif; ?>
<li onclick="location.href='/public/<?= $_SESSION['role'] ?>/informations.php'">
<?= htmlspecialchars($translations['menu_mes_informations']) ?>
</li>
<li onclick="location.href='/public/<?= $_SESSION['role'] ?>/compte.php'">
<?= htmlspecialchars($translations['menu_infos_connexion']) ?>
</li>
<li onclick="location.href='/supprimer_compte.php'">
<?= htmlspecialchars($translations['menu_supprimer_compte']) ?>
</li>
</ul>
</li>
<!-- Déconnexion -->
<li onclick="closeMenu(); openContactAdmin()">
<?= htmlspecialchars($translations['contact_admin']) ?>
</li>
<li onclick="location.href='/public/deconnexion.php'">
<?= htmlspecialchars($translations['menu_deconnexion']) ?>
</li>
</ul>
</div>
</nav>
</div>
</header>
<!-- ================================================
[SCRIPTS / LOGIQUE CLIENT]
- Badge messages non lus (polling)
- Ouverture/fermeture du menu principal
- Gestion du sous-menu Admin (ARIA)
- Bootstrap JS bundle
================================================ -->
<script>
(function(){
function setBadge(el, n){
if (!el) return;
n = Number(n || 0);
if (n > 0){
el.style.display = 'inline-block';
el.textContent = String(n > 99 ? '99+' : n);
} else {
el.style.display = 'none';
el.textContent = '0';
}
}
async function tickBadges(){
try {
const r = await fetch('/push/badge_count.php', { credentials:'same-origin', cache:'no-store' });
if (!r.ok) return;
const j = await r.json();
if (!j || !j.ok) return;
// UI header
setBadge(document.getElementById('msg-badge'), j.msg_unread);
setBadge(document.getElementById('rdv-badge'), j.rdv_unread);
// Badge PWA (si supporté)
const n = Number(j.app_badge || 0);
if ('setAppBadge' in navigator) {
try {
if (n > 0) await navigator.setAppBadge(n);
else if ('clearAppBadge' in navigator) await navigator.clearAppBadge();
} catch(_) {}
}
} catch(e){}
}
// 1) au chargement
tickBadges();
// 2) refresh (mets 10s au lieu de 5s pour soulager)
setInterval(tickBadges, 10000);
// 3) quand on revient sur l’onglet
document.addEventListener('visibilitychange', () => {
if (!document.hidden) tickBadges();
});
// optionnel : dispo ailleurs
window.refreshBadges = tickBadges;
})();
// -------------------------------------------------
// [MENU PRINCIPAL] — Ouverture / Fermeture
// -------------------------------------------------
function toggleMenu() {
const menu = document.getElementById('menu');
if (!menu) return;
menu.style.display = (menu.style.display === 'block') ? 'none' : 'block';
}
function closeMenu() {
const menu = document.getElementById('menu');
if (menu) menu.style.display = 'none';
closeSubmenus();
}
document.getElementById('btnMenu').addEventListener('click', toggleMenu);
// Ferme le menu en cliquant à l'extérieur
document.addEventListener('click', (e) => {
const menu = document.getElementById('menu');
if (!menu) return;
const toggle = document.getElementById('btnMenu');
if (!toggle.contains(e.target) && !menu.contains(e.target)) {
closeMenu();
}
});
// Ferme avec la touche Escape
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeMenu();
});
// -------------------------------------------------
// [SOUS-MENU ADMIN] — Toggle + ARIA
// -------------------------------------------------
document.addEventListener('click', function(e) {
const btn = e.target.closest('.submenu-toggle');
if (!btn) return;
const submenuId = btn.getAttribute('aria-controls');
const submenu = document.getElementById(submenuId);
if (!submenu) return;
const isOpen = btn.getAttribute('aria-expanded') === 'true';
btn.setAttribute('aria-expanded', String(!isOpen));
submenu.hidden = isOpen;
e.stopPropagation();
});
function closeSubmenus() {
document
.querySelectorAll('.submenu-toggle[aria-expanded="true"]')
.forEach(btn => {
btn.setAttribute('aria-expanded', 'false');
const sub = document.getElementById(btn.getAttribute('aria-controls'));
if (sub) sub.hidden = true;
});
}
</script>
<script>
(async function registerSW(){
if (!('serviceWorker' in navigator)) return;
try {
// IMPORTANT : scope racine pour couvrir /push/, /messages/, /public/...
const reg = await navigator.serviceWorker.register('/sw.js', { scope: '/' });
// optionnel : log utile
// console.log('SW registered', reg.scope);
} catch (e) {
console.error('SW register failed', e);
}
})();
</script>
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.addEventListener('message', async (e) => {
const msg = e.data || {};
if (msg.type === 'SET_BADGE') {
const n = Number(msg.badgeCount ?? 0);
// iOS/Safari: parfois pas supporté => silence
if ('setAppBadge' in navigator) {
try {
if (n > 0) await navigator.setAppBadge(n);
else if ('clearAppBadge' in navigator) await navigator.clearAppBadge();
} catch (_) {}
}
}
});
}
</script>
<script>
(async function pushOnboardingPWA(){
const isPWA =
window.matchMedia('(display-mode: standalone)').matches ||
window.navigator.standalone === true;
if (!isPWA) return;
if (!('serviceWorker' in navigator) || !('PushManager' in window)) return;
if (!('Notification' in window)) return;
const VAPID_PUBLIC = "<?= htmlspecialchars($pushCfg['vapid_public'] ?? '', ENT_QUOTES) ?>";
if (!VAPID_PUBLIC) return;
const onboardKey = "<?= htmlspecialchars($pushOnboardKey, ENT_QUOTES) ?>";
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
const raw = atob(base64);
const arr = new Uint8Array(raw.length);
for (let i = 0; i < raw.length; i++) arr[i] = raw.charCodeAt(i);
return arr;
}
async function getSub(){
const reg = await navigator.serviceWorker.ready;
return await reg.pushManager.getSubscription();
}
async function syncToServer(sub){
const res = await fetch('/push/subscribe.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
cache: 'no-store',
body: JSON.stringify({
endpoint: sub.endpoint,
keys: sub.toJSON().keys,
userAgent: navigator.userAgent,
deviceLabel: 'PWA'
})
});
if (!res.ok) throw new Error('subscribe.php failed: ' + res.status);
return true;
}
async function subscribeNow(){
const reg = await navigator.serviceWorker.ready;
// 1) permission
const perm = await Notification.requestPermission();
if (perm !== 'granted') {
// on note qu'on a déjà proposé (évite spam)
try { localStorage.setItem(onboardKey, 'done'); } catch(_) {}
return false;
}
// 2) subscribe
const sub = await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC)
});
// 3) sync DB
await syncToServer(sub);
// 4) mark done
try { localStorage.setItem(onboardKey, 'done'); } catch(_) {}
return true;
}
// --- Step 1: si sub existe déjà => sync DB silencieux ---
try {
const existing = await getSub();
if (existing) {
await syncToServer(existing);
// on marque done pour ne pas reproposer
try { localStorage.setItem(onboardKey, 'done'); } catch(_) {}
return;
}
} catch(e) {
// silencieux (pas bloquant)
}
// --- Step 2: pas de sub => afficher modal une seule fois ---
let alreadyDone = false;
try { alreadyDone = localStorage.getItem(onboardKey) === 'done'; } catch(_) {}
// Si l'utilisateur a déjà refusé au niveau OS => pas de modal (ou tu peux afficher un toast)
if (Notification.permission === 'denied') {
try { localStorage.setItem(onboardKey, 'done'); } catch(_) {}
return;
}
// Si déjà proposé => stop
if (alreadyDone) return;
// Affiche modal (Bootstrap)
const modalEl = document.getElementById('pushOnboardModal');
if (!modalEl || typeof bootstrap === 'undefined') return;
const modal = new bootstrap.Modal(modalEl, { backdrop: 'static' });
const laterBtn = document.getElementById('pushLaterBtn');
const enableBtn = document.getElementById('pushEnableBtn');
if (laterBtn) {
laterBtn.addEventListener('click', () => {
try { localStorage.setItem(onboardKey, 'done'); } catch(_) {}
modal.hide();
}, { once:true });
}
if (enableBtn) {
enableBtn.addEventListener('click', async () => {
enableBtn.disabled = true;
try {
await subscribeNow();
} catch(e) {
// si échec (réseau / serveur), on ne spam pas l'utilisateur
try { localStorage.setItem(onboardKey, 'done'); } catch(_) {}
} finally {
modal.hide();
}
}, { once:true });
}
modal.show();
})();
</script>
<script>
window.applyBadges = function (data) {
const msgUnread = Number(data?.msg_unread || 0);
const rdvUnread = Number(data?.rdv_unread || 0);
const appBadge = Number(data?.app_badge || (msgUnread + rdvUnread));
const msgSpan = document.getElementById('msg-badge');
const rdvSpan = document.getElementById('rdv-badge');
if (msgSpan){
msgSpan.style.display = msgUnread > 0 ? 'inline-block' : 'none';
msgSpan.textContent = msgUnread > 99 ? '99+' : String(msgUnread);
}
if (rdvSpan){
rdvSpan.style.display = rdvUnread > 0 ? 'inline-block' : 'none';
rdvSpan.textContent = rdvUnread > 99 ? '99+' : String(rdvUnread);
}
if ('setAppBadge' in navigator) {
try {
if (appBadge > 0) navigator.setAppBadge(appBadge);
else navigator.clearAppBadge?.();
} catch(_) {}
}
};
</script>
+231
View File
@@ -0,0 +1,231 @@
<?php
// ⚠️ NE PAS inclure contact_admin.php ici (risque d'exécution à l'inclusion)
require_once __DIR__ . '/../contact_admin.php';
$sec_uid = (int)($_SESSION['compte_id'] ?? $_SESSION['user_id'] ?? 0);
// =====================================================
// [CONTEXT UTILISATEUR] — Initiales avatar secrétaire
// =====================================================
$prenomUser = trim((string)($_SESSION['user_prenom'] ?? $_SESSION['prenom'] ?? ''));
$nomUser = trim((string)($_SESSION['user_nom'] ?? $_SESSION['nom'] ?? ''));
if ($prenomUser === '' && $nomUser === '') {
// petit fallback
$prenomUser = 'Secr';
$nomUser = 'Etaire';
}
$initiales = mb_strtoupper(
mb_substr($prenomUser, 0, 1, 'UTF-8') .
mb_substr($nomUser, 0, 1, 'UTF-8'),
'UTF-8'
);
// =====================================================
// [PRATICIEN COURANT] — affichage nom/prénom dans le header
// - on prend ?praticien_id si présent, sinon une valeur de session si dispo
// =====================================================
$currentPraticienId = null;
if (isset($_GET['praticien_id'])) {
$currentPraticienId = (int)$_GET['praticien_id'];
$_SESSION['current_praticien_id'] = $currentPraticienId; // mémorise pour les autres pages
} elseif (!empty($_SESSION['current_praticien_id'])) {
$currentPraticienId = (int)$_SESSION['current_praticien_id'];
}
$currentPraticienName = '';
if ($currentPraticienId) {
$stmt = $pdo->prepare("SELECT id, prenom, nom FROM praticien WHERE id = ? LIMIT 1");
$stmt->execute([$currentPraticienId]);
if ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$p = trim((string)($row['prenom'] ?? ''));
$n = trim((string)($row['nom'] ?? ''));
$pid = trim((string)($row['id'] ?? ''));
$currentPraticienName = trim($p.' '.$n);
}
}
// =====================================================
// [Liste Praticien avec autorisations]
// =====================================================
$pid = (int)$currentPraticienId;
$permRow = null;
if ($pid && $sec_uid) {
$q = $pdo->prepare("
SELECT sp.can_manage_rdv, sp.can_edit_slots
FROM secretaire_praticien sp
WHERE sp.secretaire_user_id = :u
AND sp.praticien_id = :pid
AND sp.is_active = 1
LIMIT 1
");
$q->execute(['u' => $sec_uid, 'pid' => $pid]);
$permRow = $q->fetch(PDO::FETCH_ASSOC) ?: null;
}
// =====================================================
// [RETOUR / REFERER SÉCURISÉ]
// =====================================================
$ref = $_SERVER['HTTP_REFERER'] ?? '';
$host = $_SERVER['HTTP_HOST'] ?? '';
$safeRef = '';
if ($ref) {
$u = parse_url($ref);
if (!empty($u['host']) && $u['host'] === $host) $safeRef = $ref;
}
$fallbackBack = $safeRef ?: '/public/cabinet/dashboard.php';
// =====================================================
// [DETECTION MOBILE]
// =====================================================
$is_mobile = preg_match('/Mobile|Android|iPhone|iPad/i', $_SERVER['HTTP_USER_AGENT'] ?? '');
?>
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title><?= htmlspecialchars($PAGE_TITLE ?? 'Cabinet', ENT_QUOTES, 'UTF-8') ?></title>
<?= $HEAD_EXTRA ?? '' ?>
<!-- Montserrat -->
<!-- Bootstrap -->
<link href="/css/bootstrap.min.css" rel="stylesheet">
<!-- Styles (chemins corrigés) -->
<link rel="stylesheet" href="<?= asset('/includes/site_style.css') ?>">
<link rel="stylesheet" href="<?= asset('/includes/custom_style.css') ?>"> <!-- ta home -->
<link rel="stylesheet" href="<?= asset('/includes/admin_style.css') ?>">
<link rel="stylesheet" href="<?= asset('/includes/mobil_style.css') ?>">
</head>
<body>
<header class="site-header py-2 border-bottom">
<div class="container d-flex justify-content-between align-items-center">
<!-- Bouton retour + Logo -->
<div class="header-left">
<div class="brand">
<a
href="<?= htmlspecialchars($fallbackBack, ENT_QUOTES, 'UTF-8') ?>"
id="btnBack"
class="btn btn-light btn-circle me-2"
style="border-radius:5px; box-shadow:0 1px 2px rgba(0,0,0,.08);"
aria-label="<?= htmlspecialchars($translations['retour'] ?? 'Retour', ENT_QUOTES, 'UTF-8') ?>"
>
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" aria-hidden="true">
<path d="M15 6l-6 6 6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</a>
<a href="/public/cabinet/dashboard.php">
<img src="/public/uploads/img/logo_u.png"
alt="uslugi.fr"
class="logo-mobile">
<img src="/public/uploads/img/logo.png"
alt="uslugi.fr"
class="logo-desktop">
</a>
</div>
<div class="lang-switch">
<?php require __DIR__ . '/../../includes/menu_langue.php'; ?>
</div>
</div>
<!-- Actions header -->
<nav class="header-actions d-flex align-items-center gap-3 position-relative">
<!-- ⚡ A LA PLACE DE L’ICÔNE MESSAGES : le praticien courant -->
<div class="text-nowrap" style="font-weight:600;">
<?php if ($currentPraticienName): ?>
<?= htmlspecialchars($currentPraticienName, ENT_QUOTES, 'UTF-8') ?>
<?php else: ?>
<span class="text-muted"><?= htmlspecialchars($translations['aucun_praticien_selectionne'] ?? 'Praticien non sélectionné', ENT_QUOTES, 'UTF-8') ?></span>
<?php endif; ?>
</div>
<!-- Menu utilisateur (avatar) -->
<div class="menu-wrap">
<button type="button" id="btnMenu" class="btn p-0 border-0 bg-transparent">
<div class="avatar d-flex align-items-center justify-content-center">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 12a5 5 0 1 0-5-5 5 5 0 0 0 5 5m0 2c-5.33 0-8 2.67-8 4v2h16v-2c0-1.33-2.67-4-8-4"/>
</svg>
</div>
</button>
<ul class="dropdown-menu-custom menu-panel" id="menu" style="display:none; min-width: 220px;">
<?php if (($_SESSION['role'] ?? '') === 'secretaire'): ?>
<li onclick="location.href='/public/cabinet/dashboard.php'">
<?= htmlspecialchars($translations['home'] ?? 'Accueil') ?>
</li>
<?php if ($pid && $permRow): ?>
<?php if (!empty($permRow['can_manage_rdv'])): ?>
<li onclick="location.href='/public/cabinet/agenda.php?praticien_id=<?= $pid ?>'">
<?= htmlspecialchars($translations['sec_btn_agenda'] ?? 'Agenda') ?>
</li>
<li onclick="location.href='/public/cabinet/rendez_vous_du_jour.php?praticien_id=<?= $pid ?>'">
<?= htmlspecialchars($translations['sec_btn_rdv'] ?? 'Rendez-vous') ?>
</li>
<li onclick="location.href='/public/cabinet/rendez_vous.php?praticien_id=<?= $pid ?>'">
<?= htmlspecialchars($translations['sec_btn_rdv_jour'] ?? 'Les rendez-vous') ?>
</li>
<li onclick="location.href='/public/cabinet/reserver_rdv.php?praticien_id=<?= $pid ?>'">
<?= htmlspecialchars($translations['sec_btn_prendre'] ?? 'Prendre un RDV') ?>
</li>
<?php endif; ?>
<?php if (!empty($permRow['can_edit_slots'])): ?>
<li onclick="location.href='/public/cabinet/disponibilites.php?praticien_id=<?= $pid ?>'">
<?= htmlspecialchars($translations['sec_btn_dispos'] ?? 'Disponibilités') ?>
</li>
<?php endif; ?>
<?php endif; ?>
<li onclick="closeMenu(); openContactAdmin()">
<?= htmlspecialchars($translations['contact_admin'] ?? 'Contact Admin') ?>
</li>
<li onclick="location.href='/public/deconnexion.php'">
<?= htmlspecialchars($translations['menu_deconnexion'] ?? 'Déconnexion') ?>
</li>
<?php endif; ?>
</ul>
</div>
</nav>
</div>
</header>
<script>
// --- Menu principal
function toggleMenu() {
const menu = document.getElementById('menu');
if (!menu) return;
menu.style.display = (menu.style.display === 'block') ? 'none' : 'block';
}
function closeMenu() {
const menu = document.getElementById('menu');
if (menu) menu.style.display = 'none';
}
document.getElementById('btnMenu')?.addEventListener('click', function(e){
e.stopPropagation();
toggleMenu();
});
document.addEventListener('click', function(e){
const menu = document.getElementById('menu');
const btn = document.getElementById('btnMenu');
if (!menu || !btn) return;
if (!btn.contains(e.target) && !menu.contains(e.target)) closeMenu();
});
document.addEventListener('keydown', function(e){ if (e.key === 'Escape') closeMenu(); });
</script>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
+240
View File
@@ -0,0 +1,240 @@
<?php
require_once __DIR__ . '/securite.php';
require_once __DIR__ . '/db.php';
require_once __DIR__ . '/verifier_abonnement.php';
require_once __DIR__ . '/../contact_admin.php'; // ← depuis /public/includes/header.php
require_once __DIR__ . '/../includes/lang.php';
require_once __DIR__ . '/../../includes/pretty_urls.php';
require_once __DIR__ . '/../../includes/mailer.php'; // uslugi_mail()
if (!isset($_SESSION)) session_start();
// Charger les données client ou praticien
$nom = $prenom = '';
if ($_SESSION['role'] === 'client') {
$stmt = $pdo->prepare("SELECT nom, prenom FROM client WHERE id = ?");
} else {
$stmt = $pdo->prepare("SELECT nom, prenom FROM praticien WHERE id = ?");
}
$stmt->execute([$_SESSION['reference_id']]);
$user = $stmt->fetch();
if ($user) {
$nom = htmlspecialchars($user['nom']);
$prenom = htmlspecialchars($user['prenom']);
}
//Initiales pour Avatar
$initiales = mb_strtoupper(
mb_substr((string)$prenom, 0, 1, 'UTF-8') .
mb_substr((string)$nom, 0, 1, 'UTF-8'),
'UTF-8'
);
//Function pour gerer hystorique de la navigation
//*************************************************
$ref = $_SERVER['HTTP_REFERER'] ?? '';
$host = $_SERVER['HTTP_HOST'] ?? '';
$safeRef = '';
if ($ref) {
$u = parse_url($ref);
if (!empty($u['host']) && $u['host'] === $host) {
$safeRef = $ref; // referer même origine uniquement
}
}
// Mets ici ta route par défaut si pas d'historique / referer externe
$fallbackBack = $safeRef ?: '/';
//********************FIN********************************
?>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<link href="/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="../../includes/site_style.css">
<link rel="stylesheet" href="../../includes/mobil_style.css">
</head>
<body>
<header class="site-header">
<div class="container">
<div class="brand">
<!--Bouton hystory qui fait retour en arriere-->
<a href="<?= htmlspecialchars($fallbackBack, ENT_QUOTES, 'UTF-8') ?>"
id="btnBack"
class="btn btn-light btn-circle me-2" style="border-radius: 5px; box-shadow: 0 1px 2px rgba(0,0,0,.08);"
aria-label="<?= htmlspecialchars($translations['retour'] ?? 'Retour', ENT_QUOTES, 'UTF-8') ?>">
<!-- inline SVG, compatible partout -->
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" aria-hidden="true">
<path d="M15 6l-6 6 6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</a>
<!-- *************************FIN*************************-->
<a href="https://uslugi.fr/index.php"><img src="/public/uploads/img/logo.png" width="130"></a>
</div>
<?php if (isset($alerte_abonnement) && $alerte_abonnement): ?>
<div class="alert alert-warning m-0 py-1 px-2 rounded text-dark" role="alert" style="background:#fff3cd; border: 1px solid #ffeeba; font-size: 14px;">
<?= htmlspecialchars($alerte_abonnement) ?>
</div>
<?php endif; ?>
<nav class="header-actions">
<div class="avatar"><?= $initiales ?></div>
<!--**************************Bouton message conv*************************************************-->
<a href="/messages/" class="nav-msg" style="position:relative; display:inline-block; margin-left:12px;">
<span aria-hidden="true"><img src="/public/uploads/img/message.png" width="20px"></span>
<span id="msg-badge" style="
position:absolute; top:-6px; right:-8px;
background:#e11d48; color:#fff; font-size:12px; min-width:18px; height:18px; line-height:18px;
text-align:center; border-radius:999px; padding:0 5px; display:none;">0</span>
</a>
<script>
(function(){
async function tick(){
try{
const r = await fetch('/messages/api/unread_count.php', { credentials:'same-origin' });
if(!r.ok) throw 0;
const j = await r.json();
const b = document.getElementById('msg-badge');
const n = Number(j.total||0);
if(n>0){ b.style.display='inline-block'; b.textContent = String(n); }
else { b.style.display='none'; }
}catch(e){}
}
tick();
setInterval(tick, 5000);
})();
</script>
<!--*************************************FIN BOUTON**********************************************
KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK-->
<div class="lang-switch">
<?php require_once __DIR__ .'/../../includes/menu_langue.php'; ?>
</div>
<div class="menu-wrap">
<button
type="button"
id="btnMenu"
class="btn btn-light btn-circle me-2" style="border-radius: 5px; box-shadow: 0 1px 2px rgba(0,0,0,.08);"
aria-label="<?= htmlspecialchars($translations['menu'] ?? 'Menu', ENT_QUOTES, 'UTF-8') ?>"
onclick="toggleMenu()">
<!-- icône hamburger 20x20, même style que la flèche -->
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" aria-hidden="true">
<path d="M4 6h16M4 12h16M4 18h16"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<!-- Menu -->
<ul class="dropdown-menu-custom" id="menu">
<li onclick="location.href='/public/<?= $_SESSION['role'] ?>/informations.php'">
<?= htmlspecialchars($translations['menu_mes_informations']) ?>
</li>
<li onclick="location.href='/public/<?= $_SESSION['role'] ?>/compte.php'">
<?= htmlspecialchars($translations['menu_infos_connexion']) ?>
</li>
<?php if ($_SESSION['role'] === 'client'): ?>
<li onclick="location.href='/public/client/list_praticien.php'">
<?= htmlspecialchars($translations['menu_liste_specialistes']) ?>
</li>
<li onclick="location.href='/public/client/mes_favori.php'">
<?= htmlspecialchars($translations['menu_mes_favoris']) ?>
</li>
<li onclick="location.href='/public/client/mes_rendez_vous.php'">
<?= htmlspecialchars($translations['menu_mes_rendez_vous']) ?>
</li>
<?php else: ?>
<li onclick="location.href='/public/praticien/disponibilites.php'">
<?= htmlspecialchars($translations['menu_mes_disponibilites']) ?>
</li>
<li onclick="location.href='/public/praticien/rendez_vous.php'">
<?= htmlspecialchars($translations['menu_mes_rendez_vous']) ?>
</li>
<li onclick="location.href='/public/praticien/agenda.php'">
<?= htmlspecialchars($translations['menu_mon_agenda']) ?>
</li>
<li onclick="location.href='/public/praticien/synchronisation.php'">
<?= htmlspecialchars($translations['menu_sync_agendas']) ?>
</li>
<li onclick="location.href='/public/praticien/resiliation.php'">
<?= htmlspecialchars($translations['menu_gerer_abonnement']) ?>
</li>
<li onclick="location.href='/public/praticien/factures.php'">
<?= htmlspecialchars($translations['menu_mes_factures']) ?>
</li>
<?php endif; ?>
<li onclick="closeMenu(); openContactAdmin()" class="text-primary">
<?= htmlspecialchars($translations['contact_admin']) ?>
</li>
<li onclick="location.href='/public/deconnexion.php'" class="text-danger">
<?= htmlspecialchars($translations['menu_deconnexion']) ?>
</li>
</ul>
</div>
</nav>
</div>
</header>
<!-- JS -->
<script>
function toggleMenu() {
const menu = document.getElementById('menu');
if (!menu) return;
menu.style.display = (menu.style.display === 'block') ? 'none' : 'block';
}
document.addEventListener('click', function(e) {
const toggle = document.getElementById('btnMenu');
const menu = document.getElementById('menu');
if (!menu) return;
const clickOnToggle = toggle ? toggle.contains(e.target) : false;
const clickInMenu = menu.contains(e.target);
if (!clickOnToggle && !clickInMenu) {
menu.style.display = 'none';
}
});
</script>
<script>
// --- déjà présent ---
// function toggleMenu() { ... }
// document.addEventListener('click', ...);
// 1) Petite utilitaire pour fermer le menu
function closeMenu() {
const menu = document.getElementById('menu');
if (menu) menu.style.display = 'none';
// (optionnel) retire un flag si tu l'ajoutes ailleurs
document.body.classList.remove('menu-open');
}
// 2) Si ta popup est ouverte par une fonction JS (ex: openContactAdmin)
// on injecte la fermeture du menu AVANT l’ouverture
// -> Si openContactAdmin existe, on la “wrap”
if (typeof window.openContactAdmin === 'function') {
const _origOpenContactAdmin = window.openContactAdmin;
window.openContactAdmin = function(...args){
closeMenu();
return _origOpenContactAdmin.apply(this, args);
};
}
// 3) Bonus : touche Échap ferme le menu
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeMenu();
});
</script>
+15
View File
@@ -0,0 +1,15 @@
<?php
//init_admin.php
declare(strict_types=1);
require_once __DIR__ . '/init_auth.php';
init_auth_page(['admin']);
// ensuite, renfort strict admin (optionnel mais recommandé)
require_login_or_redirect(['admin'], [
'pdo' => $GLOBALS['pdo'] ?? null,
'recheck_db_role' => true, // recheck DB comme secure_admin.php si besoin
'admin_strict_reference' => 88888888 // ton super-admin
]);
// CSRF ready
ensure_csrf_token();
+64
View File
@@ -0,0 +1,64 @@
<?php
//init_auth.php
declare(strict_types=1);
require_once __DIR__ . '/../../../includes/bootstrap_global.php';
require_once __DIR__ . '/securite.php';
function init_auth_page(?array $roles = null): void
{
require_login_or_redirect($roles);
// Push config (si utilisé dans header site)
$pushCfgPath = __DIR__ . '/../../push/config.php'; // /public/push/config.php
$GLOBALS['pushCfg'] = is_file($pushCfgPath) ? require $pushCfgPath : [];
// Abonnement (si tu l’utilises dans le header site praticien)
$abonnementPath = __DIR__ . '/verifier_abonnement.php';
if (is_file($abonnementPath)) {
require $abonnementPath;
$GLOBALS['alerte_abonnement'] = $alerte_abonnement ?? null;
} else {
$GLOBALS['alerte_abonnement'] = null;
}
// Avatars
$GLOBALS['initiales'] = 'U';
$GLOBALS['avatar_svg'] = null;
$role = strtolower((string)($_SESSION['role'] ?? ''));
// Admin / Secrétaire => SVG
if ($role === 'admin') {
$GLOBALS['avatar_svg'] = '/public/uploads/img/avatar_admin.svg';
return;
}
if ($role === 'secretaire') {
$GLOBALS['avatar_svg'] = '/public/uploads/img/avatar_secretaire.svg';
return;
}
// Client / Praticien => initiales depuis tables
$refId = (int)($_SESSION['reference_id'] ?? 0);
$pdo = $GLOBALS['pdo'] ?? null;
if (!($pdo instanceof PDO) || $refId <= 0) return;
if ($role === 'client') {
$stmt = $pdo->prepare("SELECT nom, prenom FROM client WHERE id = ?");
} elseif ($role === 'praticien') {
$stmt = $pdo->prepare("SELECT nom, prenom FROM praticien WHERE id = ?");
} else {
return;
}
$stmt->execute([$refId]);
$u = $stmt->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';
}
+4
View File
@@ -0,0 +1,4 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/init_auth.php';
init_auth_page(['client']);
+4
View File
@@ -0,0 +1,4 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/init_auth.php';
init_auth_page(['client', 'praticien']);
+4
View File
@@ -0,0 +1,4 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/init_auth.php';
init_auth_page(['praticien']);
+4
View File
@@ -0,0 +1,4 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/init_auth.php';
init_auth_page(['secretaire']);
+36
View File
@@ -0,0 +1,36 @@
<?php
// Définir la langue par défaut
if (!isset($_SESSION['lang'])) {
$_SESSION['lang'] = 'ru'; // par défaut russe
}
// Charger le fichier de langue correspondant
$lang = $_SESSION['lang'];
$lang_file = __DIR__ . "/../lang/{$lang}.php";
// Si le fichier n'existe pas, on repasse en russe
if (!file_exists($lang_file)) {
$lang_file = __DIR__ . "/../lang/ru.php";
}
// Charger les traductions
$translations = require $lang_file;
/**
* Asset helper: ajoute une version ?v= basée sur filemtime()
* Usage: asset('/includes/custom_style.css')
*/
if (!function_exists('asset')) {
function asset(string $webPath): string
{
// Normalise
if ($webPath === '') return $webPath;
if ($webPath[0] !== '/') $webPath = '/' . $webPath;
$docRoot = rtrim($_SERVER['DOCUMENT_ROOT'] ?? '', '/');
$fsPath = $docRoot . $webPath;
$v = (is_file($fsPath) ? filemtime($fsPath) : time()); // fallback si non trouvé
return $webPath . '?v=' . $v;
}
}
+27
View File
@@ -0,0 +1,27 @@
(function(){
function initMobileCards(tbl){
if (!tbl) return;
const heads = Array.from(tbl.querySelectorAll('thead th')).map(th => th.textContent.trim());
tbl.querySelectorAll('tbody tr').forEach(tr => {
Array.from(tr.children).forEach((td, i) => {
if (!td.hasAttribute('data-label')) {
td.setAttribute('data-label', heads[i] || '');
}
});
});
// ✅ une seule source de vérité
document.documentElement.classList.add('mc-ready');
}
function ready(fn){
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn);
else fn();
}
ready(function(){
const tbl = document.getElementById('table_rdv') || document.querySelector('table.mobile-cards');
initMobileCards(tbl);
});
})();
+175
View File
@@ -0,0 +1,175 @@
<?php
declare(strict_types=1);
// Interdit l’accès direct au fichier (pas de HTML ici)
if (isset($_SERVER['SCRIPT_FILENAME']) && realpath(__FILE__) === realpath($_SERVER['SCRIPT_FILENAME'])) {
http_response_code(403);
exit('Forbidden');
}
/** Est-ce que l'utilisateur est connecté ? */
function is_logged_in(): bool {
return isset($_SESSION['compte_id'], $_SESSION['reference_id'])
&& $_SESSION['compte_id'] !== ''
&& $_SESSION['reference_id'] !== '';
}
/** Timeout inactivité (2h) */
function enforce_session_timeout(int $maxIdleSeconds = 7200): void {
$now = time();
$last = (int)($_SESSION['last_activity'] ?? 0);
if ($last > 0 && ($now - $last) > $maxIdleSeconds) {
logout_and_redirect_to_login($_SERVER['REQUEST_URI'] ?? '/');
}
$_SESSION['last_activity'] = $now;
}
/** Anti open-redirect : n’autorise que les chemins internes commençant par '/' */
function next_safe(?string $u): ?string {
if (!$u) return null;
if (!preg_match('#^/[^\\r\\n]*$#', $u)) return null;
if (strpos($u, '//') === 0) return null;
return $u;
}
function redirect_forbidden(?string $requestUri = null): void {
$u = $requestUri ?: ($_SERVER['REQUEST_URI'] ?? '/');
$u = next_safe($u) ?? '/';
header('Location: /403.php?next=' . rawurlencode($u), true, 303);
exit;
}
function redirect_to_login_with_next(?string $requestUri = null): void {
$u = $requestUri ?: ($_SERVER['REQUEST_URI'] ?? '/');
$u = next_safe($u) ?? '/';
$_SESSION['redirect_to'] = $u;
header('Location: /connexion.php?next=' . rawurlencode($u), true, 303);
exit;
}
function logout_and_redirect_to_login(?string $requestUri = null): void {
$_SESSION = [];
if (session_status() === PHP_SESSION_ACTIVE) {
$params = session_get_cookie_params();
setcookie(
session_name(),
'',
time() - 42000,
$params['path'] ?? '/',
$params['domain'] ?? '',
(bool)($params['secure'] ?? false),
(bool)($params['httponly'] ?? true)
);
}
if (session_status() === PHP_SESSION_ACTIVE) {
session_destroy();
}
redirect_to_login_with_next($requestUri ?? ($_SERVER['REQUEST_URI'] ?? '/'));
}
/**
* 🔒 Re-check rôle depuis DB (optionnel) + hydrate $_SESSION['role'] si absent.
* Retourne le rôle (string) ou '' si introuvable.
*/
function refresh_role_from_db(PDO $pdo): string {
$compteId = (int)($_SESSION['compte_id'] ?? 0);
if ($compteId <= 0) return '';
$stmt = $pdo->prepare("SELECT role FROM compte WHERE id=? LIMIT 1");
$stmt->execute([$compteId]);
$role = (string)($stmt->fetchColumn() ?: '');
if ($role !== '') {
$_SESSION['role'] = $role;
}
return strtolower($role);
}
/**
* ✅ À appeler en haut des pages protégées.
* - $allowedRoles = null => tous rôles
* - options:
* - 'pdo' => PDO (pour re-check DB si demandé)
* - 'recheck_db_role' => true/false
* - 'admin_strict_reference' => int|null (ex: 88888888)
*/
function require_login_or_redirect(?array $allowedRoles = null, array $options = []): void {
if (!is_logged_in()) {
redirect_to_login_with_next($_SERVER['REQUEST_URI'] ?? '/');
}
enforce_session_timeout(7200);
$pdo = $options['pdo'] ?? ($GLOBALS['pdo'] ?? null);
$recheck = (bool)($options['recheck_db_role'] ?? false);
// Rôle: session d'abord, DB si demandé (ou si role manquant)
$userRole = strtolower((string)($_SESSION['role'] ?? ''));
if (($userRole === '' || $recheck) && ($pdo instanceof PDO)) {
$userRole = refresh_role_from_db($pdo);
}
if ($allowedRoles) {
$allowed = array_map(static fn($r) => strtolower((string)$r), $allowedRoles);
if (!in_array($userRole, $allowed, true)) {
redirect_forbidden($_SERVER['REQUEST_URI'] ?? '/');
}
// 🔥 Renfort admin optionnel : role + reference_id
if (in_array('admin', $allowed, true)) {
$strictRef = $options['admin_strict_reference'] ?? null;
if ($strictRef !== null) {
$refId = (int)($_SESSION['reference_id'] ?? 0);
if ($refId !== (int)$strictRef) {
redirect_forbidden($_SERVER['REQUEST_URI'] ?? '/');
}
}
}
}
}
/* =========================
Admin helpers (CSRF + logs)
========================= */
/** Token CSRF session */
function ensure_csrf_token(): string {
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return (string)$_SESSION['csrf_token'];
}
/** Exige POST + CSRF (supporte aussi header X-CSRF-Token si tu veux fetch/AJAX) */
function require_post_csrf(): void {
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
http_response_code(405);
exit('Méthode non autorisée');
}
$token = $_POST['csrf_token'] ?? '';
if ($token === '' && !empty($_SERVER['HTTP_X_CSRF_TOKEN'])) {
$token = (string)$_SERVER['HTTP_X_CSRF_TOKEN'];
}
if ($token === '' || !hash_equals((string)($_SESSION['csrf_token'] ?? ''), (string)$token)) {
http_response_code(419);
exit('CSRF invalide');
}
}
/** Log admin minimal */
function log_admin_action(PDO $pdo, int $userId, string $action, string $details = ''): void {
// limite simple pour éviter d'exploser la table
if (strlen($details) > 2000) $details = substr($details, 0, 2000);
$pdo->prepare(
"INSERT INTO admin_actions_log (user_id, action, details, ip) VALUES (?,?,?,?)"
)->execute([
$userId,
$action,
$details,
$_SERVER['REMOTE_ADDR'] ?? ''
]);
}
+102
View File
@@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
/**
* /public/includes/verifier_abonnement.php
*
* Rôle: praticien uniquement
* - Définit $alerte_abonnement (string|null) pour affichage dans header
* - Peut rediriger si abonnement expiré (selon règles)
*
* Pré-requis: session démarrée, $pdo dispo (via bootstrap_global.php / init_auth.php)
*/
$alerte_abonnement = null;
// --- Guard: uniquement praticien
$role = (string)($_SESSION['role'] ?? '');
if ($role !== 'praticien') {
return;
}
// --- PDO
$pdo = $GLOBALS['pdo'] ?? null;
if (!($pdo instanceof PDO)) {
return;
}
// --- IDs
$praticienId = (int)($_SESSION['reference_id'] ?? 0);
if ($praticienId <= 0) {
return;
}
// --- Traductions (fallbacks)
$translations = $GLOBALS['translations'] ?? [];
$t_expire = (string)($translations['alerte_abonnement_expire'] ?? "Votre abonnement est expiré.");
$t_resilie = (string)($translations['alerte_abonnement_resilie'] ?? "Votre abonnement est résilié. Il se termine le {date}.");
// --- Récupère abonnement
$stmt = $pdo->prepare("SELECT abonnement_id, date_expiration FROM praticien WHERE id = ?");
$stmt->execute([$praticienId]);
$p = $stmt->fetch(PDO::FETCH_ASSOC) ?: [];
$abonnement_id = $p['abonnement_id'] ?? null;
// date_expiration -> timestamp (0 si absent/invalide)
$expiration_ts = 0;
if (!empty($p['date_expiration'])) {
$ts = strtotime((string)$p['date_expiration']);
$expiration_ts = ($ts !== false) ? $ts : 0;
}
$now_ts = time();
// --- Pages autorisées même si expiré
$page_actuelle = basename((string)($_SERVER['PHP_SELF'] ?? ''));
$pages_autorisees = [
'choisir_abonnement.php',
'souscrire_abonnement.php',
'resiliation.php',
'resiliation_action.php',
'deconnexion.php',
'paiement_simule.php',
'create_checkout.php',
];
// --- État abonnement
$is_expired = ($expiration_ts > 0 && $expiration_ts < $now_ts);
// ⚠️ Cas "résilié mais encore actif": abonnement_id NULL mais expiration future
$is_cancelled_but_active = (!$abonnement_id && $expiration_ts >= $now_ts && $expiration_ts > 0);
// --- Bannière (affichage)
if ($is_cancelled_but_active) {
$alerte_abonnement = strtr($t_resilie, [
'{date}' => date('d/m/Y', $expiration_ts),
]);
} elseif ($is_expired) {
$alerte_abonnement = $t_expire;
}
// (Optionnel) expose aussi un état global utile
$GLOBALS['abonnement_state'] = [
'abonnement_id' => $abonnement_id,
'expiration_ts' => $expiration_ts,
'is_expired' => $is_expired,
'is_cancelled_but_active' => $is_cancelled_but_active,
];
// --- Redirection si expiré (uniquement si expiration connue)
if ($is_expired && !in_array($page_actuelle, $pages_autorisees, true)) {
// Important: éviter "headers already sent"
if (!headers_sent()) {
header('Location: /public/choisir_abonnement.php', true, 302);
exit;
}
// Si headers déjà envoyés, on ne peut pas header() -> fallback JS
echo '<script>location.href="/public/choisir_abonnement.php";</script>';
exit;
}