Files
fitme3070.pt/shared/platform-tools/api/feedback.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

70 lines
3.0 KiB
PHP

<?php
// ─── Endpoint da bolha de feedback ───────────────────────────────────────────
//
// POST multipart/form-data (o que um <form> envia), resposta JSON:
// {"ok":true,"ref":"T-1234","track_url":"https://…"}
// {"ok":false,"error":"…"}
//
// 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 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.
//
// Sendo autenticado por cookie, é atacável por CSRF: um site qualquer podia
// levar o browser de um administrador a abrir tickets em nome dele. Daí o
// token do platform_csrf_ok(). Não substitui a verificação do papel — CSRF e
// permissão respondem a perguntas diferentes.
declare(strict_types=1);
require_once __DIR__ . '/../hub_bootstrap.php';
require_once __DIR__ . '/../functions_feedback.php';
require_once __DIR__ . '/../functions_csrf.php';
header('Content-Type: application/json; charset=utf-8');
// Nada disto deve ficar em cache nem ser partilhado entre sessões.
header('Cache-Control: no-store');
function feedback_out(int $status, array $payload): never
{
http_response_code($status);
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
feedback_out(405, ['ok' => false, 'error' => 'Usa POST.']);
}
$user = platform_feedback_user();
if (!$user || !platform_feedback_is_admin($user)) {
// 403 e não 401: quem chega aqui sem sessão não tem como a obter a partir
// deste endereço, e um 401 mandaria o browser pedir credenciais.
feedback_out(403, ['ok' => false, 'error' => 'Sessão terminada ou sem permissão. Recarrega a página.']);
}
if (!platform_csrf_ok()) {
feedback_out(403, ['ok' => false, 'error' => 'Pedido rejeitado (token expirado). Recarrega a página e tenta outra vez.']);
}
$res = platform_feedback_submit([
'title' => (string) ($_POST['title'] ?? ''),
'description' => (string) ($_POST['description'] ?? ''),
'category' => (string) ($_POST['category'] ?? 'problema'),
'blocking' => !empty($_POST['blocking']),
'page_url' => (string) ($_POST['page_url'] ?? ''),
], $user);
// 200 mesmo quando o hub recusa: o erro já vem escrito para a pessoa ler, e é
// o corpo que a bolha mostra. Um 4xx aqui só faria o fetch tratar como avaria
// de rede uma coisa que tem explicação.
feedback_out(200, [
'ok' => (bool) $res['ok'],
'error' => (string) ($res['error'] ?? ''),
'ref' => (string) ($res['ref'] ?? ''),
'track_url' => (string) ($res['track_url'] ?? ''),
]);