Files
fitme3070.pt/shared/platform-tools/index.php
T
andreandClaude Opus 5.5 0e8b53f620 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>
2026-09-25 07:28:13 +01:00

186 lines
8.7 KiB
PHP

<?php
require_once __DIR__ . '/functions_settings.php'; // platform_admin_url()
// ─── Shared sysadmin hub ─────────────────────────────────────────────────────
//
// Landing page for the sysadmin area. The host navbar points only here; every
// other sysadmin tool is reached via the cards on this page.
//
// Bootstrap contract — stub at <host>/views/sysadmin/index.php must:
// 1. Load $pdo (config/db.php + functions.php)
// 2. Run require_sysadmin()
// 3. Set $repo_root
// Optional overrides (same as platform_update.php / backups.php):
// $platform_layout_header / $platform_layout_navbar / $platform_layout_footer
// $sysadmin_tools — array of card definitions; merged with the defaults below.
// Each entry: ['title'=>str, 'description'=>str, 'icon'=>str (bi-*),
// 'url'=>str, 'badge'=>?str, 'color'=>str (primary/info/…)]
if (!isset($repo_root) || !is_string($repo_root)) {
$repo_root = realpath(dirname(__DIR__, 2)) ?: dirname(__DIR__, 2);
}
$repo_root = rtrim($repo_root, '/');
$base = function (string $path): string {
return function_exists('base_url') ? base_url($path) : '/' . ltrim($path, '/');
};
$default_tools = [
[
'title' => 'Atualizar plataforma',
'description' => 'Git pull/push, OPcache, migrations e log de erros.',
'icon' => 'bi-arrow-repeat',
'url' => platform_admin_url('platform_update.php'),
'color' => 'primary',
],
[
'title' => 'Backups',
'description' => 'Criar e transferir cópias de segurança dos uploads e da base de dados.',
'icon' => 'bi-archive',
'url' => platform_admin_url('backups.php'),
'color' => 'secondary',
],
[
'title' => 'Estado SMTP',
'description' => 'Configuração do envio de email, teste de ligação e mensagem de teste.',
'icon' => 'bi-envelope-check',
'url' => platform_admin_url('smtp_status.php'),
'color' => 'info',
],
// Por omissão e não site a site: a partir do momento em que o cliente do hub
// é partilhado, uma plataforma que o tenha instalado sem mostrar a página
// fica ligada ao hub sem ninguém saber por onde a desligar.
[
'title' => 'Integração com o hub',
'description' => 'Anunciar esta plataforma à Frota e gerir as chaves de leitura e de operações.',
'icon' => 'bi-hdd-network',
'url' => platform_admin_url('hub_integration.php'),
'color' => 'secondary',
// Um site que ainda não tenha o stub não deve mostrar um cartão para 404.
'available' => (function_exists('platform_router_enabled') && platform_router_enabled())
|| is_file($repo_root . '/public/views/sysadmin/hub_integration.php')
|| is_file($repo_root . '/public/views/admin/hub_integration.php'),
],
];
// Com o router, todas as páginas partilhadas existem em todos os sites — por
// isso aparecem sozinhas, e uma ferramenta nova chega a toda a frota sem cartão
// nenhum a escrever site a site. Sem router ficam de fora: cada site tem só os
// stubs que tem, e um cartão para 404 é pior do que cartão nenhum.
$routed = function_exists('platform_router_enabled') && platform_router_enabled();
$default_tools = array_merge($default_tools, [
[
'title' => 'Bloqueio de IP',
'description' => 'Endereços recusados à porta por atividade automatizada repetida, e quem está a caminho disso.',
'icon' => 'bi-shield-slash',
'url' => platform_admin_url('ip_bans.php'),
'color' => 'warning',
'available' => $routed,
],
[
'title' => 'Bloqueio por país',
'description' => 'Recusar formulários — ou o site — a quem chega de fora do país.',
'icon' => 'bi-globe-americas',
'url' => platform_admin_url('geo.php'),
'color' => 'warning',
'available' => $routed,
],
[
'title' => 'Restaurar base de dados',
'description' => 'Aplicar um backup de base de dados, com inspecção prévia e cópia de segurança automática do estado actual.',
'icon' => 'bi-arrow-counterclockwise',
'url' => platform_admin_url('restore.php'),
'color' => 'danger',
'available' => $routed,
],
[
'title' => 'Origem das visitas',
'description' => 'De onde vêm as pessoas, com que dispositivo e de que país — e quais dessas origens produzem pedidos.',
'icon' => 'bi-graph-up-arrow',
'url' => platform_admin_url('tracking.php'),
'color' => 'success',
'available' => $routed,
],
[
'title' => 'Registo de auditoria',
'description' => 'Quem fez o quê, a quê e quando — com o antes e o depois de cada alteração.',
'icon' => 'bi-journal-text',
'url' => platform_admin_url('audit.php'),
'color' => 'secondary',
'available' => $routed,
],
]);
// Um site pode esconder um cartão por omissão declarando
// $platform_hide_tools = ['smtp_status.php'] no seu stub. Serve para quando a
// plataforma tem uma página própria que já faz o mesmo -- o dance tem as
// Comunicações, que editam a configuração e correm os mesmos testes, e ter os
// dois cartões lado a lado só faz hesitar em qual clicar.
$hide = array_map('strval', $platform_hide_tools ?? []);
if ($hide) {
$default_tools = array_filter(
$default_tools,
fn($t) => !array_filter($hide, fn($h) => str_contains((string) $t['url'], $h))
);
}
// Cards podem declarar 'available' => bool para serem escondidos quando o
// servidor não suporta a ferramenta (ex.: exige proc_open). Ausente = visível.
$tools = array_filter(
array_merge($default_tools, $sysadmin_tools ?? []),
fn($t) => !isset($t['available']) || $t['available']
);
// Um cartão próprio que aponta para o mesmo sítio que um por omissão não o
// repete (um site que ainda declara «Bloqueio de IP» à mão, por exemplo).
$seen = [];
$tools = array_filter($tools, function ($t) use (&$seen) {
$url = (string) ($t['url'] ?? '');
if ($url === '' || isset($seen[$url])) return $url === '';
return $seen[$url] = true;
});
$layout_header = $platform_layout_header ?? ($repo_root . '/includes/header.php');
$layout_navbar = $platform_layout_navbar ?? ($repo_root . '/includes/navbar.php');
$layout_footer = $platform_layout_footer ?? ($repo_root . '/includes/footer.php');
$pageTitle = 'Sistema';
if (is_file($layout_header)) include $layout_header;
if (is_file($layout_navbar)) include $layout_navbar;
?>
<div class="container py-4" style="max-width:1000px;">
<div class="mb-4">
<h2 class="mb-1"><i class="bi bi-shield-lock me-2"></i>Sistema</h2>
<p class="text-muted small mb-0">Ferramentas de administração da plataforma.</p>
</div>
<div class="row g-3">
<?php foreach ($tools as $t): ?>
<div class="col-12 col-md-6">
<a href="<?= htmlspecialchars($t['url']) ?>"
class="card shadow-sm h-100 text-decoration-none text-reset border-0">
<div class="card-body d-flex gap-3">
<div class="flex-shrink-0">
<span class="d-inline-flex align-items-center justify-content-center rounded-3 text-bg-<?= htmlspecialchars($t['color'] ?? 'primary') ?>"
style="width:48px;height:48px;font-size:1.5rem;">
<i class="bi <?= htmlspecialchars($t['icon'] ?? 'bi-tools') ?>"></i>
</span>
</div>
<div class="flex-grow-1">
<div class="d-flex align-items-center gap-2">
<h5 class="mb-0"><?= htmlspecialchars($t['title']) ?></h5>
<?php if (!empty($t['badge'])): ?>
<span class="badge text-bg-warning"><?= htmlspecialchars($t['badge']) ?></span>
<?php endif; ?>
</div>
<p class="text-muted small mb-0 mt-1"><?= htmlspecialchars($t['description']) ?></p>
</div>
<div class="flex-shrink-0 align-self-center">
<i class="bi bi-chevron-right text-muted"></i>
</div>
</div>
</a>
</div>
<?php endforeach; ?>
</div>
</div>
<?php if (is_file($layout_footer)) include $layout_footer; ?>