122 lines
3.5 KiB
PHP
122 lines
3.5 KiB
PHP
<?php
|
|
|
|
namespace App\Libraries;
|
|
|
|
use App\Models\AuditLogModel;
|
|
use Config\Services;
|
|
|
|
class AuditLogger
|
|
{
|
|
private const SENSITIVE_KEYS = [
|
|
'password',
|
|
'api_auth_value',
|
|
'api_token',
|
|
'password_hash',
|
|
'public_password',
|
|
'reset_token',
|
|
'verify_token',
|
|
'connection_uri',
|
|
];
|
|
|
|
/**
|
|
* @param mixed $oldValue JSON-serializable; arrays are redacted in place for known keys
|
|
* @param mixed $newValue JSON-serializable
|
|
*/
|
|
public static function log(
|
|
string $action,
|
|
?string $resourceType = null,
|
|
?int $resourceId = null,
|
|
mixed $oldValue = null,
|
|
mixed $newValue = null,
|
|
?int $workspaceId = null,
|
|
?int $userId = null
|
|
): void {
|
|
try {
|
|
$request = Services::request();
|
|
$session = session();
|
|
|
|
if ($userId === null) {
|
|
$uid = $session->get('user_id');
|
|
$userId = is_numeric($uid) ? (int) $uid : null;
|
|
}
|
|
|
|
$ip = method_exists($request, 'getIPAddress') ? $request->getIPAddress() : null;
|
|
$ua = '';
|
|
if (method_exists($request, 'getUserAgent')) {
|
|
$ua = substr((string) $request->getUserAgent(), 0, 500);
|
|
}
|
|
|
|
$oldJson = self::toJsonColumn(self::redact(self::normalize($oldValue)));
|
|
$newJson = self::toJsonColumn(self::redact(self::normalize($newValue)));
|
|
|
|
(new AuditLogModel())->insert([
|
|
'workspace_id' => $workspaceId,
|
|
'user_id' => $userId,
|
|
'action' => substr($action, 0, 100),
|
|
'resource_type' => $resourceType !== null ? substr($resourceType, 0, 50) : null,
|
|
'resource_id' => $resourceId,
|
|
'old_value' => $oldJson,
|
|
'new_value' => $newJson,
|
|
'ip_address' => $ip !== '' && $ip !== null ? substr((string) $ip, 0, 45) : null,
|
|
'user_agent' => $ua !== '' ? $ua : null,
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
log_message('error', 'AuditLogger failed: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
private static function normalize(mixed $value): mixed
|
|
{
|
|
if ($value === null) {
|
|
return null;
|
|
}
|
|
|
|
if (is_scalar($value) || $value instanceof \Stringable) {
|
|
return (string) $value;
|
|
}
|
|
|
|
if (is_array($value)) {
|
|
return $value;
|
|
}
|
|
|
|
if (is_object($value)) {
|
|
$json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
|
|
|
|
return is_string($json) ? json_decode($json, true) : null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static function redact(mixed $value): mixed
|
|
{
|
|
if (! is_array($value)) {
|
|
return $value;
|
|
}
|
|
|
|
$out = [];
|
|
foreach ($value as $k => $v) {
|
|
$key = (string) $k;
|
|
if (in_array($key, self::SENSITIVE_KEYS, true)) {
|
|
$out[$key] = $v !== null && $v !== '' ? '[redacted]' : null;
|
|
|
|
continue;
|
|
}
|
|
$out[$key] = is_array($v) ? self::redact($v) : $v;
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
private static function toJsonColumn(mixed $value): ?string
|
|
{
|
|
if ($value === null) {
|
|
return null;
|
|
}
|
|
|
|
$json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
|
|
|
|
return $json === false ? '{}' : $json;
|
|
}
|
|
}
|