Files
aj-portfolio/includes/auth.php
2025-12-23 13:18:58 +02:00

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;
}
}