chartboard/app/Libraries/AuditLogger.php
2026-03-30 09:51:33 +05:30

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;
}
}