368 lines
12 KiB
C#
368 lines
12 KiB
C#
using System.Reflection;
|
|
using System.Security.Claims;
|
|
using System.Text.Json;
|
|
using Castle.DynamicProxy;
|
|
using FluentResults;
|
|
using Microsoft.AspNetCore.Http;
|
|
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)
|
|
{
|
|
var method = invocation.MethodInvocationTarget ?? invocation.Method;
|
|
var auditAttr = method.GetCustomAttribute<AuditAttribute>()
|
|
?? invocation.Method.GetCustomAttribute<AuditAttribute>();
|
|
|
|
if (auditAttr == null)
|
|
{
|
|
invocation.Proceed();
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
invocation.Proceed();
|
|
AuditAction(invocation, invocation.ReturnValue, null);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
AuditAction(invocation, null, ex);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public void InterceptAsynchronous(IInvocation invocation) =>
|
|
invocation.ReturnValue = InternalInterceptAsynchronous(invocation);
|
|
|
|
public void InterceptAsynchronous<TResult>(IInvocation invocation) =>
|
|
invocation.ReturnValue = InternalInterceptAsynchronous<TResult>(invocation);
|
|
|
|
private async Task InternalInterceptAsynchronous(IInvocation invocation)
|
|
{
|
|
var method = invocation.MethodInvocationTarget ?? invocation.Method;
|
|
var auditAttr = method.GetCustomAttribute<AuditAttribute>()
|
|
?? invocation.Method.GetCustomAttribute<AuditAttribute>();
|
|
|
|
if (auditAttr == null)
|
|
{
|
|
invocation.Proceed();
|
|
var t = (Task)invocation.ReturnValue;
|
|
await t.ConfigureAwait(false);
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
invocation.Proceed();
|
|
var task = (Task)invocation.ReturnValue;
|
|
await task.ConfigureAwait(false);
|
|
await AuditActionAsync(invocation, null, null).ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await AuditActionAsync(invocation, null, ex).ConfigureAwait(false);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private async Task<TResult> InternalInterceptAsynchronous<TResult>(IInvocation invocation)
|
|
{
|
|
var method = invocation.MethodInvocationTarget ?? invocation.Method;
|
|
var auditAttr = method.GetCustomAttribute<AuditAttribute>()
|
|
?? invocation.Method.GetCustomAttribute<AuditAttribute>();
|
|
|
|
if (auditAttr == null)
|
|
{
|
|
invocation.Proceed();
|
|
var t = (Task<TResult>)invocation.ReturnValue;
|
|
return await t.ConfigureAwait(false);
|
|
}
|
|
|
|
TResult resultObj;
|
|
try
|
|
{
|
|
invocation.Proceed();
|
|
var task = (Task<TResult>)invocation.ReturnValue;
|
|
resultObj = await task.ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await AuditActionAsync(invocation, null, ex).ConfigureAwait(false);
|
|
throw;
|
|
}
|
|
|
|
await AuditActionAsync(invocation, resultObj, null).ConfigureAwait(false);
|
|
return resultObj;
|
|
}
|
|
|
|
private async Task AuditActionAsync(IInvocation invocation, object? resultObj, Exception? exception)
|
|
{
|
|
try
|
|
{
|
|
var method = invocation.MethodInvocationTarget ?? invocation.Method;
|
|
var action = ResolveActionName(method.Name);
|
|
var entityName = ResolveEntityName(method, resultObj, invocation.Arguments);
|
|
var entityId = ResolveEntityId(resultObj, invocation.Arguments);
|
|
var changes = BuildPayloadSummary(method.GetParameters(), invocation.Arguments);
|
|
var performedBy = ResolveUserId();
|
|
var tenantId = ResolveTenantId(invocation.Arguments);
|
|
|
|
if (exception != null)
|
|
{
|
|
var errorDetails = $"[Exception] {exception.GetType().Name}: {exception.Message}\n{exception.StackTrace}";
|
|
await auditService.LogActionAsync(
|
|
entityId: entityId,
|
|
entityName: entityName,
|
|
action: action,
|
|
changes: changes,
|
|
performedBy: performedBy,
|
|
error: errorDetails,
|
|
tenantId: tenantId).ConfigureAwait(false);
|
|
return;
|
|
}
|
|
|
|
if (resultObj is IResultBase result)
|
|
{
|
|
if (result.IsSuccess)
|
|
{
|
|
await auditService.LogActionAsync(
|
|
entityId: entityId,
|
|
entityName: entityName,
|
|
action: action,
|
|
changes: changes,
|
|
performedBy: performedBy,
|
|
error: null,
|
|
tenantId: tenantId).ConfigureAwait(false);
|
|
}
|
|
else if (result.IsFailed)
|
|
{
|
|
var errorMessages = new List<string>();
|
|
if (result.Reasons != null)
|
|
{
|
|
foreach (var reason in result.Reasons)
|
|
{
|
|
if (reason is IExceptionalError exceptionalError)
|
|
errorMessages.Add($"{exceptionalError.Message} ({exceptionalError.Exception.GetType().Name}: {exceptionalError.Exception.Message})\n{exceptionalError.Exception.StackTrace}");
|
|
else if (reason is IError error)
|
|
errorMessages.Add(error.Message);
|
|
}
|
|
}
|
|
|
|
var rawErrorType = result.Reasons?.FirstOrDefault()?.GetType().Name.Replace("Error", string.Empty);
|
|
var errorType = string.IsNullOrWhiteSpace(rawErrorType) || rawErrorType == "Reason" ? "Error" : rawErrorType;
|
|
var errorsString = string.Join("; ", errorMessages);
|
|
var errorDetails = $"[{errorType}] {errorsString}";
|
|
|
|
await auditService.LogActionAsync(
|
|
entityId: entityId,
|
|
entityName: entityName,
|
|
action: action,
|
|
changes: changes,
|
|
performedBy: performedBy,
|
|
error: errorDetails,
|
|
tenantId: tenantId).ConfigureAwait(false);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
await auditService.LogActionAsync(
|
|
entityId: entityId,
|
|
entityName: entityName,
|
|
action: action,
|
|
changes: changes,
|
|
performedBy: performedBy,
|
|
error: null,
|
|
tenantId: tenantId).ConfigureAwait(false);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
/* eat all exceptions, don't interrupt program flow */
|
|
}
|
|
}
|
|
|
|
private void AuditAction(IInvocation invocation, object? resultObj, Exception? exception)
|
|
{
|
|
try
|
|
{
|
|
AuditActionAsync(invocation, resultObj, exception).GetAwaiter().GetResult();
|
|
}
|
|
catch
|
|
{
|
|
/* eat all exceptions, don't interrupt program flow */
|
|
}
|
|
}
|
|
|
|
private static long? ResolveTenantId(object[] args)
|
|
{
|
|
foreach (var arg in args)
|
|
{
|
|
if (arg == null || arg is CancellationToken or IFormFile) continue;
|
|
var prop = arg.GetType().GetProperty("TenantId");
|
|
if (prop != null)
|
|
{
|
|
var val = prop.GetValue(arg);
|
|
if (val is long l && l > 0) return l;
|
|
if (val is int i && i > 0) return i;
|
|
if (val is string s && long.TryParse(s, out var parsed) && parsed > 0) return parsed;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
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<>))
|
|
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<string>();
|
|
|
|
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<IFormFile> 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;
|
|
}
|
|
}
|