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(): void { $_SESSION = []; if (session_status() === PHP_SESSION_ACTIVE) { $params = session_get_cookie_params(); setcookie(session_name(), '', [ 'expires' => time() - 42000, 'path' => $params['path'] ?? '/', 'domain' => $params['domain'] ?? '', 'secure' => (bool) ($params['secure'] ?? false), 'httponly' => (bool) ($params['httponly'] ?? true), 'samesite' => $params['samesite'] ?? 'Lax', ]); session_destroy(); } header('Location: /index.php', true, 303); exit; } /** * 🔒 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'] ?? '', ]); }