Files
JobcardSystem/config/bootstrap.php
T

142 lines
4.1 KiB
PHP
Raw Normal View History

2026-09-01 18:54:47 +02:00
<?php
declare(strict_types=1);
function env_required(string $name): string
{
$value = getenv($name);
if ($value === false || trim($value) === '') {
throw new RuntimeException("Missing required environment variable: {$name}");
}
return $value;
}
function db(): PDO
{
static $pdo = null;
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'), [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
return $pdo;
}
function csrf_token(): string
{
if (empty($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf'];
}
function verify_csrf(): void
{
$provided = (string)($_POST['_csrf'] ?? '');
if (!hash_equals((string)($_SESSION['csrf'] ?? ''), $provided)) {
http_response_code(419);
exit('Invalid CSRF token');
}
}
function ensure_initial_administrator(): void
{
static $checked = false;
if ($checked) {
return;
}
$checked = true;
$count = (int)db()->query('SELECT COUNT(*) FROM users')->fetchColumn();
if ($count !== 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');
}
$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');
}
$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),
]);
}
function current_user(): ?array
{
ensure_initial_administrator();
static $user = false;
if ($user !== false) {
return $user;
}
$id = $_SESSION['user_id'] ?? 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);
}
function require_login(): array
{
$user = current_user();
if (!$user) {
header('Location: /?route=login');
exit;
}
return $user;
}
function can(string $permission): bool
{
$user = current_user();
if (!$user) return false;
static $permissions = null;
if ($permissions === null) {
$stmt = db()->prepare('SELECT p.name FROM permissions p JOIN role_permissions rp ON rp.permission_id = p.id WHERE rp.role_id = :role');
$stmt->execute(['role' => $user['role_id']]);
$permissions = array_column($stmt->fetchAll(), 'name');
}
return in_array($permission, $permissions, true);
}
function require_permission(string $permission): void
{
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,
]);
}
function e(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}