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>
This commit is contained in:
andreandClaude Sonnet 5 committed 2026-09-25 17:25:54 +01:00
1 parent ccc644ac0d
commit fa87aabb87
9 files changed
+196 -274

No files matched your search

+47 -26
View File
@@ -66,36 +66,57 @@ try {
_bootstrap_fail('PDO connect failed: ' . $e->getMessage());
}
// Distinct session cookie per app so installs that share a host (e.g.
// development.andmore.pt/id.andmore.pt/ and /fitme3070.pt/) don't overwrite
// each other's PHPSESSID. Must run before session_start().
//
// Cookie e modo estrito definidos aqui, no código, para valerem em qualquer
// servidor: antes o cookie saía só com path=/ (sem Secure, HttpOnly nem
// SameSite) e o PHP aceitava IDs de sessão inventados pelo cliente.
if (session_status() === PHP_SESSION_NONE) {
ini_set('session.use_strict_mode', '1');
ini_set('session.use_only_cookies', '1');
ini_set('session.cookie_httponly', '1');
ini_set('session.cookie_samesite', 'Lax');
if (request_is_https()) ini_set('session.cookie_secure', '1');
session_name('FIT_SESS');
session_start();
}
// ─── Ligação ao platform-tools ───────────────────────────────────────────────
//
// A app mantém as suas próprias passwords e sessão (`config/auth.php`,
// `views/auth/login.php`) — não passa a usar o functions_auth.php partilhado
// (`'auth' => 'own'`), porque a `auth_attempts` desta app não tem a coluna
// `identifier` que o throttle partilhado exige (tem `ip`+`scope`, do seu
// próprio throttle em functions_throttle.php). Ainda assim liga-se ao
// bloqueio de IP da frota, ao router das páginas de sistema (Backups,
// Actualizações, Auditoria, IP bans, Geo, Origem das visitas, Hub) e ao
// resto do platform-tools, tudo por cima da autenticação própria.
// 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' => 'own',
'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