@@ -0,0 +1,71 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ROLAC.API.Services.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Serializes audit before/after payloads to JSON and masks sensitive property names.
|
||||
/// Shared by <see cref="AuditLogger"/> and the EF audit interceptor so masking is consistent.
|
||||
/// </summary>
|
||||
public static class AuditChangeSerializer
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
|
||||
};
|
||||
|
||||
/// <summary>Property names whose values are replaced with <see cref="MaskValue"/> wherever they appear.</summary>
|
||||
private static readonly HashSet<string> SensitiveNames = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"BankAccountNumber",
|
||||
"BankRoutingNumber",
|
||||
"PasswordHash",
|
||||
"Password",
|
||||
"SecurityStamp",
|
||||
"ConcurrencyStamp",
|
||||
};
|
||||
|
||||
public const string MaskValue = "***";
|
||||
|
||||
public static bool IsSensitive(string propertyName) => SensitiveNames.Contains(propertyName);
|
||||
|
||||
/// <summary>Builds the <c>{ before, after }</c> JSON; returns null when both sides are empty.</summary>
|
||||
public static string? BuildChanges(object? before, object? after)
|
||||
{
|
||||
if (before is null && after is null)
|
||||
return null;
|
||||
|
||||
var payload = new Dictionary<string, object?>();
|
||||
if (before is not null) payload["before"] = MaskObject(before);
|
||||
if (after is not null) payload["after"] = MaskObject(after);
|
||||
|
||||
return JsonSerializer.Serialize(payload, JsonOptions);
|
||||
}
|
||||
|
||||
/// <summary>Serializes a value (e.g. a property dictionary built by the interceptor) to JSON.</summary>
|
||||
public static string Serialize(object value) => JsonSerializer.Serialize(value, JsonOptions);
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
private static object MaskObject(object value)
|
||||
{
|
||||
if (value is IDictionary<string, object?> dict)
|
||||
{
|
||||
var masked = new Dictionary<string, object?>();
|
||||
foreach (var (key, val) in dict)
|
||||
masked[key] = IsSensitive(key) ? MaskValue : val;
|
||||
return masked;
|
||||
}
|
||||
|
||||
var result = new Dictionary<string, object?>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user