exec(" CREATE TABLE IF NOT EXISTS admin_remember_tokens ( id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, admin_id INT UNSIGNED NOT NULL, selector CHAR(24) NOT NULL UNIQUE, token_hash CHAR(64) NOT NULL, expires_at INT UNSIGNED NOT NULL, created_at INT UNSIGNED NOT NULL, INDEX idx_admin_expires (admin_id, expires_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci "); } catch (Throwable $e) { // fail silently; remember-me stays disabled } $done = true; } function admin_clear_remember_cookie(): void { setcookie(ADMIN_REMEMBER_COOKIE, '', [ 'expires' => time() - 3600, 'path' => admin_cookie_path(), 'secure' => is_https(), 'httponly' => true, 'samesite' => 'Lax', ]); unset($_COOKIE[ADMIN_REMEMBER_COOKIE]); } function admin_delete_remember_tokens(?int $adminId = null, ?string $selector = null): void { admin_ensure_remember_table(); $now = time(); $sql = ''; $args = []; if ($selector !== null) { $sql = "DELETE FROM admin_remember_tokens WHERE selector = ?"; $args = [$selector]; } elseif ($adminId !== null) { $sql = "DELETE FROM admin_remember_tokens WHERE admin_id = ? OR expires_at < ?"; $args = [$adminId, $now]; } else { $sql = "DELETE FROM admin_remember_tokens WHERE expires_at < ?"; $args = [$now]; } try { pdo()->prepare($sql)->execute($args); } catch (Throwable $e) { // ignore cleanup issues } } function admin_issue_remember_token(int $adminId): void { if ($adminId <= 0) return; admin_delete_remember_tokens($adminId); $selector = bin2hex(random_bytes(12)); // 24 chars, indexed lookup $token = bin2hex(random_bytes(32)); // 64 chars secret $hash = hash('sha256', $token); $expires = time() + ADMIN_REMEMBER_TTL; try { pdo()->prepare(" INSERT INTO admin_remember_tokens (admin_id, selector, token_hash, expires_at, created_at) VALUES (?, ?, ?, ?, ?) ")->execute([$adminId, $selector, $hash, $expires, time()]); } catch (Throwable $e) { return; } $cookieVal = $selector . ':' . $token; setcookie(ADMIN_REMEMBER_COOKIE, $cookieVal, [ 'expires' => $expires, 'path' => admin_cookie_path(), 'secure' => is_https(), 'httponly' => true, 'samesite' => 'Lax', ]); $_COOKIE[ADMIN_REMEMBER_COOKIE] = $cookieVal; } function admin_start_session(array $adminRow): void { if (session_status() === PHP_SESSION_ACTIVE) { session_regenerate_id(true); } $_SESSION['admin_id'] = (int)($adminRow['id'] ?? 0); $_SESSION['admin_user'] = (string)($adminRow['username'] ?? ''); } function admin_forget_remember_me(): void { $selector = null; $cookie = (string)($_COOKIE[ADMIN_REMEMBER_COOKIE] ?? ''); if (strpos($cookie, ':') !== false) { [$selector] = explode(':', $cookie, 2); } if ($selector) { admin_delete_remember_tokens(null, $selector); } if (!empty($_SESSION['admin_id'])) { admin_delete_remember_tokens((int)$_SESSION['admin_id']); } else { admin_delete_remember_tokens(); } admin_clear_remember_cookie(); } function admin_try_remember_login(): bool { static $checked = false; if ($checked) return !empty($_SESSION['admin_id']); $checked = true; $cookie = (string)($_COOKIE[ADMIN_REMEMBER_COOKIE] ?? ''); if ($cookie === '' || strpos($cookie, ':') === false) return false; [$selector, $token] = explode(':', $cookie, 2); if ($selector === '' || $token === '') { admin_clear_remember_cookie(); return false; } // guard against oversized/invalid payloads before hitting DB if (!preg_match('/^[a-f0-9]{24}$/i', $selector) || !preg_match('/^[a-f0-9]{64}$/i', $token)) { admin_clear_remember_cookie(); return false; } admin_delete_remember_tokens(); // prune expired try { $st = pdo()->prepare("SELECT admin_id, token_hash, expires_at FROM admin_remember_tokens WHERE selector = ? LIMIT 1"); $st->execute([$selector]); $row = $st->fetch(); } catch (Throwable $e) { return false; } if (!$row) { admin_clear_remember_cookie(); return false; } if ((int)$row['expires_at'] < time()) { admin_delete_remember_tokens(null, $selector); admin_clear_remember_cookie(); return false; } $expected = (string)($row['token_hash'] ?? ''); if (!hash_equals($expected, hash('sha256', $token))) { admin_delete_remember_tokens(null, $selector); admin_clear_remember_cookie(); return false; } try { $u = pdo()->prepare("SELECT id, username FROM admin_users WHERE id = ? LIMIT 1"); $u->execute([(int)$row['admin_id']]); $adminRow = $u->fetch() ?: []; } catch (Throwable $e) { return false; } if (empty($adminRow)) { admin_delete_remember_tokens(null, $selector); admin_clear_remember_cookie(); return false; } admin_start_session($adminRow); admin_issue_remember_token((int)$adminRow['id']); // rotate token after successful auto-login return true; } function admin_is_logged_in(): bool { if (!empty($_SESSION['admin_id'])) return true; return admin_try_remember_login(); } function admin_login(array $adminRow, bool $remember = false): void { admin_start_session($adminRow); if ($remember) { admin_issue_remember_token((int)$_SESSION['admin_id']); } else { admin_forget_remember_me(); } } function admin_logout(): void { admin_forget_remember_me(); unset($_SESSION['admin_id'], $_SESSION['admin_user']); } function require_admin_login(): void { if (!admin_is_logged_in()) { header('Location: ' . url_path('/public/admin/login.php')); exit; } } // Optional: normal site users (not admin panel) function user_is_logged_in(): bool { return !empty($_SESSION['uid']); } function user_login(array $user): void { if (session_status() === PHP_SESSION_ACTIVE) { session_regenerate_id(true); } $_SESSION['uid'] = (int)($user['id'] ?? 0); } function user_logout(): void { unset($_SESSION['uid']); } function require_user_login(): void { if (!user_is_logged_in()) { header('Location: ' . url_path('/login.php')); exit; } }