using System.Text.Json;
namespace ROLAC.API.Services.Logging;
///
/// Serializes audit before/after payloads to JSON and masks sensitive property names.
/// Shared by and the EF audit interceptor so masking is consistent.
///
public static class AuditChangeSerializer
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
};
/// Property names whose values are replaced with wherever they appear.
private static readonly HashSet SensitiveNames = new(StringComparer.OrdinalIgnoreCase)
{
"BankAccountNumber",
"BankRoutingNumber",
"PasswordHash",
"Password",
"SecurityStamp",
"ConcurrencyStamp",
};
public const string MaskValue = "***";
public static bool IsSensitive(string propertyName) => SensitiveNames.Contains(propertyName);
/// Builds the { before, after } JSON; returns null when both sides are empty.
public static string? BuildChanges(object? before, object? after)
{
if (before is null && after is null)
return null;
var payload = new Dictionary();
if (before is not null) payload["before"] = MaskObject(before);
if (after is not null) payload["after"] = MaskObject(after);
return JsonSerializer.Serialize(payload, JsonOptions);
}
/// Serializes a value (e.g. a property dictionary built by the interceptor) to JSON.
public static string Serialize(object value) => JsonSerializer.Serialize(value, JsonOptions);
///
/// Masks a free-form object by reflecting over its public properties. Used for the explicit
/// IAuditLogger.Write path (the interceptor masks per-property as it builds its dictionary).
///
private static object MaskObject(object value)
{
if (value is IDictionary dict)
{
var masked = new Dictionary();
foreach (var (key, val) in dict)
masked[key] = IsSensitive(key) ? MaskValue : val;
return masked;
}
var result = new Dictionary();
foreach (var prop in value.GetType().GetProperties())
{
if (!prop.CanRead || prop.GetIndexParameters().Length > 0)
continue;
result[prop.Name] = IsSensitive(prop.Name) ? MaskValue : prop.GetValue(value);
}
return result;
}
}