refactor: support portable PHP hosting without Docker
This commit is contained in:
+32
-49
@@ -1,6 +1,26 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
function load_dotenv(string $path): void
|
||||
{
|
||||
if (!is_file($path) || !is_readable($path)) return;
|
||||
foreach (file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '' || str_starts_with($line, '#')) continue;
|
||||
$separator = strpos($line, '=');
|
||||
if ($separator === false) continue;
|
||||
$name = trim(substr($line, 0, $separator));
|
||||
$value = trim(substr($line, $separator + 1));
|
||||
if ($name === '' || getenv($name) !== false) continue;
|
||||
if (strlen($value) >= 2 && (($value[0] === '"' && $value[-1] === '"') || ($value[0] === "'" && $value[-1] === "'"))) {
|
||||
$value = substr($value, 1, -1);
|
||||
}
|
||||
putenv("{$name}={$value}");
|
||||
}
|
||||
}
|
||||
|
||||
load_dotenv(dirname(__DIR__) . '/.env');
|
||||
|
||||
function env_required(string $name): string
|
||||
{
|
||||
$value = getenv($name);
|
||||
@@ -13,10 +33,7 @@ function env_required(string $name): string
|
||||
function db(): PDO
|
||||
{
|
||||
static $pdo = null;
|
||||
if ($pdo instanceof PDO) {
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
if ($pdo instanceof PDO) return $pdo;
|
||||
$dsn = sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4',
|
||||
env_required('DB_HOST'), getenv('DB_PORT') ?: '3306', env_required('DB_DATABASE'));
|
||||
$pdo = new PDO($dsn, env_required('DB_USERNAME'), env_required('DB_PASSWORD'), [
|
||||
@@ -29,9 +46,7 @@ function db(): PDO
|
||||
|
||||
function csrf_token(): string
|
||||
{
|
||||
if (empty($_SESSION['csrf'])) {
|
||||
$_SESSION['csrf'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
if (empty($_SESSION['csrf'])) $_SESSION['csrf'] = bin2hex(random_bytes(32));
|
||||
return $_SESSION['csrf'];
|
||||
}
|
||||
|
||||
@@ -52,44 +67,25 @@ function verify_csrf(): void
|
||||
function ensure_initial_administrator(): void
|
||||
{
|
||||
static $checked = false;
|
||||
if ($checked) {
|
||||
return;
|
||||
}
|
||||
if ($checked) return;
|
||||
$checked = true;
|
||||
$count = (int)db()->query('SELECT COUNT(*) FROM users')->fetchColumn();
|
||||
if ($count !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((int)db()->query('SELECT COUNT(*) FROM users')->fetchColumn() !== 0) return;
|
||||
$email = strtolower(trim(env_required('ADMIN_EMAIL')));
|
||||
$password = env_required('ADMIN_PASSWORD');
|
||||
if (strlen($password) < 12) {
|
||||
throw new RuntimeException('ADMIN_PASSWORD must be at least 12 characters');
|
||||
}
|
||||
if (strlen($password) < 12) throw new RuntimeException('ADMIN_PASSWORD must be at least 12 characters');
|
||||
$roleId = (int)db()->query("SELECT id FROM roles WHERE name = 'Administrator'")->fetchColumn();
|
||||
if ($roleId < 1) {
|
||||
throw new RuntimeException('Administrator role is missing from the database');
|
||||
}
|
||||
if ($roleId < 1) throw new RuntimeException('Administrator role is missing from the database');
|
||||
$stmt = db()->prepare('INSERT INTO users (role_id, email, name, password_hash) VALUES (:role, :email, :name, :hash)');
|
||||
$stmt->execute([
|
||||
'role' => $roleId,
|
||||
'email' => $email,
|
||||
'name' => 'System Administrator',
|
||||
'hash' => password_hash($password, PASSWORD_DEFAULT),
|
||||
]);
|
||||
$stmt->execute(['role' => $roleId, 'email' => $email, 'name' => 'System Administrator', 'hash' => password_hash($password, PASSWORD_DEFAULT)]);
|
||||
}
|
||||
|
||||
function current_user(): ?array
|
||||
{
|
||||
ensure_initial_administrator();
|
||||
static $user = false;
|
||||
if ($user !== false) {
|
||||
return $user;
|
||||
}
|
||||
if ($user !== false) return $user;
|
||||
$id = $_SESSION['user_id'] ?? null;
|
||||
if (!$id) {
|
||||
return $user = null;
|
||||
}
|
||||
if (!$id) return $user = null;
|
||||
$stmt = db()->prepare('SELECT u.*, r.name AS role_name FROM users u JOIN roles r ON r.id = u.role_id WHERE u.id = :id AND u.is_active = 1');
|
||||
$stmt->execute(['id' => $id]);
|
||||
return $user = ($stmt->fetch() ?: null);
|
||||
@@ -98,10 +94,7 @@ function current_user(): ?array
|
||||
function require_login(): array
|
||||
{
|
||||
$user = current_user();
|
||||
if (!$user) {
|
||||
header('Location: /?route=login');
|
||||
exit;
|
||||
}
|
||||
if (!$user) { header('Location: /?route=login'); exit; }
|
||||
return $user;
|
||||
}
|
||||
|
||||
@@ -120,24 +113,14 @@ function can(string $permission): bool
|
||||
|
||||
function require_permission(string $permission): void
|
||||
{
|
||||
if (!can($permission)) {
|
||||
http_response_code(403);
|
||||
exit('Forbidden');
|
||||
}
|
||||
if (!can($permission)) { http_response_code(403); exit('Forbidden'); }
|
||||
}
|
||||
|
||||
function audit(string $action, string $entityType, ?int $entityId = null, array $metadata = []): void
|
||||
{
|
||||
$user = current_user();
|
||||
$stmt = db()->prepare('INSERT INTO audit_events (user_id, action, entity_type, entity_id, metadata, ip_address) VALUES (:user_id, :action, :entity_type, :entity_id, :metadata, :ip)');
|
||||
$stmt->execute([
|
||||
'user_id' => $user['id'] ?? null,
|
||||
'action' => $action,
|
||||
'entity_type' => $entityType,
|
||||
'entity_id' => $entityId,
|
||||
'metadata' => $metadata ? json_encode($metadata, JSON_THROW_ON_ERROR) : null,
|
||||
'ip' => $_SERVER['REMOTE_ADDR'] ?? null,
|
||||
]);
|
||||
$stmt->execute(['user_id' => $user['id'] ?? null, 'action' => $action, 'entity_type' => $entityType, 'entity_id' => $entityId, 'metadata' => $metadata ? json_encode($metadata, JSON_THROW_ON_ERROR) : null, 'ip' => $_SERVER['REMOTE_ADDR'] ?? null]);
|
||||
}
|
||||
|
||||
function e(string $value): string
|
||||
|
||||
Reference in New Issue
Block a user