From 3f2c505c17aad74733b808fb9636ca6768e68d9d Mon Sep 17 00:00:00 2001 From: andre Date: Fri, 25 Sep 2026 23:48:18 +0100 Subject: [PATCH] feat(rgpd): pedido de acesso/apagamento de dados + processo escrito (item 9.5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - migration_20260925_rgpd_requests.sql: tabela rgpd_requests (email, tipo, estado, consentimento). - config/functions_rgpd.php: rgpd_find() (acesso, art.º 15) e rgpd_erase() (apagamento por anonimização, art.º 17) sobre membership_leads/trial_leads/ event_leads, + CRUD dos pedidos. - Política de Privacidade: formulário público (POST + CSRF + anti-spam) para pedir acesso ou apagamento, e o processo escrito (resposta em 30 dias). - Backoffice: página "Pedidos RGPD" (nav + index) para exportar os dados de um email (CSV, audit) ou anonimizá-los (POST + CSRF, audit). O email está desligado, por isso os pedidos ficam guardados (como as leads), não dependem de SMTP. Texto legal com placeholders — rever com jurista antes do go-live (como já constava na Política). Co-Authored-By: Claude Fable 5 --- config/functions.php | 1 + config/functions_rgpd.php | 74 ++++++++++++++++ database/migration_20260925_rgpd_requests.sql | 20 +++++ includes/header.php | 4 + site/privacidade.php | 63 +++++++++++++- views/admin/rgpd/index.php | 87 +++++++++++++++++++ 6 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 config/functions_rgpd.php create mode 100644 database/migration_20260925_rgpd_requests.sql create mode 100644 views/admin/rgpd/index.php diff --git a/config/functions.php b/config/functions.php index 26a22b7..e964b23 100644 --- a/config/functions.php +++ b/config/functions.php @@ -43,6 +43,7 @@ require_once __DIR__ . '/functions_reviews.php'; // reviews Google (Places require_once __DIR__ . '/functions_trials.php'; // pedidos de aula experimental require_once __DIR__ . '/functions_privacy.php'; // mapa e fotos da Google só a pedido / servidos localmente require_once __DIR__ . '/functions_forms.php'; // anti-spam + bloqueio de IP dos formulários públicos +require_once __DIR__ . '/functions_rgpd.php'; // pedidos de acesso/apagamento (art.º 15/17) // ─── DB-backed settings → legacy constants ─────────────────────────────────── // Resolved once from the settings cache so call sites read plain constants. diff --git a/config/functions_rgpd.php b/config/functions_rgpd.php new file mode 100644 index 0000000..3aaf684 --- /dev/null +++ b/config/functions_rgpd.php @@ -0,0 +1,74 @@ + 'Pedidos de inscrição', + 'trial_leads' => 'Aulas experimentais', + 'event_leads' => 'Inscrições em eventos', + ]; +} + +// Devolve os dados pessoais associados a um email, organizados por tabela. +function rgpd_find(PDO $pdo, string $email): array { + $out = []; + foreach (rgpd_pii_tables() as $table => $label) { + $st = $pdo->prepare( + "SELECT name, email, phone, message, created_at FROM `$table` WHERE email = ? ORDER BY created_at DESC" + ); + $st->execute([$email]); + $rows = $st->fetchAll(PDO::FETCH_ASSOC); + if ($rows) $out[$table] = $rows; + } + return $out; +} + +// Anonimiza os dados pessoais de um email em todas as tabelas públicas. +// Devolve o número de registos alterados. +function rgpd_erase(PDO $pdo, string $email): int { + $n = 0; + foreach (array_keys(rgpd_pii_tables()) as $table) { + $st = $pdo->prepare( + "UPDATE `$table` SET name = 'Apagado (RGPD)', email = '', phone = '', message = '' WHERE email = ?" + ); + $st->execute([$email]); + $n += $st->rowCount(); + } + return $n; +} + +function db_rgpd_request_create(PDO $pdo, array $d): int { + $st = $pdo->prepare( + "INSERT INTO rgpd_requests (gym_id, email, request_type, message, consent) VALUES (?, ?, ?, ?, ?)" + ); + $st->execute([ + (int) ($d['gym_id'] ?? 0), + (string) $d['email'], + (string) $d['request_type'], + (string) ($d['message'] ?? ''), + (int) ($d['consent'] ?? 0), + ]); + return (int) $pdo->lastInsertId(); +} + +function db_rgpd_request_get(PDO $pdo, int $id): ?array { + $st = $pdo->prepare("SELECT * FROM rgpd_requests WHERE id = ? LIMIT 1"); + $st->execute([$id]); + return $st->fetch(PDO::FETCH_ASSOC) ?: null; +} + +function db_rgpd_requests_list(PDO $pdo): array { + return $pdo->query( + "SELECT * FROM rgpd_requests WHERE deleted_at IS NULL ORDER BY created_at DESC" + )->fetchAll(PDO::FETCH_ASSOC); +} + +function db_rgpd_request_mark(PDO $pdo, int $id, string $status, int $by): void { + $st = $pdo->prepare("UPDATE rgpd_requests SET status = ?, processed_at = NOW(), processed_by = ? WHERE id = ?"); + $st->execute([$status, $by, $id]); +} diff --git a/database/migration_20260925_rgpd_requests.sql b/database/migration_20260925_rgpd_requests.sql new file mode 100644 index 0000000..18594b4 --- /dev/null +++ b/database/migration_20260925_rgpd_requests.sql @@ -0,0 +1,20 @@ +-- RGPD (art.º 15.º/17.º): pedidos de acesso e de apagamento de dados pessoais. +-- O visitante submete um pedido na Política de Privacidade; fica aqui (o email +-- está desligado, por isso o pedido é guardado, como as leads). O backoffice +-- processa: acesso = exportar os dados; apagamento = anonimizar. +CREATE TABLE IF NOT EXISTS `rgpd_requests` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `gym_id` INT NOT NULL DEFAULT 0, + `email` VARCHAR(190) NOT NULL, + `request_type` ENUM('acesso','apagamento') NOT NULL, + `message` TEXT NULL, + `status` ENUM('novo','concluido') NOT NULL DEFAULT 'novo', + `consent` TINYINT(1) NOT NULL DEFAULT 0, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `processed_at` DATETIME NULL DEFAULT NULL, + `processed_by` INT UNSIGNED NULL DEFAULT NULL, + `deleted_at` DATETIME NULL DEFAULT NULL, + `deleted_by` INT UNSIGNED NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_rgpd_requests_status` (`status`, `created_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/includes/header.php b/includes/header.php index a7634ee..0e4d3ef 100644 --- a/includes/header.php +++ b/includes/header.php @@ -173,6 +173,10 @@ $app_name = defined('APP_NAME') ? APP_NAME : 'FitMe 3070'; Lixeira + diff --git a/site/privacidade.php b/site/privacidade.php index 1fc9fa7..354814b 100644 --- a/site/privacidade.php +++ b/site/privacidade.php @@ -11,6 +11,36 @@ require __DIR__ . '/_open.php'; $pp_name = htmlspecialchars($info['name'] ?? 'FitMe 3070'); $pp_email = htmlspecialchars($info['email'] ?? ''); $pp_addr = htmlspecialchars($info['adress'] ?? ''); + +// Pedido RGPD (acesso/apagamento). POST + CSRF + anti-spam; fica guardado +// (o email está desligado, não pode depender de SMTP) e é processado no +// backoffice (Sistema → Pedidos RGPD). +$rgpd_status = ''; +if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') { + csrf_check(); + $rgpd_email = trim((string) ($_POST['rgpd_email'] ?? '')); + $rgpd_type = (string) ($_POST['rgpd_type'] ?? '') === 'apagamento' ? 'apagamento' : 'acesso'; + $rgpd_msg = trim((string) ($_POST['rgpd_message'] ?? '')); + $rgpd_consent = !empty($_POST['rgpd_consent']); + if (!filter_var($rgpd_email, FILTER_VALIDATE_EMAIL) || !$rgpd_consent) { + $rgpd_status = 'erro'; + } elseif (public_form_screen($pdo, 'rgpd', 'rgpd_requests', $rgpd_email, $rgpd_msg) !== null) { + $rgpd_status = 'ok'; // anti-spam: mesma resposta de sucesso + } else { + try { + db_rgpd_request_create($pdo, [ + 'gym_id' => (int) $gym['id'], + 'email' => $rgpd_email, + 'request_type' => $rgpd_type, + 'message' => $rgpd_msg, + 'consent' => 1, + ]); + $rgpd_status = 'ok'; + } catch (Throwable $e) { + $rgpd_status = 'erro'; + } + } +} ?>
@@ -59,10 +89,41 @@ $pp_addr = htmlspecialchars($info['adress'] ?? '');

O mapa da secção de contactos é do Google Maps e só é carregado se carregares em «Mostrar mapa»; nesse momento a Google recebe o teu endereço IP e pode guardar cookies, segundo a política da Google. As avaliações apresentadas vêm do perfil público do ginásio no Google; as fotos dos autores são guardadas neste site para o teu navegador não ter de as pedir à Google.

5. Os teus direitos

-

Tens direito de acesso, retificação, apagamento, limitação, portabilidade e oposição, e de retirar o consentimento a qualquer momento (sem afetar o tratamento feito até aí). Basta escreveres para ; respondemos no prazo de um mês. +

Tens direito de acesso, retificação, apagamento, limitação, portabilidade e oposição, e de retirar o consentimento a qualquer momento (sem afetar o tratamento feito até aí). Podes pedir a exportação dos teus dados ou o seu apagamento com o formulário abaixo; respondemos no prazo de um mês. Se tiveres conta na área de cliente, podes corrigir os teus dados e preferências no teu perfil. Tens ainda o direito de apresentar reclamação à Comissão Nacional de Proteção de Dados (www.cnpd.pt).

+

Pedir acesso ou apagamento

+ +
Pedido recebido. Vamos responder no prazo de um mês.
+ +
Não foi possível enviar o pedido. Confirma o e-mail e tenta de novo.
+ +
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+

6. Cookies

Este site usa apenas um cookie técnico de sessão (FIT_SESS), necessário para os formulários e para a área reservada, que expira ao fechar o navegador. Não usamos cookies de publicidade nem de estatística. O Google Maps só coloca cookies se escolheres ver o mapa.

diff --git a/views/admin/rgpd/index.php b/views/admin/rgpd/index.php new file mode 100644 index 0000000..f1ecb25 --- /dev/null +++ b/views/admin/rgpd/index.php @@ -0,0 +1,87 @@ + 'rgpd_request', 'target_id' => $id, + 'summary' => 'Apagamento RGPD de ' . audit_mask_email($req['email']) . ' — ' . $n . ' registos anonimizados', + ]); + session()->setFlashdata('success', 'Dados de ' . htmlspecialchars($req['email']) . ' anonimizados (' . $n . ' registos).'); + } + } + header('Location: ' . base_url('views/admin/rgpd/index.php')); + exit; +} + +// Exportar dados (acesso, art.º 15.º) — CSV com tudo o que existe para o email. +if (isset($_GET['csv']) && is_numeric($_GET['csv'])) { + $req = db_rgpd_request_get($pdo, (int) $_GET['csv']); + if ($req) { + audit('rgpd.export', [ + 'target_type' => 'rgpd_request', 'target_id' => (int) $_GET['csv'], + 'summary' => 'Exportação RGPD (acesso) de ' . audit_mask_email($req['email']), + ]); + $rows = []; + foreach (rgpd_find($pdo, $req['email']) as $table => $items) { + foreach ($items as $it) { + $rows[] = [rgpd_pii_tables()[$table] ?? $table, $it['name'], $it['email'], $it['phone'], $it['message'] ?? '', $it['created_at']]; + } + } + csv_export('rgpd', 'dados-' . substr(md5($req['email']), 0, 8), 'Exportação RGPD (acesso) de ' . audit_mask_email($req['email']), ['Fonte', 'Nome', 'E-mail', 'Telefone', 'Mensagem', 'Data'], $rows); + } +} + +$requests = db_rgpd_requests_list($pdo); +$page_title = 'Pedidos RGPD'; +include __DIR__ . '/../../../includes/header.php'; +?> +
+
+

Pedidos RGPD

+
+

Pedidos de acesso (exportação) e apagamento feitos na Política de Privacidade. O apagamento anonimiza o nome/e-mail/telefone nas leads — a linha fica para estatística e audit.

+ +
+
+ + + + + + + + + + + + + + + + + + +
DataE-mailPedidoMensagemEstadoAções
Sem pedidos RGPD.
+ Exportar + +
+ + + +
+ +
+
+
+
+