0, 'path' => '/admin', 'secure' => (php_sapi_name() !== 'cli-server'), 'httponly' => true, 'samesite' => 'Strict', ]); session_start(); } /** * Fetch the admin password hash from site_settings. * Returns null if not set (dev mode). */ private static function getStoredHash(): ?string { // Legacy fallback: if the old constant is still defined, honour it. if (defined('ADMIN_PASSWORD_HASH') && ADMIN_PASSWORD_HASH !== '') { return ADMIN_PASSWORD_HASH; } // Lazy-load minimal DB just for this lookup. require_once APP_ROOT . '/src/Database.php'; $db = new Database(); $hash = $db->getSetting('admin_password_hash'); return $hash !== '' ? $hash : null; } /** * Gate every admin page. * * Authentication order: * 1. No password hash configured → dev mode, pass through. * 2. Session already authenticated → pass through. * 3. nginx Basic Auth password present in $_SERVER['PHP_AUTH_PW'] * → validate it with password_verify; on success create session * (seamless: user only sees the browser Basic Auth dialog). * 4. Neither → redirect to the PHP login form. */ public static function requireLogin(): void { self::startSession(); $storedHash = self::getStoredHash(); if ($storedHash === null) { return; // No password configured → dev / cli-server, skip. } if (!empty($_SESSION[self::SESSION_KEY])) { return; // Already authenticated via session. } // Try to auto-authenticate from the nginx Basic Auth credentials. if (isset($_SERVER['PHP_AUTH_PW']) && self::verifyHash($_SERVER['PHP_AUTH_PW'], $storedHash)) { return; } header('Location: ' . self::LOGIN_URL); exit; } /** * Validate a plaintext password against the stored hash. * On success: regenerates the session ID and marks the session authenticated. * * @return bool true on success, false on wrong password / no hash stored. */ public static function login(string $password): bool { $storedHash = self::getStoredHash(); if ($storedHash === null || !self::verifyHash($password, $storedHash)) { return false; } self::startSession(); session_regenerate_id(true); $_SESSION[self::SESSION_KEY] = true; $_SESSION['admin_login_at'] = time(); return true; } /** * Bcrypt verification wrapper. */ private static function verifyHash(string $password, string $hash): bool { return password_verify($password, $hash); } /** * Update the stored admin password hash in the database. */ public static function setPasswordHash(string $newHash): void { require_once APP_ROOT . '/src/Database.php'; $db = new Database(); $db->setSetting('admin_password_hash', $newHash); } /** * Remove the stored admin password hash (revert to dev mode). */ public static function removePasswordHash(): void { require_once APP_ROOT . '/src/Database.php'; $db = new Database(); $db->setSetting('admin_password_hash', ''); } /** * Check whether the current request is authenticated (without redirecting). */ public static function isAuthenticated(): bool { self::startSession(); $storedHash = self::getStoredHash(); if ($storedHash === null) { return true; // No password configured → dev mode. } return !empty($_SESSION[self::SESSION_KEY]); } /** * Check whether a password hash is configured in the system. */ public static function hasPassword(): bool { return self::getStoredHash() !== null; } /** * Destroy the session (logout). */ public static function logout(): void { self::startSession(); $_SESSION = []; if (ini_get('session.use_cookies')) { $p = session_get_cookie_params(); setcookie( session_name(), '', time() - 86400, $p['path'], $p['domain'], $p['secure'], $p['httponly'] ); } session_destroy(); } }