feat: extract MediaController, wire into Dispatcher, delete media.php

This commit is contained in:
Pontoporeia
2026-04-17 11:44:08 +02:00
parent b03be51b92
commit 75f808bee4
157 changed files with 1713 additions and 452 deletions

170
app/src/AdminAuth.php Normal file
View File

@@ -0,0 +1,170 @@
<?php
/**
* Minimal PHP session guard for the admin panel.
*
* This is a defence-in-depth layer that sits behind nginx Basic Auth.
* It protects against proxy misconfiguration, bypass, and local-dev
* scenarios where the reverse proxy may be absent.
*
* The admin password hash is stored in the site_settings table
* (key = 'admin_password_hash').
*
* If the hash is empty/missing the guard is a no-op (dev / cli-server).
*/
class AdminAuth
{
private const SESSION_KEY = 'admin_authenticated';
private const LOGIN_URL = '/admin/login.php';
/**
* Start the PHP session with hardened cookie parameters.
* Idempotent — safe to call even if session is already active.
*/
private static function startSession(): void
{
if (session_status() !== PHP_SESSION_NONE) {
return;
}
// Harden session cookie (item #8)
session_set_cookie_params([
'lifetime' => 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();
}
}