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