feat: mapa carrega logo no site; sysadmin edita utilizadores (incl. promover a admin)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
andreandClaude Sonnet 5.5 committed 2026-09-29 12:39:36 +01:00
1 parent 4bf177289a
commit f2a9c5181b
4 files changed
+111 -19

No files matched your search

+2 -16
View File
@@ -9,22 +9,8 @@
// e o browser pede-a ao próprio site.
function map_click_to_load(string $iframeHtml, string $mapsLink = ''): string {
if (trim($iframeHtml) === '') return '';
static $n = 0;
$id = 'map-consent-' . (++$n);
$esc = fn(string $s): string => htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
$link = $mapsLink !== ''
? '<a href="' . $esc($mapsLink) . '" target="_blank" rel="noopener" class="map-consent-link">Abrir no Google Maps</a>'
: '';
return '<div class="map-consent" id="' . $id . '" style="position:absolute;inset:0;display:flex;flex-direction:column;'
. 'align-items:center;justify-content:center;gap:.6rem;padding:1rem;text-align:center;color:#f5f5f5;">'
. '<i class="fas fa-map-marker-alt" aria-hidden="true" style="font-size:1.8rem;color:#C2D500"></i>'
. '<button type="button" class="site-btn" style="min-height:44px" '
. 'onclick="var w=document.getElementById(\'' . $id . '\');var t=w.nextElementSibling;'
. 'w.insertAdjacentHTML(\'afterend\',t.innerHTML);w.remove();">Mostrar mapa</button>'
. '<small style="max-width:32em;opacity:.8">O mapa é do Google Maps: ao abri-lo, a Google recebe o seu endereço IP e pode guardar cookies.</small>'
. $link
. '</div><template>' . $iframeHtml . '</template>';
// O mapa aparece logo com a página; o nome ficou por compatibilidade.
return trim($iframeHtml);
}
function review_avatar_url(array $r): string {
+2 -2
View File
@@ -86,7 +86,7 @@ if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
<li><b>[FORNECEDOR DO SERVIDOR]</b> — centro de dados onde a plataforma está alojada, na União Europeia;</li>
<li><b>Zoho Corporation</b> — processa o formulário de contacto, com alojamento na União Europeia.</li>
</ul>
<p>O mapa da secção de contactos é do <b>Google Maps</b> e só é carregado se carregares em «Mostrar mapa»; nesse momento a Google recebe o teu endereço IP e pode guardar cookies, segundo a <a href="https://policies.google.com/privacy" target="_blank" rel="noopener">política da Google</a>. 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.</p>
<p>O mapa da secção de contactos é do <b>Google Maps</b>: ao abrires a página a Google recebe o teu endereço IP e pode guardar cookies, segundo a <a href="https://policies.google.com/privacy" target="_blank" rel="noopener">política da Google</a>. 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.</p>
<h2 class="h4 mt-4">5. Os teus direitos</h2>
<p>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.
@@ -125,7 +125,7 @@ if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
</form>
<h2 class="h4 mt-4">6. Cookies</h2>
<p>Este site usa apenas um cookie técnico de sessão (<code>FIT_SESS</code>), 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.</p>
<p>Este site usa apenas um cookie técnico de sessão (<code>FIT_SESS</code>), 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 mapa do Google Maps pode colocar cookies da Google.</p>
<h2 class="h4 mt-4">7. Segurança</h2>
<p>As comunicações são cifradas (HTTPS), o acesso ao backoffice é restrito e registado, e as cópias de segurança são cifradas.</p>
+101
View File
@@ -0,0 +1,101 @@
<?php
// Edição de um utilizador pelo administrador de sistema: nome, email, telefone,
// perfil (incluindo promover a admin) e estado. A palavra-passe continua em
// set_password.php. Só o sysadmin chega aqui — um admin não se pode promover.
require __DIR__ . '/../../config/db.php';
require __DIR__ . '/../../config/functions.php';
$user = require_sysadmin();
$target = db_get_user($pdo, (int) ($_GET['id'] ?? $_POST['id'] ?? 0));
if (!$target) {
header('Location: ' . base_url('views/user/index.php'));
exit;
}
// Não se altera o próprio perfil/estado aqui: evita o sysadmin ficar sem acesso
// por engano (o último sysadmin deixaria de existir).
$is_self = ((int) $target['id'] === (int) $user['id']);
$roles = ['sysadmin', 'admin', 'staff', 'client'];
$error = '';
$saved = false;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
csrf_check();
$name = trim($_POST['name'] ?? '');
$email = strtolower(trim($_POST['email'] ?? ''));
$phone = trim($_POST['phone'] ?? '');
$role = $is_self ? $target['role'] : (string) ($_POST['role'] ?? '');
$active = $is_self ? (int) $target['is_active'] : (isset($_POST['is_active']) ? 1 : 0);
if ($name === '' || mb_strlen($name) > 100) {
$error = 'Indique um nome (máx. 100 caracteres).';
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL) || strlen($email) > 190) {
$error = 'Email inválido.';
} elseif (!in_array($role, $roles, true)) {
$error = 'Perfil inválido.';
} elseif (mb_strlen($phone) > 20) {
$error = 'Telefone demasiado longo (máx. 20 caracteres).';
} else {
$other = db_get_user_by_email($pdo, $email);
if ($other && (int) $other['id'] !== (int) $target['id']) {
$error = 'Já existe outro utilizador com esse email.';
}
}
if ($error === '') {
$fields = ['name', 'email', 'phone', 'role', 'is_active'];
$before = array_intersect_key($target, array_flip($fields));
$new = ['name' => $name, 'email' => $email, 'phone' => $phone !== '' ? $phone : null,
'role' => $role, 'is_active' => $active];
$pdo->prepare('UPDATE users SET name=?, email=?, phone=?, role=?, is_active=? WHERE id=?')
->execute([$name, $email, $new['phone'], $role, $active, (int) $target['id']]);
$changes = platform_audit_diff($before, $new);
if ($changes) {
audit('user.update', [
'target_type' => 'user', 'target_id' => (int) $target['id'],
'target_label' => $name, 'changes' => $changes,
]);
}
$saved = true;
$target = db_get_user($pdo, (int) $target['id']) ?? $target;
} else {
// Mantém o que foi escrito para o formulário não perder as alterações.
$target = array_merge($target, ['name' => $name, 'email' => $email, 'phone' => $phone, 'role' => $role, 'is_active' => $active]);
}
}
$page_title = 'Editar utilizador';
include __DIR__ . '/../../includes/header.php';
?>
<div class="container" style="max-width:640px;">
<h1 class="h3 mb-4">Editar utilizador</h1>
<?php if ($saved): ?><div class="alert alert-success">Utilizador atualizado.</div><?php endif; ?>
<?php if ($error): ?><div class="alert alert-danger"><?= htmlspecialchars($error) ?></div><?php endif; ?>
<?php if ($is_self): ?>
<div class="alert alert-info small">É a sua própria conta: o perfil e o estado não podem ser alterados aqui, para não perder o acesso.</div>
<?php endif; ?>
<form method="POST" class="card shadow-sm"><div class="card-body">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int) $target['id'] ?>">
<div class="mb-3"><label class="form-label" for="eu-name">Nome</label>
<input class="form-control" id="eu-name" name="name" required maxlength="100" value="<?= htmlspecialchars($target['name']) ?>"></div>
<div class="mb-3"><label class="form-label" for="eu-email">Email</label>
<input type="email" class="form-control" id="eu-email" name="email" required maxlength="190" value="<?= htmlspecialchars($target['email']) ?>"></div>
<div class="mb-3"><label class="form-label" for="eu-phone">Telefone</label>
<input class="form-control" id="eu-phone" name="phone" maxlength="20" value="<?= htmlspecialchars((string) ($target['phone'] ?? '')) ?>"></div>
<div class="mb-3"><label class="form-label" for="eu-role">Perfil</label>
<select class="form-select" id="eu-role" name="role"<?= $is_self ? ' disabled' : '' ?>>
<?php foreach ($roles as $r): ?>
<option value="<?= $r ?>"<?= $target['role'] === $r ? ' selected' : '' ?>><?= htmlspecialchars(role_label($r)) ?></option>
<?php endforeach; ?>
</select></div>
<div class="form-check mb-3"><input class="form-check-input" type="checkbox" name="is_active" id="eu-active" value="1"
<?= !empty($target['is_active']) ? 'checked' : '' ?><?= $is_self ? ' disabled' : '' ?>>
<label class="form-check-label" for="eu-active">Conta ativa</label></div>
<button class="btn btn-primary">Guardar</button>
<a class="btn btn-link" href="<?= base_url('views/user/index.php') ?>">Voltar</a>
</div></form>
</div>
<?php include __DIR__ . '/../../includes/footer.php'; ?>
+6 -1
View File
@@ -36,7 +36,12 @@ include __DIR__ . '/../../includes/header.php';
· <?= htmlspecialchars(parse_user_agent((string) $u['last_login_ua'])) ?>
<?php else: ?>—<?php endif; ?>
</td>
<td class="text-end">
<td class="text-end text-nowrap">
<?php if (is_sysadmin()): ?>
<a class="btn btn-sm btn-outline-secondary"
href="<?= base_url('views/user/edit.php?id=' . (int) $u['id']) ?>"
title="Editar utilizador"><i class="bi bi-pencil"></i></a>
<?php endif; ?>
<a class="btn btn-sm btn-outline-secondary"
href="<?= base_url('views/user/set_password.php?id=' . (int) $u['id']) ?>"
title="Gerir palavra-passe"><i class="bi bi-key"></i></a>