- 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>
87 lines
5.9 KiB
PHP
87 lines
5.9 KiB
PHP
<?php
|
|
// ─── Peças do formulário de entrada (partilhadas) ───────────────────────────
|
|
//
|
|
// O platform-tools dá a mecânica da autenticação (functions_auth.php,
|
|
// functions_remember.php) e cada site desenha a sua página de entrada — a
|
|
// marca e o layout são do site. O que NÃO deve variar de site para site são as
|
|
// duas peças que qualquer entrada da frota tem de ter, e é o que vive aqui:
|
|
//
|
|
// platform_password_field() — a palavra-passe com o olho para a mostrar
|
|
// platform_remember_field() — a caixa «Manter sessão iniciada»
|
|
//
|
|
// Antes disto, cada site tinha as suas (ou não tinha: a silver não tinha
|
|
// nenhuma das duas). Markup Bootstrap (input-group, form-check), que é o que as
|
|
// páginas de entrada da frota usam; o olho é um SVG embutido, sem depender dos
|
|
// ícones, e o script sai uma vez por página.
|
|
//
|
|
// Do lado do servidor, «manter sessão» é o auth_remember_issue() depois de
|
|
// entrar, o 'remember' => true no platform_connect() para repor a sessão, e o
|
|
// auth_remember_clear() ao sair — ver o README, «Connecting a site».
|
|
|
|
/**
|
|
* O campo da palavra-passe com o botão de a mostrar.
|
|
*
|
|
* $o: name ('password'), id ('password'), label ('Palavra-passe'; '' = sem
|
|
* etiqueta), autocomplete ('current-password' | 'new-password'), required
|
|
* (true), minlength (null), class (classes extra do <input>).
|
|
*/
|
|
function platform_password_field(array $o = []): string
|
|
{
|
|
static $scriptPrinted = false;
|
|
$o += ['name' => 'password', 'id' => 'password', 'label' => 'Palavra-passe',
|
|
'autocomplete' => 'current-password', 'required' => true, 'minlength' => null, 'class' => ''];
|
|
$e = fn($s) => htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8');
|
|
|
|
$eye = '<svg class="pt-eye-open" width="18" height="18" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">'
|
|
. '<path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8M1.17 8a13 13 0 0 1 1.66-2.04C4.12 4.67 5.88 3.5 8 3.5s3.88 1.17 5.17 2.46A13 13 0 0 1 14.83 8q-.09.13-.2.3c-.3.46-.73 1.06-1.29 1.66C12 11.33 10.12 12.5 8 12.5s-3.88-1.17-5.17-2.46A13 13 0 0 1 1.17 8"/>'
|
|
. '<path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5M4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0"/></svg>';
|
|
$eyeOff = '<svg class="pt-eye-closed" width="18" height="18" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" style="display:none">'
|
|
. '<path d="M13.36 11.24C15.07 9.72 16 8 16 8s-3-5.5-8-5.5a7 7 0 0 0-2.8.6l.77.77A6 6 0 0 1 8 3.5c2.12 0 3.88 1.17 5.17 2.46A13 13 0 0 1 14.83 8q-.09.13-.2.3c-.3.46-.73 1.06-1.29 1.66q-.18.18-.37.35z"/>'
|
|
. '<path d="M11.3 9.18a3.5 3.5 0 0 0-4.47-4.47l.82.82a2.5 2.5 0 0 1 2.83 2.83zm-2.94 1.47.82.82a3.5 3.5 0 0 1-4.47-4.47l.82.82a2.5 2.5 0 0 0 2.83 2.83"/>'
|
|
. '<path d="M3.35 5.47q-.27.24-.52.49A13 13 0 0 0 1.17 8l.2.3c.3.46.73 1.06 1.29 1.66C4.12 11.33 5.88 12.5 8 12.5c.72 0 1.4-.13 2.03-.36l.77.77A7 7 0 0 1 8 13.5C3 13.5 0 8 0 8s.94-1.73 2.64-3.24zm10.3 8.88-12-12 .7-.7 12 12z"/></svg>';
|
|
|
|
$html = '';
|
|
if ($o['label'] !== '') $html .= '<label class="form-label" for="' . $e($o['id']) . '">' . $e($o['label']) . '</label>';
|
|
$html .= '<div class="input-group">'
|
|
. '<input type="password" class="form-control ' . $e($o['class']) . '" id="' . $e($o['id']) . '" name="' . $e($o['name']) . '"'
|
|
. ' autocomplete="' . $e($o['autocomplete']) . '"'
|
|
. ($o['required'] ? ' required' : '')
|
|
. ($o['minlength'] ? ' minlength="' . (int) $o['minlength'] . '"' : '') . '>'
|
|
. '<button type="button" class="btn btn-outline-secondary" data-pt-reveal="' . $e($o['id']) . '"'
|
|
. ' aria-label="Mostrar a palavra-passe" aria-pressed="false" title="Mostrar a palavra-passe">' . $eye . $eyeOff . '</button>'
|
|
. '</div>';
|
|
|
|
if (!$scriptPrinted) {
|
|
$scriptPrinted = true;
|
|
// Um clique mostra, outro esconde. Volta sempre a esconder ao submeter,
|
|
// para o browser não oferecer guardar a palavra-passe como texto.
|
|
$html .= '<script>(function(){'
|
|
. 'document.addEventListener("click",function(ev){var b=ev.target.closest("[data-pt-reveal]");if(!b)return;'
|
|
. 'var i=document.getElementById(b.getAttribute("data-pt-reveal"));if(!i)return;var show=i.type==="password";'
|
|
. 'i.type=show?"text":"password";b.setAttribute("aria-pressed",show?"true":"false");'
|
|
. 'b.setAttribute("aria-label",show?"Esconder a palavra-passe":"Mostrar a palavra-passe");b.title=b.getAttribute("aria-label");'
|
|
. 'b.querySelector(".pt-eye-open").style.display=show?"none":"";b.querySelector(".pt-eye-closed").style.display=show?"":"none";});'
|
|
. 'document.addEventListener("submit",function(ev){ev.target.querySelectorAll("[data-pt-reveal]").forEach(function(b){'
|
|
. 'var i=document.getElementById(b.getAttribute("data-pt-reveal"));if(i)i.type="password";});},true);'
|
|
. '})();</script>';
|
|
}
|
|
return $html;
|
|
}
|
|
|
|
/**
|
|
* A caixa «Manter sessão iniciada». Desmarcada por omissão (e remarcada quando
|
|
* o formulário volta com um erro). O número de dias vem do mesmo sítio que o
|
|
* mecanismo usa, para o texto não mentir.
|
|
*/
|
|
function platform_remember_field(string $name = 'remember'): string
|
|
{
|
|
$days = function_exists('auth_remember_days') ? auth_remember_days() : 30;
|
|
$checked = !empty($_POST[$name]) ? ' checked' : '';
|
|
$id = 'pt-remember-' . preg_replace('/[^a-z0-9_-]/i', '', $name);
|
|
return '<div class="form-check">'
|
|
. '<input class="form-check-input" type="checkbox" name="' . htmlspecialchars($name) . '" id="' . $id . '" value="1"' . $checked . '>'
|
|
. '<label class="form-check-label" for="' . $id . '">Manter sessão iniciada</label>'
|
|
. '<div class="form-text">Durante ' . (int) $days . ' dias neste dispositivo. Não marque num computador partilhado.</div>'
|
|
. '</div>';
|
|
}
|