using PostFundManagement.Application.Services.Shared; using PostFundManagement.Domain.Attributes; namespace PostFundManagement.Application.Interceptors; public sealed class AuditInterceptor(AuditService auditService, IHttpContextAccessor httpContextAccessor) : IAsyncInterceptor { public void InterceptSynchronous(IInvocation invocation) => invocation.Proceed(); public void InterceptAsynchronous(IInvocation invocation) => invocation.ReturnValue = InternalInterceptAsynchronous(invocation); public void InterceptAsynchronous(IInvocation invocation) => invocation.ReturnValue = InternalInterceptAsynchronous(invocation); private static async Task InternalInterceptAsynchronous(IInvocation invocation) { invocation.Proceed(); var task = (Task)invocation.ReturnValue; await task.ConfigureAwait(false); } private async Task InternalInterceptAsynchronous(IInvocation invocation) { invocation.Proceed(); var task = (Task)invocation.ReturnValue; var resultObj = await task.ConfigureAwait(false); var method = invocation.MethodInvocationTarget ?? invocation.Method; var auditAttr = method.GetCustomAttribute() ?? invocation.Method.GetCustomAttribute(); if (auditAttr == null) return resultObj; if (resultObj is IResultBase result && result.IsSuccess) { try { var action = ResolveActionName(method.Name); var entityName = ResolveEntityName(method, resultObj, invocation.Arguments); var entityId = ResolveEntityId(resultObj, invocation.Arguments); var changes = BuildPayloadSummary(invocation.Method.GetParameters(), invocation.Arguments); var performedBy = ResolveUserId(); await auditService.LogActionAsync( entityId: entityId, entityName: entityName, action: action, changes: changes, performedBy: performedBy).ConfigureAwait(false); } catch { /* eat all exceptions, don't interrupt program flow */ } } return resultObj; } private static string ResolveActionName(string methodName) => methodName.EndsWith("Async", StringComparison.Ordinal) ? methodName[..^5] : methodName; private static string ResolveEntityName(MethodInfo method, object? resultObj, object[] args) { var returnType = method.ReturnType; Type? innerType = null; if (returnType.IsGenericType) { var genericDef = returnType.GetGenericTypeDefinition(); if (genericDef == typeof(Task<>) || genericDef == typeof(Task<>)) innerType = returnType.GetGenericArguments()[0]; } if (innerType != null && innerType.IsGenericType && typeof(IResultBase).IsAssignableFrom(innerType)) { var valueType = innerType.GetGenericArguments().FirstOrDefault(); if (valueType != null && valueType != typeof(bool) && valueType != typeof(int) && valueType != typeof(string)) return valueType.Name; } foreach (var arg in args) { if (arg == null || arg is CancellationToken or IFormFile) continue; var argTypeName = arg.GetType().Name; if (argTypeName.EndsWith("Request", StringComparison.Ordinal)) { var trimmed = argTypeName[..^7]; return trimmed.Replace("Create", "").Replace("Update", ""); } } return ResolveActionName(method.Name); } private static string ResolveEntityId(object? resultObj, object[] args) { if (resultObj != null) { var valueProp = resultObj.GetType().GetProperty("Value"); if (valueProp != null) { var value = valueProp.GetValue(resultObj); if (value != null) { var idProp = value.GetType().GetProperty("Id") ?? value.GetType().GetProperty("id"); if (idProp != null) { var idVal = idProp.GetValue(value); if (idVal != null) return idVal.ToString()!; } } } } foreach (var arg in args) { if (arg is long or int or Guid or string) return arg.ToString()!; if (arg != null && arg is not CancellationToken and not IFormFile) { var idProp = arg.GetType().GetProperty("Id") ?? arg.GetType().GetProperty("id"); if (idProp != null) { var idVal = idProp.GetValue(arg); if (idVal != null) return idVal.ToString()!; } } } return "N/A"; } private static string[] BuildPayloadSummary(ParameterInfo[] parameters, object[] args) { var entries = new List(); for (var i = 0; i < parameters.Length; i++) { var param = parameters[i]; var arg = args[i]; if (param.ParameterType == typeof(CancellationToken)) continue; if (arg == null) { entries.Add($"{param.Name}: null"); continue; } if (arg is IFormFile file) { entries.Add($"{param.Name}: [File] Name='{file.FileName}', Size={file.Length} bytes, Type='{file.ContentType}'"); continue; } if (arg is IEnumerable files) { var fileSummaries = files.Select(f => $"'{f.FileName}' ({f.Length} bytes)"); entries.Add($"{param.Name}: [Files] count={fileSummaries.Count()} [{string.Join(", ", fileSummaries)}]"); continue; } if (arg is Stream stream) { entries.Add($"{param.Name}: [Stream] Length={(stream.CanSeek ? stream.Length : -1)} bytes"); continue; } try { var json = JsonSerializer.Serialize(arg); entries.Add($"{param.Name}: {json}"); } catch { entries.Add($"{param.Name}: [Unserializable]"); } } return [.. entries]; } private Guid ResolveUserId() { var user = httpContextAccessor.HttpContext?.User; var sub = user?.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? user?.FindFirst("sub")?.Value; return Guid.TryParse(sub, out var guid) ? guid : Guid.Empty; } }