Files
fitme3070.pt/config/functions_audit.php
andreandClaude Opus 5.5 04c8c83e66 feat: aulas experimentais guardadas, anti-spam partilhado nos formulários, audit das escritas
- Pedido de aula experimental passa a ficar em trial_leads (por ginásio)
  ANTES do email: sem SMTP o visitante já não vê «erro» e o pedido não se
  perde. Gestão → Aulas experimentais: estados, notas, CSV (auditado),
  apagar para a lixeira; aviso «sem email» quando o aviso não saiu.
- Aula experimental, inscrição e eventos usam o anti-spam do platform-tools
  (honeypot, carimbo assinado com tempo mínimo, prova de JS, limite por IP,
  pontuação de conteúdo) e o bloqueio de IP, em vez de honeypots locais.
  Recusas ficam em blocked_submissions; resposta igual à de sucesso.
  Falha aberto se o módulo rebentar.
- Checkbox de consentimento da aula experimental passa a ser guardada.
- Audit das escritas de conteúdo: criar/editar equipa, modalidades,
  eventos, mensalidades, pedidos de inscrição, galerias, horário (incl.
  arrastar no calendário), horas, contactos e ginásios, com antes/depois
  campo a campo (audit_row/audit_change). Exportação CSV de inscrições
  auditada.
- .table-responsive em todas as tabelas do backoffice.
- Migração: database/migration_20260926_trial_leads.sql (só cria tabelas).

Testado numa cópia da BD de produção: 63/63 verificações HTTP.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:33:24 +01:00

60 lines
3.0 KiB
PHP

<?php
// ─── Registo de auditoria ────────────────────────────────────────────────────
// O registo é o do platform-tools (shared/platform-tools/functions_audit.php):
// tabela audit_log criada no primeiro uso, autor e IP tirados da sessão e do
// proxy, segredos ocultados em `changes`, nunca parte a ação. Consulta em
// Sistema → Auditoria (views/sysadmin/audit.php).
//
// audit() é só um atalho que vai buscar o $pdo global, para as funções de
// modelo que não o recebem. Convenção das ações: <entidade>.<verbo>
// (team.update, trash.purge, auth.login_failed, export.membership_leads…).
require_once __DIR__ . '/../shared/platform-tools/functions_audit.php';
function audit(string $action, array $d = []): void {
platform_audit($GLOBALS['pdo'] ?? null, $action, $d);
}
// Email mascarado para registos de falhas: permite ver padrões (o mesmo
// endereço atacado muitas vezes) sem guardar o endereço de quem se enganou.
function audit_mask_email(string $email): string {
$email = strtolower(trim($email));
if (!str_contains($email, '@')) return $email === '' ? '' : mb_substr($email, 0, 1) . '…';
[$u, $d] = explode('@', $email, 2);
return mb_substr($u, 0, 1) . '…@' . $d;
}
// ─── Escritas de conteúdo ────────────────────────────────────────────────────
// Os modelos (config/functions_*.php) chamam audit_row() antes de alterar e
// audit_change() depois: o registo leva o antes/depois campo a campo
// (platform_audit_diff) ou, numa criação, os campos criados. Tabela e chave vêm
// do código, nunca do pedido, e ainda assim são validadas.
function audit_row(PDO $pdo, string $table, string $pk, int $id): ?array {
if (!preg_match('/^[A-Za-z0-9_]+$/', $table) || !preg_match('/^[A-Za-z0-9_]+$/', $pk)) return null;
try {
$st = $pdo->prepare("SELECT * FROM `{$table}` WHERE `{$pk}` = ? LIMIT 1");
$st->execute([$id]);
$row = $st->fetch(PDO::FETCH_ASSOC);
return $row ?: null;
} catch (Throwable $e) {
return null;
}
}
function audit_change(PDO $pdo, string $entity, string $table, string $pk, int $id, string $verb,
?array $before = null, string $labelCol = 'name'): void {
if (!function_exists('platform_audit') || $id < 1) return;
$after = audit_row($pdo, $table, $pk, $id) ?? [];
$changes = $before !== null
? platform_audit_diff($before, $after, ['updated_at', 'created_at', 'updated_by'])
: platform_audit_created(array_diff_key($after, array_flip(['created_at', 'updated_at'])));
if ($verb !== 'create' && $before !== null && !$changes) return; // gravou sem mudar nada
platform_audit($pdo, $entity . '.' . $verb, [
'target_type' => $entity,
'target_id' => $id,
'target_label' => (string) ($after[$labelCol] ?? $before[$labelCol] ?? ''),
'changes' => $changes,
]);
}