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:
1 parent
5e300fc341
commit
0e8b53f620
65 files changed
+5852
-455
No files matched your search
@@ -6,9 +6,8 @@
|
||||
// dirname(__DIR__, 3) === <host>
|
||||
// The per-platform stub at <host>/api/agent.php is a one-line require of this file.
|
||||
//
|
||||
// Hosts on a different stack (surf: src/ + App\Database) write their own thin
|
||||
// stub that bootstraps their way and calls agent_dispatch() directly — the
|
||||
// action set lives in functions_agent.php so both paths share it.
|
||||
// The action set lives in functions_agent.php, so a host that needs its own
|
||||
// stub (surf sets MIGRATIONS_DIR first) still shares it.
|
||||
|
||||
$repo_root = dirname(__DIR__, 3);
|
||||
require_once $repo_root . '/config/db.php'; // opens $pdo, starts the session
|
||||
|
||||
@@ -8,9 +8,8 @@
|
||||
// Ao contrário dos outros endpoints desta pasta, este NÃO se autentica por
|
||||
// chave: quem chama é o browser de um administrador com sessão aberta neste
|
||||
// site. Precisa por isso da sessão e dos helpers de autenticação do site — e
|
||||
// obtém-nos do mesmo hub_bootstrap.php que os outros, que é quem sabe distinguir
|
||||
// os dois stacks da frota (config/db.php ou src/config.php + App\Database) e
|
||||
// deixa em cima da mesa a sessão aberta, o $pdo e o functions_hub.
|
||||
// obtém-nos do mesmo hub_bootstrap.php que os outros, que abre o config/db.php
|
||||
// do site e deixa em cima da mesa a sessão aberta, o $pdo e o functions_hub.
|
||||
//
|
||||
// Assim o stub de cada site é uma linha, igual à dos restantes endpoints.
|
||||
//
|
||||
|
||||
@@ -126,6 +126,22 @@ if ($git) $checks['git'] = $git;
|
||||
$backup = $hub_has_db_layer ? $health_check(fn() => hub_backup_check()) : [];
|
||||
if ($backup) $checks['backup'] = $backup;
|
||||
|
||||
// Envio de eventos de segurança ao hub. [] numa plataforma por emparelhar ou
|
||||
// sem base de dados — não reportado, como os outros.
|
||||
$security = !($pdo instanceof PDO) ? [] : $health_check(function () use ($pdo) {
|
||||
require_once __DIR__ . '/../functions_security_events.php';
|
||||
return hub_security_check($pdo);
|
||||
});
|
||||
if ($security) $checks['security'] = $security;
|
||||
|
||||
// A base de dados tem a forma que o código partilhado espera? Reporta as avarias
|
||||
// silenciosas (uma coluna em falta que desliga o throttle sem erro nenhum) —
|
||||
// ver functions_connector.php. Só leitura, uma consulta ao information_schema.
|
||||
if ($pdo instanceof PDO) $checks['conformance'] = $health_check(function () use ($pdo) {
|
||||
require_once __DIR__ . '/../functions_connector.php';
|
||||
return platform_conformance_check($pdo);
|
||||
});
|
||||
|
||||
hub_json([
|
||||
// Só a base de dados: ver a nota no topo sobre o que significa `ok`. Numa
|
||||
// plataforma estática não há base de dados que decida nada -- o que torna a
|
||||
@@ -156,4 +172,9 @@ hub_json([
|
||||
'enabled' => true,
|
||||
'auth' => 'X-Hub-Key: <chave de operações>',
|
||||
],
|
||||
// As páginas de sistema deste site, para o hub ligar para a página local
|
||||
// (o recurso quando o hub não chega). Só com router: ver functions_routes.php.
|
||||
'tools' => function_exists('platform_routes_announced') ? platform_routes_announced() : [],
|
||||
// E os endpoints pelo router da API — o caminho para endpoints novos.
|
||||
'api' => function_exists('platform_api_routes_announced') ? platform_api_routes_announced() : [],
|
||||
]);
|
||||
@@ -34,10 +34,20 @@ $root = $repo_root;
|
||||
// vez de deixar as ações chamarem métodos sobre um $pdo a null — um erro de PHP
|
||||
// apareceria no hub como plataforma avariada, que é uma coisa diferente.
|
||||
if (!$pdo instanceof PDO
|
||||
&& in_array($action, ['migrations.list', 'migrations.apply_pending'], true)) {
|
||||
&& in_array($action, ['migrations.list', 'migrations.apply_pending', 'update.run', 'update.status',
|
||||
'ip_bans.list', 'ip_bans.ban', 'ip_bans.unban', 'tracking.summary'], true)) {
|
||||
hub_json(['ok' => false, 'error' => 'plataforma sem base de dados — não há migrações a aplicar'], 400);
|
||||
}
|
||||
|
||||
// Auditoria: o que o hub mandou fazer aqui fica escrito, com o hub como autor.
|
||||
// Só as ações que mudam alguma coisa; as leituras (list, status) não.
|
||||
if ($pdo instanceof PDO && function_exists('platform_audit')
|
||||
&& in_array($action, ['backup.trigger', 'update.run', 'git.pull', 'migrations.apply_pending', 'ip_bans.ban', 'ip_bans.unban'], true)) {
|
||||
$params = is_array($req) ? $req : $_POST; unset($params['action']);
|
||||
platform_audit($pdo, 'hub.' . $action, ['actor' => ['id' => null, 'name' => 'hub', 'role' => 'hub'],
|
||||
'target_type' => 'platform', 'changes' => $params ?: null]);
|
||||
}
|
||||
|
||||
switch ($action) {
|
||||
|
||||
// ── Cópias de segurança ──────────────────────────────────────────────────
|
||||
@@ -91,9 +101,10 @@ switch ($action) {
|
||||
$output = $pull['output'] !== '' ? $pull['output'] : '(sem saída)';
|
||||
if ($pull['ok']) {
|
||||
// O ff-only recusa-se a fundir; um repositório divergente falha aqui
|
||||
// em vez de inventar um merge que ninguém pediu.
|
||||
$sub = git_run(['submodule', 'update', '--init', '--recursive'], $root);
|
||||
if (trim($sub['output']) !== '') $output .= "\n\n[submodules]\n" . $sub['output'];
|
||||
// em vez de inventar um merge que ninguém pediu. Os submódulos vão
|
||||
// pelo git_sync_submodules(), que respeita os que seguem o branch.
|
||||
$sub = git_sync_submodules($root);
|
||||
$output .= "\n\n[submodules]\n" . implode("\n", $sub['log']);
|
||||
if (function_exists('opcache_reset')) @opcache_reset();
|
||||
if (function_exists('settings_cache_clear')) settings_cache_clear();
|
||||
if (function_exists('platform_settings_cache_clear')) platform_settings_cache_clear();
|
||||
@@ -101,6 +112,80 @@ switch ($action) {
|
||||
}
|
||||
hub_json(['ok' => $pull['ok'], 'output' => $output]);
|
||||
|
||||
// ── Atualização completa ─────────────────────────────────────────────────
|
||||
// O mesmo caminho do webhook de deploy (auto_update_run): cópia antes de
|
||||
// atualizar se estiver ligada, pull que pára em trabalho local, migrações
|
||||
// pendentes e caches. É o que o botão do hub deve fazer — o git.pull acima
|
||||
// fica para quem quer só o código.
|
||||
//
|
||||
// Responde já e trabalha depois de fechar a ligação: a cópia prévia pode
|
||||
// demorar minutos e o hub só espera MONITOR_OPS_TIMEOUT_SECONDS. O
|
||||
// resultado lê-se a seguir com update.status.
|
||||
case 'update.run':
|
||||
require_once __DIR__ . '/../functions_auto_update.php';
|
||||
hub_load_backup_helpers();
|
||||
if (!function_exists('git_run') || !is_dir($root . '/.git')) {
|
||||
hub_json(['ok' => false, 'error' => 'esta instalação não é um repositório git (típico em cPanel) — as cópias continuam a funcionar'], 503);
|
||||
}
|
||||
@session_write_close();
|
||||
platform_respond_and_continue(202, [
|
||||
'ok' => true,
|
||||
'status' => 'accepted',
|
||||
'note' => 'Atualização iniciada. O resultado fica em update.status daqui a instantes.',
|
||||
]);
|
||||
auto_update_run($pdo, $root, null);
|
||||
exit;
|
||||
|
||||
case 'update.status':
|
||||
require_once __DIR__ . '/../functions_auto_update.php';
|
||||
hub_json(['ok' => true, 'last' => auto_update_last($pdo)]);
|
||||
|
||||
// ── Bloqueio de IP ───────────────────────────────────────────────────────
|
||||
// A mesma lista e as mesmas acções da página ip_bans.php deste site, para o
|
||||
// hub mostrar numa aba. Um banimento feito daqui fica com source='hub' e,
|
||||
// como qualquer outro, segue para a frota.
|
||||
case 'ip_bans.list':
|
||||
require_once __DIR__ . '/../functions_ip_ban.php';
|
||||
hub_json(['ok' => true, 'bans' => platform_ip_ban_list($pdo, 200),
|
||||
'offenders' => platform_ip_offenders($pdo, 7, 30)]);
|
||||
|
||||
case 'ip_bans.ban':
|
||||
case 'ip_bans.unban':
|
||||
require_once __DIR__ . '/../functions_ip_ban.php';
|
||||
$ip = trim((string) (is_array($req) ? ($req['ip'] ?? '') : ''));
|
||||
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
|
||||
hub_json(['ok' => false, 'error' => 'endereço IP inválido'], 400);
|
||||
}
|
||||
if ($action === 'ip_bans.unban') {
|
||||
hub_json(['ok' => platform_ip_unban($pdo, $ip), 'ip' => $ip]);
|
||||
}
|
||||
if (platform_ip_ban_exempt($ip)) {
|
||||
hub_json(['ok' => false, 'error' => 'endereços internos não podem ser banidos'], 400);
|
||||
}
|
||||
// `minutes: null` é permanente; ausente é um dia.
|
||||
$minutes = array_key_exists('minutes', $req) && $req['minutes'] === null
|
||||
? null : max(1, (int) ($req['minutes'] ?? 1440));
|
||||
$reason = trim((string) ($req['reason'] ?? '')) ?: 'banido a partir do hub';
|
||||
$ok = platform_ip_ban($pdo, $ip, $reason, $minutes, 'hub', null);
|
||||
hub_json(['ok' => $ok, 'ip' => $ip, 'minutes' => $minutes], $ok ? 200 : 500);
|
||||
|
||||
// ── Origem das visitas ───────────────────────────────────────────────────
|
||||
// Só agregados — origens, formulários, países, dispositivos. Nenhuma visita
|
||||
// individual sai daqui, nem o hash do IP.
|
||||
case 'tracking.summary':
|
||||
require_once __DIR__ . '/../functions_tracking.php';
|
||||
$days = (int) (is_array($req) ? ($req['days'] ?? 30) : 30);
|
||||
if (!in_array($days, [7, 30, 90, 365], true)) $days = 30;
|
||||
platform_tracking_ensure_tables($pdo);
|
||||
hub_json([
|
||||
'ok' => true,
|
||||
'days' => $days,
|
||||
'sources' => array_slice(platform_tracking_report($pdo, $days), 0, 25),
|
||||
'forms' => platform_tracking_conversions_by_form($pdo, $days),
|
||||
'countries' => platform_tracking_breakdown($pdo, 'country', $days, 10),
|
||||
'devices' => platform_tracking_breakdown($pdo, 'device_type', $days, 6),
|
||||
]);
|
||||
|
||||
// ── Migrações ────────────────────────────────────────────────────────────
|
||||
// Um pull sem as migrações aplicadas deixa a app a correr contra um esquema
|
||||
// antigo, portanto o hub tem de conseguir fazer as duas coisas.
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
// ─── Front controller dos endpoints partilhados ─────────────────────────────
|
||||
//
|
||||
// Um só stub por site para toda a API partilhada:
|
||||
//
|
||||
// <?php require __DIR__ . '/../../shared/platform-tools/api/router.php'; // public/api/platform.php
|
||||
//
|
||||
// e cada endpoint responde em api/platform.php?endpoint=<nome> (ou
|
||||
// api/platform?endpoint=… com 'clean_urls'). A lista é fechada
|
||||
// (platform_api_routes()), e cada endpoint continua a autenticar-se a si
|
||||
// próprio: este ficheiro só carrega o arranque do site e despacha.
|
||||
//
|
||||
// Um site com implementação própria de um endpoint declara-a no
|
||||
// platform_connect(): 'api_files' => ['backup_trigger' => <caminho absoluto>]
|
||||
// (a silver tem guardas com outros nomes). O 'migrations_dir' do site chega ao
|
||||
// agente, como chega às páginas.
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../hub_bootstrap.php'; // $repo_root, $pdo, o arranque do site
|
||||
require_once __DIR__ . '/../functions_routes.php';
|
||||
|
||||
$endpoint = (string) ($_GET['endpoint'] ?? '');
|
||||
$file = platform_api_routes()[$endpoint] ?? null;
|
||||
if ($file === null) {
|
||||
hub_json(['ok' => false, 'error' => 'endpoint desconhecido'], 404);
|
||||
}
|
||||
|
||||
$cfg = function_exists('platform_config') ? platform_config() : [];
|
||||
if (!empty($cfg['migrations_dir'])) {
|
||||
$GLOBALS['MIGRATIONS_DIR'] = (string) $cfg['migrations_dir'];
|
||||
}
|
||||
$target = (string) ($cfg['api_files'][$endpoint] ?? (dirname(__DIR__) . '/' . $file));
|
||||
unset($cfg, $file);
|
||||
|
||||
require $target;
|
||||
Reference in new issue
Block a user