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

+110
View File
@@ -0,0 +1,110 @@
<?php
// ─── Front controller das páginas de sistema ─────────────────────────────────
//
// Um só stub por site, em vez de um por página:
//
// <?php require __DIR__ . '/../shared/platform-tools/router.php'; // public/platform.php
//
// e cada página partilhada abre em platform.php?tool=<nome>. Uma ferramenta nova
// no platform-tools aparece em todos os sites no próximo update do submódulo,
// sem ficheiro nenhum a criar do lado de lá.
//
// O que os stubs faziam à mão vem agora do platform_connect() do site:
//
// guard quem pode entrar (obrigatório — sem ele isto recusa)
// guards guarda própria de uma ferramenta, p. ex. ['tracking' => 'require_admin']
// replaced_tools ferramenta que o site substitui por página sua:
// ['smtp_status' => 'views/sysadmin/comms.php']
// layout ['header' => …, 'navbar' => …, 'footer' => …]
// dashboard_url o «Voltar» das páginas (relativo ao base_url, ou absoluto);
// em array, por ferramenta — as outras voltam ao índice
// migrations_dir onde vivem as migrações, quando não é database/migrations/
// sysadmin_tools cartões próprios do índice (array, ou callable que o devolve)
// hide_tools cartões por omissão a esconder, p. ex. ['smtp_status']
// page_vars variáveis `platform_*` extra de uma página, p. ex.
// ['platform_error_log_file' => …]
//
// Query string (platform.php?tool=backups) e não /platform/backups: um caminho
// bonito precisa de regras de reescrita, e a frota tem Apache em Docker, cPanel
// e talvez nginx. Isto funciona em todos sem configurar nada.
//
// A tabela de rotas é uma lista fechada, e cada página continua a fazer as suas
// verificações (CSRF, confirmação escrita). O router só despacha e guarda a
// porta — nunca inclui um nome vindo do pedido.
declare(strict_types=1);
require_once __DIR__ . '/hub_bootstrap.php'; // $repo_root, $pdo, config/db.php + functions.php
require_once __DIR__ . '/functions_connector.php';
require_once __DIR__ . '/functions_routes.php';
$tool = (string) ($_GET['tool'] ?? 'index');
$route = platform_routes()[$tool] ?? null;
if ($route === null) {
http_response_code(404);
exit('Ferramenta desconhecida.');
}
$cfg = platform_config();
// Uma guarda por ferramenta quando o site a declara (a Origem das visitas é de
// admin na fotografia, não só de sysadmin); senão a geral.
$guard = $cfg['guards'][$tool] ?? ($cfg['guard'] ?? null);
if ($cfg === [] || !is_callable($guard)) {
// Falha fechada: sem guarda declarada não há forma de saber quem pode entrar,
// e uma página de sistema aberta a qualquer um é pior do que nenhuma.
http_response_code(500);
exit('Este site ainda não declara uma guarda de sysadmin no platform_connect() — '
. 'as páginas de sistema não abrem sem ela.');
}
$guard();
// Um site pode ter página própria no lugar de uma partilhada (as Comunicações
// da fotografia em vez do Estado SMTP: dois editores da mesma configuração
// acabavam por mostrar um o que o outro não gravou). O URL partilhado continua
// a chegar lá.
if (!empty($cfg['replaced_tools'][$tool])) {
header('Location: ' . platform_route_absolute_url((string) $cfg['replaced_tools'][$tool]), true, 302);
exit;
}
if (!$pdo instanceof PDO && !empty($route['needs_db'])) {
http_response_code(503);
exit('Esta ferramenta precisa de base de dados, e este site não tem.');
}
// O que os stubs definiam antes de incluir a página.
foreach (['header', 'navbar', 'footer'] as $part) {
if (!empty($cfg['layout'][$part])) ${'platform_layout_' . $part} = $cfg['layout'][$part];
}
// Em array é por ferramenta, e as que lá não estão voltam ao índice do Sistema
// (é de lá que se chega a elas): ['index' => 'views/admin/users.php'].
if (is_array($cfg['dashboard_url'] ?? null)) {
$platform_dashboard_url = isset($cfg['dashboard_url'][$tool])
? platform_route_absolute_url((string) $cfg['dashboard_url'][$tool])
: platform_route_url('index');
} elseif (!empty($cfg['dashboard_url'])) {
$platform_dashboard_url = platform_route_absolute_url((string) $cfg['dashboard_url']);
}
if (!empty($cfg['migrations_dir'])) {
$GLOBALS['MIGRATIONS_DIR'] = (string) $cfg['migrations_dir'];
}
foreach ((array) ($cfg['page_vars'] ?? []) as $name => $value) {
if (is_string($name) && str_starts_with($name, 'platform_')) ${$name} = $value;
}
if ($tool === 'index') {
$tools = $cfg['sysadmin_tools'] ?? [];
$sysadmin_tools = is_callable($tools) ? (array) $tools() : (array) $tools;
foreach ($sysadmin_tools as &$card) {
if (isset($card['url'])) $card['url'] = platform_route_absolute_url((string) $card['url']);
}
unset($card);
// Sem o .php: o índice compara com str_contains() no URL do cartão, e o do
// router é …?tool=smtp_status, não …/smtp_status.php.
$platform_hide_tools = array_map(fn($h) => preg_replace('/\.php$/', '', (string) $h), (array) ($cfg['hide_tools'] ?? []));
}
if (!empty($route['backup_helpers'])) {
hub_load_backup_helpers();
}
unset($cfg, $tools, $part, $name, $value);
require __DIR__ . '/' . $route['file'];