Files
fitme3070.pt/database/migration_20260925_shared_auth_attempts.sql
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

35 lines
2.2 KiB
SQL

-- ─── auth_attempts: torna-a conforme com o throttle do platform-tools ────────
-- Auditoria 2026-09-24, ponto 1.3: a app passa a usar o auth partilhado
-- (shared/platform-tools/functions_auth.php) em vez do seu próprio login/
-- sessão/roles. Ver config/db.php ('auth' => 'shared').
--
-- Esta tabela já existia (migration_20260913_local_auth.sql) com o formato do
-- throttle PRÓPRIO da app (config/functions_throttle.php: `ip` + `scope`,
-- cobre login/registo/recuperação). O throttle partilhado
-- (auth_throttle_check/hit em functions_auth.php) espera `identifier` + `ip`
-- e falharia (INSERT INTO auth_attempts (identifier, ip) ...) com um erro de
-- SQL — silenciosamente engolido pelo try/catch do throttle partilhado, que
-- por desenho falha aberto e deixaria de bloquear ninguém.
--
-- Decisão: a app CONTINUA a usar só o seu próprio throttle (login+registo+
-- recuperação, dois níveis IP+conta) — não passa a chamar
-- auth_throttle_check()/auth_throttle_hit() em lado nenhum, para não duplicar
-- a defesa contra o mesmo formulário com duas contagens diferentes. Esta
-- migração só torna a TABELA conforme com o que o platform_conformance()
-- (config/db.php, 'conformance' => 'report') verifica, para o relatório de
-- saúde ficar limpo e para deixar a porta aberta a usar o throttle partilhado
-- no futuro sem precisar de outra migração.
--
-- `identifier` e `success` são aditivos, com omissão — invisíveis para o
-- INSERT/SELECT do throttle próprio, que continua a nomear só `ip`+`scope`.
-- `scope` passa a ter omissão (era NOT NULL sem omissão) porque é a única
-- coluna que o INSERT do throttle partilhado não preenche; sem omissão esse
-- INSERT falhava em modo estrito.
--
-- Safe to re-run (IF NOT EXISTS é extensão do MariaDB).
ALTER TABLE `auth_attempts` ADD COLUMN IF NOT EXISTS `identifier` VARCHAR(190) NOT NULL DEFAULT '' AFTER `id`;
ALTER TABLE `auth_attempts` ADD COLUMN IF NOT EXISTS `success` TINYINT(1) NOT NULL DEFAULT 0 AFTER `attempted_at`;
ALTER TABLE `auth_attempts` MODIFY COLUMN `scope` VARCHAR(20) NOT NULL DEFAULT '';
ALTER TABLE `auth_attempts` ADD INDEX IF NOT EXISTS `idx_identifier_time` (`identifier`, `attempted_at`);