Initial commit
This commit is contained in:
241
includes/auth.php
Normal file
241
includes/auth.php
Normal file
@@ -0,0 +1,241 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user