feat: implement SQLite backup & data integrity plan (Phases 2-4)

This commit is contained in:
Pontoporeia
2026-05-11 01:08:46 +02:00
parent c0163ca4d5
commit 926659087f
18 changed files with 683 additions and 151 deletions

80
app/src/Audit.php Normal file
View File

@@ -0,0 +1,80 @@
<?php
/**
* Data-level audit logger.
*
* Writes a row to the `audit_log` table for every INSERT, UPDATE, or DELETE
* on core data tables. Unlike AdminLogger (which tracks admin *actions*),
* this captures the actual data change — before and after snapshots.
*
* Usage:
* Audit::log($db, $actor, 'DELETE', 'tags', $tagId, $oldRow);
* Audit::log($db, $actor, 'UPDATE', 'theses', $thesisId, $oldRow, $newRow);
* Audit::log($db, $actor, 'INSERT', 'tags', $newId, null, $newRow);
*/
class Audit
{
/**
* Log a data mutation.
*
* @param Database $db Database instance.
* @param string $actor Who triggered this (IP, username, 'system', etc.)
* @param string $action 'INSERT', 'UPDATE', or 'DELETE'.
* @param string $tableName The table being mutated.
* @param int|null $recordId The primary key of the affected row.
* @param array|null $oldData Row data before the mutation (null for INSERT).
* @param array|null $newData Row data after the mutation (null for DELETE).
*/
public static function log(
Database $db,
string $actor,
string $action,
string $tableName,
?int $recordId,
?array $oldData = null,
?array $newData = null
): void {
try {
$stmt = $db->getConnection()->prepare(
'INSERT INTO audit_log (actor, action, table_name, record_id, old_data, new_data)
VALUES (?, ?, ?, ?, ?, ?)'
);
$stmt->execute([
$actor,
$action,
$tableName,
$recordId,
$oldData !== null ? self::safeJsonEncode($oldData) : null,
$newData !== null ? self::safeJsonEncode($newData) : null,
]);
} catch (\Throwable $e) {
// Audit logging is best-effort — never crash the app over it.
error_log('[Audit] write failed: ' . $e->getMessage());
}
}
/**
* Build the actor string from the current request context.
*/
public static function actor(): string
{
$ip = $_SERVER['REMOTE_ADDR'] ?? 'cli';
$user = $_SESSION['admin_user'] ?? null;
return $user ? "$user@$ip" : $ip;
}
/**
* JSON-encode data, redacting sensitive/password fields.
*/
private static function safeJsonEncode(array $data): string
{
$safe = $data;
// Redact password-like fields
foreach (['password', 'pass', 'secret', 'token', 'credential'] as $key) {
if (array_key_exists($key, $safe)) {
$safe[$key] = '[REDACTED]';
}
}
return json_encode($safe, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR);
}
}