72 lines
2.7 KiB
C#
72 lines
2.7 KiB
C#
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;
|
|
}
|
|
}
|