Files
fitme3070.pt/config/db.php
T
andreandClaude Sonnet 5 fa87aabb87 feat(auth): migra login/sessão/roles para o auth partilhado (auditoria 1.3)
config/db.php liga-se com 'auth' => 'shared' em vez de 'own': login,
sessão, papéis e guardas passam a correr sobre
shared/platform-tools/functions_auth.php (auth_attempt/auth_login/
auth_at_least/auth_is/auth_session_start/auth_remember_*) em vez da
lógica local duplicada. Os ~30 ficheiros em views/ que chamam
require_login()/require_admin()/require_staff()/require_sysadmin()/
is_admin()/etc. não mudam — só o que está por baixo dessas funções.

- roles: staff=40, client=10 registados para preservar a hierarquia
  sysadmin>admin>staff>client que os antigos in_array() garantiam.
  `users` já tinha exactamente as colunas por omissão do auth_schema(),
  não foi preciso mapear nada.
- sessão: o bloco manual de ini_set()/session_start() em config/db.php
  foi substituído pela opção 'session' do platform_connect() — mesmo
  nome de cookie (FIT_SESS), mesmo SameSite=Lax, secure/httponly
  passam a ser geridos por auth_session_start().
- «Manter sessão iniciada»: config/functions_remember.php (a
  reimplementação local do selector/validator) foi eliminado a favor
  do módulo partilhado (auth_remember_issue/resume/clear via
  'remember' => true), que já cobria o mesmo desenho.
- auth_attempts: tinha (ip, scope) do throttle próprio da app
  (config/functions_throttle.php), não o `identifier` que o throttle
  partilhado exige. Migração aditiva
  (migration_20260925_shared_auth_attempts.sql) torna a tabela
  conforme sem tocar no throttle da app, que continua a ser o único
  usado para login/registo/recuperação — o throttle partilhado não é
  chamado, para não duplicar a mesma defesa com duas contagens.
- last_login_ua (sem equivalente partilhado) passa a ser gravado por
  fit_stamp_login_ua(), chamado a seguir a auth_login().

Testado ponta-a-ponta num contentor scratch na VPS (site-fitme:8.4,
cópia da BD de produção + migração aplicada): visitante anónimo é
redirigido, password errada dá o mesmo erro genérico e conta para o
throttle (bloqueia à 5ª tentativa, 429), login correcto regenera a
sessão, grava last_login_at/ip/ua, audita "auth.login" e (com
"lembrar-me") emite o cookie am_remember; esse cookie sozinho, numa
sessão nova, repõe a sessão certa com rotação do validator; logout
limpa sessão+cookie+token e audita "auth.logout"; platform.php
(guard=require_sysadmin) e views/admin/dashboard.php respondem 302
sem sessão e 200 com sessão de sysadmin. php -l limpo em todos os
ficheiros alterados. Migração já aplicada em produção (aditiva).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 17:25:54 +01:00

142 lines
6.9 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.
'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',
]],
]);