151 lines
7.5 KiB
PHP
151 lines
7.5 KiB
PHP
<?php
|
|
require_once __DIR__ . '/env.php';
|
|
env_load(__DIR__ . '/../.env');
|
|
|
|
// The legacy CI4 front controller ran from the web root, so some ported view
|
|
// partials use root-relative paths (e.g. gallery's glob('uploads/gallery/...')).
|
|
// Pin the CWD to the project root so those keep resolving regardless of which
|
|
// script Apache invoked.
|
|
@chdir(dirname(__DIR__));
|
|
|
|
// ─── Bootstrap failure helper ────────────────────────────────────────────────
|
|
// Renders a generic 500 page. Never echoes exception details — they often
|
|
// contain the host:port, DB name, and (worst case) credentials. Real details
|
|
// land in logs/db_error.log; the page just says "something went wrong".
|
|
function _bootstrap_fail(string $logMsg): void {
|
|
@mkdir(__DIR__ . '/../logs', 0775, true);
|
|
@error_log('[' . date('c') . '] ' . $logMsg . "\n", 3, __DIR__ . '/../logs/db_error.log');
|
|
error_log('[fitme bootstrap] ' . $logMsg);
|
|
http_response_code(500);
|
|
header('Content-Type: text/html; charset=utf-8');
|
|
$tpl = __DIR__ . '/../templates/error_500.php';
|
|
if (is_file($tpl)) {
|
|
require $tpl;
|
|
} else {
|
|
echo '<!doctype html><meta charset="utf-8"><title>Erro</title>'
|
|
. '<p style="font-family:sans-serif;text-align:center;margin:80px auto;">Algo correu mal. Tente novamente em instantes.</p>';
|
|
}
|
|
exit(1);
|
|
}
|
|
|
|
// Required env vars — fail loud if any is missing. Hard-coded fallback creds
|
|
// (especially `root` / `admin`) were a footgun: a missing .env would let the
|
|
// app boot with the wrong identity instead of refusing to start.
|
|
foreach (['DB_NAME', 'DB_USER'] as $req) {
|
|
if (getenv($req) === false || getenv($req) === '') {
|
|
_bootstrap_fail("required env var {$req} is not set");
|
|
}
|
|
}
|
|
if (getenv('DB_PASS') === false) {
|
|
_bootstrap_fail('required env var DB_PASS is not set (empty string is allowed)');
|
|
}
|
|
|
|
$host = env('DB_HOST', '127.0.0.1');
|
|
$port = (int) env('DB_PORT', 3306);
|
|
$db = env('DB_NAME');
|
|
// Not $user: that name leaks into the router's flat include scope (router.php
|
|
// requires this file at top level, no function wrapper) and would shadow the
|
|
// real logged-in user array that includes/header.php expects.
|
|
$dbUser = env('DB_USER');
|
|
$pass = env('DB_PASS');
|
|
|
|
$dsn = "mysql:host=$host;port=$port;dbname=$db;charset=utf8mb4";
|
|
|
|
try {
|
|
$pdo = new PDO($dsn, $dbUser, $pass, [
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
// Real server-side prepares (not driver emulation): a tighter defence
|
|
// against injection than emulated string-interpolation. Safe here — the
|
|
// codebase uses positional ? params (no reused :named, no bound LIMIT).
|
|
PDO::ATTR_EMULATE_PREPARES => false,
|
|
// Keep values coming back as strings, as the emulated driver did, so the
|
|
// type change from native prepares doesn't ripple through existing code.
|
|
PDO::ATTR_STRINGIFY_FETCHES => true,
|
|
]);
|
|
} catch (PDOException $e) {
|
|
_bootstrap_fail('PDO connect failed: ' . $e->getMessage());
|
|
}
|
|
|
|
// ─── Ligação ao platform-tools ───────────────────────────────────────────────
|
|
//
|
|
// Auditoria 2026-09-24, ponto 1.3: a app passou a usar a autenticação
|
|
// partilhada (`shared/platform-tools/functions_auth.php`) em vez da sua
|
|
// própria — login, sessão, roles e guardas (config/auth.php,
|
|
// config/functions_user.php) agora chamam auth_attempt()/auth_login()/
|
|
// auth_at_least()/etc. por baixo, com os mesmos nomes de função de sempre
|
|
// para os ~30 ficheiros que os chamam não mudarem.
|
|
//
|
|
// O cookie de sessão (nome, Secure/HttpOnly/SameSite, modo estrito) passa a
|
|
// ser inteiramente gerido por auth_session_start() através da opção
|
|
// 'session' abaixo — o bloco que estava aqui antes (ini_set + session_start
|
|
// manuais) foi removido para não haver dois sítios a configurar a mesma
|
|
// sessão.
|
|
//
|
|
// `users` já tem exactamente as colunas por omissão do auth_schema()
|
|
// (id, email, name, password_hash, role, is_active, last_login_at,
|
|
// last_login_ip — confirmado via information_schema em produção), por isso
|
|
// não é preciso mapear nada em 'schema'.
|
|
//
|
|
// auth_attempts (o throttle da APP, config/functions_throttle.php — ip+scope,
|
|
// cobre login/registo/recuperação) continua a ser o único throttle de login
|
|
// usado; o throttle do módulo partilhado (auth_throttle_check/hit, que exige
|
|
// `identifier`) não é chamado, para não duplicar a defesa do mesmo
|
|
// formulário com duas contagens diferentes. A tabela foi tornada conforme na
|
|
// mesma (database/migration_20260925_shared_auth_attempts.sql) para o
|
|
// relatório de saúde abaixo ('conformance' => 'report') não acusar erro e
|
|
// para deixar a porta aberta a usar o throttle partilhado no futuro. Ver
|
|
// views/auth/login.php para a nota completa.
|
|
require_once dirname(__DIR__) . '/shared/platform-tools/functions_connector.php';
|
|
platform_connect($pdo, [
|
|
'auth' => 'shared',
|
|
'roles' => [
|
|
// Preserva a hierarquia que config/auth.php já tinha (sysadmin >
|
|
// admin > staff > client, por in_array(), não por posto numérico):
|
|
// staff acima de client, e os dois abaixo de admin(80), para
|
|
// require_staff()/is_staff() continuarem a excluir clientes e
|
|
// require_admin()/is_admin() continuarem a excluir staff e clientes.
|
|
// sysadmin(100)/admin(80)/user(10) já vêm registados por omissão.
|
|
'staff' => 40,
|
|
'client' => 10,
|
|
],
|
|
'session' => [
|
|
'name' => 'FIT_SESS',
|
|
'samesite' => 'Lax',
|
|
// secure/httponly ficam a cargo de auth_session_start(): secure
|
|
// segue o mesmo critério de HTTPS que o bloco antigo usava,
|
|
// httponly está sempre ligado.
|
|
],
|
|
'remember' => true, // «Manter sessão iniciada» — agora auth_remember_*
|
|
// do platform-tools; ver views/auth/login.php.
|
|
// Brand feed público (api/brand.php): nome, logos e cores para o hub.
|
|
// Cores = tokens :root de site/assets/css/style.css. logo_light = logo de
|
|
// cor clara (fundos escuros), logo_dark = logo escuro (fundos claros).
|
|
// name: omitido de propósito (APP_NAME ainda não existe aqui; o default é hub_app_name()).
|
|
'brand' => [
|
|
'logo_light' => fn() => site_logo_rel('logo_light_filename', 'site/assets/img/logos/logo_crossfit_white.svg'),
|
|
'logo_dark' => fn() => site_logo_rel('logo_dark_filename', 'site/assets/img/logos/logo_crossfit.svg'),
|
|
'colors' => ['primary' => '#c2d500', 'secondary' => '#0b0b0d', 'accent' => '#a6b600'],
|
|
],
|
|
'guard' => 'require_sysadmin',
|
|
'router' => true,
|
|
'conformance' => 'report', // nunca recusa — só reporta no health endpoint
|
|
|
|
// A Comunicações desta app já cobre SMTP (config + testes); o Estado SMTP
|
|
// partilhado seria um segundo editor da mesma configuração.
|
|
'replaced_tools' => ['smtp_status' => 'views/sysadmin/comms.php'],
|
|
'hide_tools' => ['smtp_status'],
|
|
'sysadmin_tools' => [[
|
|
'title' => 'Comunicações',
|
|
'description' => 'Configuração SMTP, fila de mensagens e testes de envio.',
|
|
'icon' => 'bi-broadcast',
|
|
'url' => 'views/sysadmin/comms.php',
|
|
'color' => 'info',
|
|
], [
|
|
'title' => 'Fila de mensagens',
|
|
'description' => 'O que saiu, o que falhou e porquê.',
|
|
'icon' => 'bi-envelope-paper',
|
|
'url' => 'views/sysadmin/messages.php',
|
|
'color' => 'secondary',
|
|
]],
|
|
]);
|