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>
This commit is contained in:
1 parent
5e300fc341
commit
0e8b53f620
65 files changed
+5852
-455
No files matched your search
@@ -316,8 +316,16 @@ function platform_smtp_config(PDO $pdo): array {
|
||||
*
|
||||
* A site overrides the segment by defining PLATFORM_ADMIN_DIR (or setting it in
|
||||
* .env) to e.g. 'views/admin'. Default keeps the existing behaviour.
|
||||
*
|
||||
* A site on the front controller (`'router' => true` in platform_connect())
|
||||
* gets platform.php?tool=… for every shared page; its own pages keep the dir.
|
||||
*/
|
||||
function platform_admin_url(string $page = ''): string {
|
||||
if (function_exists('platform_router_enabled') && platform_router_enabled()) {
|
||||
[$file, $query] = array_pad(explode('?', $page, 2), 2, '');
|
||||
$tool = $file === '' ? 'index' : preg_replace('/\.php$/', '', basename($file));
|
||||
if (isset(platform_routes()[$tool])) return platform_route_url($tool, $query);
|
||||
}
|
||||
$dir = 'views/sysadmin';
|
||||
if (defined('PLATFORM_ADMIN_DIR') && constant('PLATFORM_ADMIN_DIR') !== '') {
|
||||
$dir = (string) constant('PLATFORM_ADMIN_DIR');
|
||||
@@ -329,3 +337,76 @@ function platform_admin_url(string $page = ''): string {
|
||||
$path = trim($dir, '/') . ($page !== '' ? '/' . ltrim($page, '/') : '');
|
||||
return function_exists('base_url') ? base_url($path) : '/' . $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* A stable HMAC key for a named purpose.
|
||||
*
|
||||
* Signed form tokens (anti-spam stamps, captcha challenges) are issued in one
|
||||
* request and verified in another, so the key behind them must be **the same
|
||||
* across requests** -- a per-request key rejects every visitor with "invalid
|
||||
* token" and the failure looks like a bot problem rather than a bug.
|
||||
*
|
||||
* Three sources, in order, all stable:
|
||||
*
|
||||
* 1. platform_secret_key() -- the .env key, the normal case;
|
||||
* 2. a random value stored in system_settings, when there is no .env key;
|
||||
* 3. derived from the install's own configuration, for an install with
|
||||
* neither. It is a weaker secret than the other two, but it is constant,
|
||||
* and a defence that locks every form is worse than a weaker signature.
|
||||
*
|
||||
* Always domain-separated by $purpose, so a stamp minted for one feature can
|
||||
* never validate in another. Note that functions_antispam_forms.php keeps its
|
||||
* own older derivation on purpose: changing the key of a live site invalidates
|
||||
* the forms that are open at that instant, and those submissions are refused
|
||||
* silently -- leads lost for the sake of tidiness. New callers use this one.
|
||||
*
|
||||
* Pass the PDO wherever you have one. Source 3 is a last resort, and a site
|
||||
* that reaches it from one call site with a PDO and another without would sign
|
||||
* with two different keys.
|
||||
*/
|
||||
function platform_derived_key(string $purpose, ?PDO $pdo = null, string $settingKey = 'shared_hmac_key'): string {
|
||||
static $bases = [];
|
||||
|
||||
if (!isset($bases[$settingKey])) {
|
||||
$base = '';
|
||||
|
||||
$envKey = platform_secret_key();
|
||||
if ($envKey !== null) {
|
||||
$base = bin2hex($envKey);
|
||||
} elseif ($pdo instanceof PDO) {
|
||||
$base = (string) platform_settings_get($pdo, $settingKey, '');
|
||||
if ($base === '') {
|
||||
$fresh = bin2hex(random_bytes(32));
|
||||
try {
|
||||
// Stored in the clear on purpose: encrypting it would need
|
||||
// the key we do not have (that is why we are here), and a
|
||||
// value that decrypts differently on each read means every
|
||||
// token minted before this request stops verifying.
|
||||
platform_settings_set($pdo, $settingKey, $fresh, null, false);
|
||||
$base = $fresh;
|
||||
} catch (Throwable $e) {
|
||||
error_log('[settings] ' . $settingKey . ' not persisted (' . $e->getMessage()
|
||||
. '); deriving from install config');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($base === '') {
|
||||
$read = static function (string $k): string {
|
||||
if (defined($k)) return (string) constant($k);
|
||||
if (function_exists('env')) return (string) env($k, '');
|
||||
return (string) (getenv($k) ?: ($_ENV[$k] ?? ''));
|
||||
};
|
||||
// Not cached: this branch is the fallback, and a later call that
|
||||
// does carry a PDO must be free to find the real key instead of
|
||||
// inheriting this one.
|
||||
return hash('sha256', 'am-key|' . $purpose . '|derived|'
|
||||
. $read('DB_NAME') . '|' . $read('DB_USER') . '|' . $read('DB_PASS')
|
||||
. '|' . $read('APP_BASE_URL'), true);
|
||||
}
|
||||
|
||||
$bases[$settingKey] = $base;
|
||||
}
|
||||
|
||||
return hash('sha256', 'am-key|' . $purpose . '|' . $bases[$settingKey], true);
|
||||
}
|
||||
Reference in new issue
Block a user