feat(audit): platform-tools atualizado (12f26fd) e registo de auditoria ligado

- shared/platform-tools: ea3616a → 12f26fd (27 commits). Traz o módulo de
  audit (f761d99, o pin da frota), «manter sessão iniciada», o registo de
  segurança da frota (o gancho que o login já tinha passa a funcionar) e a
  correção do IP do cliente encontrada nesta auditoria (último X-Forwarded-For
  / X-Real-IP), promovida ao partilhado. client_ip() da app delega nele.
- config/functions_audit.php: audit() sobre platform_audit(); tabela
  audit_log criada no primeiro uso, segredos ocultados, nunca parte a ação.
- Registado: login, falha de login, bloqueio por excesso de tentativas,
  logout, pedidos de recuperação, mudança/definição de palavra-passe,
  registo e criação de utilizadores, convites, perfil (antes/depois), todas
  as definições (db_update_setting, com valor anterior), cada passo da
  lixeira (apagar, restaurar, eliminar de vez) e ações nas mensagens.
- Sistema → Auditoria (views/sysadmin/audit.php), só sysadmin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
andreandClaude Opus 5.5 committed 2026-09-25 07:28:13 +01:00
1 parent 5e300fc341
commit 0e8b53f620
65 files changed
+5852 -455

No files matched your search

+19 -17
View File
@@ -21,6 +21,8 @@
require_once __DIR__ . '/functions_settings.php';
require_once __DIR__ . '/functions_client_ip.php';
require_once __DIR__ . '/functions_security_events.php';
require_once __DIR__ . '/functions_connector.php';
// ─── Schema map ─────────────────────────────────────────────────────────────
//
@@ -30,24 +32,11 @@ require_once __DIR__ . '/functions_client_ip.php';
// is a function call. Sites converging on the standard names can simply not
// call auth_schema() at all.
// The defaults live in functions_connector.php, which the conformance check
// reads too — one list of standard names, not two that drift apart.
function auth_schema(?array $map = null): array {
static $schema = [
'table' => 'users',
'id' => 'id',
'email' => 'email',
// The column the login form matches on. null => use `email`, which is
// what five of the six sites do. quack authenticates by `username`
// instead, and still needs `email` to stay the address password
// recovery mails to -- so the two cannot be the same key.
'login' => null,
'password' => 'password_hash',
'name' => 'name',
'role' => 'role',
'active' => 'is_active', // null => no active flag on this site
'deleted_at' => null, // set when the site soft-deletes instead
'last_login' => 'last_login_at',
'last_ip' => 'last_login_ip',
];
static $schema = null;
$schema ??= platform_auth_schema_defaults();
if ($map !== null) $schema = array_merge($schema, $map);
return $schema;
}
@@ -243,6 +232,9 @@ function auth_throttle_check(PDO $pdo, string $identifier, int $max = 5, int $wi
$count = (int) $row['c'];
$blocked = $count >= $max;
if ($blocked) {
platform_security_event($pdo, 'login_throttled', hub_security_mask_identifier($identifier), $ip);
}
return [
'blocked' => $blocked,
'remaining' => max(0, $max - $count),
@@ -261,6 +253,7 @@ function auth_throttle_hit(PDO $pdo, string $identifier): void {
} catch (Throwable $e) {
error_log('[auth] throttle record failed: ' . $e->getMessage());
}
platform_security_event($pdo, 'login_failed', hub_security_mask_identifier($identifier));
}
function auth_throttle_clear(PDO $pdo, string $identifier): void {
@@ -327,6 +320,11 @@ function auth_login(PDO $pdo, array $user): void {
];
$_SESSION['_auth_seen'] = time();
if (function_exists('platform_audit')) {
platform_audit($pdo, 'auth.login', ['target_type' => 'user', 'target_id' => (int) $user[$s['id']],
'target_label' => (string) ($user[$s['email']] ?? '')]);
}
$sets = []; $params = [];
if ($s['last_login']) { $sets[] = "`{$s['last_login']}` = NOW()"; }
if ($s['last_ip']) { $sets[] = "`{$s['last_ip']}` = ?"; $params[] = auth_client_ip(); }
@@ -342,6 +340,10 @@ function auth_login(PDO $pdo, array $user): void {
}
function auth_logout(bool $destroy = true): void {
// Antes de limpar a sessão: depois já não se sabe quem saiu.
if (function_exists('platform_audit') && !empty($_SESSION['user']['id'])) {
platform_audit(null, 'auth.logout', ['target_type' => 'user', 'target_id' => (int) $_SESSION['user']['id']]);
}
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$p = session_get_cookie_params();