242 lines
6.5 KiB
PHP
242 lines
6.5 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/csrf.php';
|
|
|
|
const ADMIN_REMEMBER_COOKIE = 'admin_remember';
|
|
const ADMIN_REMEMBER_TTL = 120 * 24 * 60 * 60; // 120 days
|
|
|
|
function admin_cookie_path(): string {
|
|
$base = rtrim((string)($GLOBALS['BASE_PATH'] ?? ''), '/');
|
|
return $base ? $base . '/' : '/';
|
|
}
|
|
|
|
function admin_ensure_remember_table(): void {
|
|
static $done = false;
|
|
if ($done) return;
|
|
|
|
try {
|
|
pdo()->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;
|
|
}
|
|
}
|