Implemented centralised audit logging
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
using PostFundManagement.Application.Services.Communications;
|
||||
using PostFundManagement.Domain;
|
||||
using PostFundManagement.Domain.Attributes;
|
||||
using PostFundManagement.Domain.Models;
|
||||
|
||||
namespace PostFundManagement.Application.Abstractions.Services;
|
||||
|
||||
public interface ICommunicationsService
|
||||
{
|
||||
[Audit]
|
||||
ValueTask<Result<Invite>> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Invite[]>> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default);
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
using PostFundManagement.Application.Services.Evidence;
|
||||
using PostFundManagement.Domain;
|
||||
using PostFundManagement.Domain.Attributes;
|
||||
using PostFundManagement.Domain.Models;
|
||||
|
||||
namespace PostFundManagement.Application.Abstractions.Services;
|
||||
|
||||
public interface IEvidenceService
|
||||
{
|
||||
[Audit]
|
||||
ValueTask<Result> CreateEvidenceAsync(Guid userId, IFormCollection form, IFormFile file, CancellationToken cancellationToken = default);
|
||||
|
||||
[Audit]
|
||||
ValueTask<Result> RecordIndicatorActualAmountAsync(string indicatorIdHash, decimal amount, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Indicator[]>> GetIndicatorsAsync(Pagination pagination, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Evidence[]>> GetEvidenceFilesAsync(Pagination pagination, CancellationToken cancellationToken = default);
|
||||
|
||||
[Audit]
|
||||
ValueTask<Result> CreateMilestoneAsync(CreateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
[Audit]
|
||||
ValueTask<Result> CreateIndicatorAsync(CreateIndicatorRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Indicator>> GetIndicatorAsync(string indicatorId, CancellationToken cancellationToken);
|
||||
@@ -26,6 +31,7 @@ public interface IEvidenceService
|
||||
|
||||
ValueTask<Result<Evidence[]>> GetProjectEvidencesAsync(string projectIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
[Audit]
|
||||
ValueTask<Result> UpdateEvidenceAsync(UpdateEvidenceRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<string>> GetEvidenceUrlAsync(string evidenceIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using PostFundManagement.Application.Services.Execution;
|
||||
using PostFundManagement.Domain;
|
||||
using PostFundManagement.Domain.Attributes;
|
||||
using PostFundManagement.Domain.Models;
|
||||
|
||||
namespace PostFundManagement.Application.Abstractions.Services;
|
||||
@@ -10,17 +11,22 @@ public interface IExecutionService
|
||||
|
||||
ValueTask<Result<Disbursement[]>> GetDisbursementsAsync(Pagination pagination, CancellationToken cancellationToken = default);
|
||||
|
||||
[Audit]
|
||||
ValueTask<Result> CreateRiskAsync(CreateRiskRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
[Audit]
|
||||
ValueTask<Result> CreateDisbursementAsync(CreateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
[Audit]
|
||||
ValueTask<Result> ApproveDisbursementAsync(string disbursementIdHash, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Disbursement>> GetDisbursementAsync(string disbursementIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Risk>> GetRiskAsync(string riskIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
[Audit]
|
||||
ValueTask<Result> UpdateRiskAsync(UpdateRiskRequest request, CancellationToken cancellationToken = default);
|
||||
|
||||
[Audit]
|
||||
ValueTask<Result> UpdateDisbursementAsync(UpdateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using PostFundManagement.Application.Services.Governance;
|
||||
using PostFundManagement.Domain;
|
||||
using PostFundManagement.Domain.Attributes;
|
||||
using PostFundManagement.Domain.Models;
|
||||
|
||||
namespace PostFundManagement.Application.Abstractions.Services;
|
||||
@@ -18,12 +19,16 @@ public interface IGovernanceService
|
||||
|
||||
ValueTask<Result<Dashboard>> GetDashboardAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
[Audit]
|
||||
ValueTask<Result> CreateOrganisationAsync(CreateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
[Audit]
|
||||
ValueTask<Result<Organisation>> UpdateOrganisationAsync(UpdateOrganisationRequest request, CancellationToken cancellationToken = default);
|
||||
|
||||
|
||||
[Audit]
|
||||
ValueTask<Result> CreatePortfolioAsync(CreatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
|
||||
[Audit]
|
||||
ValueTask<Result> UpdatePortfolioAsync(UpdatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Programme[]>> GetPortfolioProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using PostFundManagement.Application.Services.Grants;
|
||||
using PostFundManagement.Domain;
|
||||
using PostFundManagement.Domain.Attributes;
|
||||
using PostFundManagement.Domain.Models;
|
||||
|
||||
namespace PostFundManagement.Application.Abstractions.Services;
|
||||
@@ -18,17 +19,23 @@ public interface IGrantsService
|
||||
|
||||
ValueTask<Result<Award>> GetAwardAsync(string awardIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
[Audit]
|
||||
ValueTask<Result> ExecuteContractAsync(ExecuteContractRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
[Audit]
|
||||
ValueTask<Result> CreateContractAsync(CreateContractRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
[Audit]
|
||||
ValueTask<Result> CreateBudgetAsync(CreateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
[Audit]
|
||||
ValueTask<Result> CreateAwardAsync(CreateAwardRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<string>> GetContractUrlAsync(string contractIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
|
||||
[Audit]
|
||||
ValueTask<Result> UploadContractAsync(string contractIdHash, Guid userId, IFormFile file, CancellationToken cancellationToken = default);
|
||||
|
||||
|
||||
[Audit]
|
||||
ValueTask<Result> UpdateContractAsync(UpdateContractRequest request, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
namespace PostFundManagement.Application.Abstractions.Services;
|
||||
using PostFundManagement.Domain.Attributes;
|
||||
|
||||
namespace PostFundManagement.Application.Abstractions.Services;
|
||||
|
||||
public interface IIdentityService
|
||||
{
|
||||
[Audit]
|
||||
ValueTask<Result> UserIsAuthorisedAsync(Guid userId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using PostFundManagement.Application.Services.Projects;
|
||||
using PostFundManagement.Domain;
|
||||
using PostFundManagement.Domain.Attributes;
|
||||
using PostFundManagement.Domain.Models;
|
||||
|
||||
namespace PostFundManagement.Application.Abstractions.Services;
|
||||
|
||||
public interface IProjectService
|
||||
{
|
||||
[Audit]
|
||||
ValueTask<Result> UpdateProjectStatusAsync(UpdateProjectStatusRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Project[]>> GetProjectsAsync(Pagination pagination, CancellationToken cancellationToken = default);
|
||||
@@ -14,8 +16,10 @@ public interface IProjectService
|
||||
|
||||
ValueTask<Result<Domain.Models.Activity[]>> GetActivitiesAsync(string projectIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
[Audit]
|
||||
ValueTask<Result> CreateProjectAsync(CreateProjectRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
[Audit]
|
||||
ValueTask<Result> CreateActivityAsync(CreateActivityRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Milestone[]>> GetProjectMilestonesAsync(string projectIdHash, CancellationToken cancellationToken = default);
|
||||
@@ -26,16 +30,20 @@ public interface IProjectService
|
||||
|
||||
ValueTask<Result<Milestone[]>> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default);
|
||||
|
||||
[Audit]
|
||||
ValueTask<Result> UpdateMilestoneAsync(UpdateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
[Audit]
|
||||
ValueTask<Result> UpdateProjectAsync(UpdateProjectRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Domain.Models.Activity>> GetActivityAsync(string activityIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Risk[]>> GetProjectRisksAsync(string projectIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
[Audit]
|
||||
ValueTask<Result> CreateProgrammeAsync(CreateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
[Audit]
|
||||
ValueTask<Result> UpdateProgrammeAsync(UpdateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Programme>> GetProgrammeAsync(string programmeIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
using PostFundManagement.Application.Services.Users;
|
||||
using PostFundManagement.Domain;
|
||||
using PostFundManagement.Domain.Attributes;
|
||||
using PostFundManagement.Domain.Models;
|
||||
|
||||
namespace PostFundManagement.Application.Abstractions.Services;
|
||||
|
||||
public interface IUserService
|
||||
{
|
||||
[Audit]
|
||||
ValueTask<Result> SyncUserAsync(Guid userId, string email, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<User[]>> GetUsersAsync(Pagination pagination, UserFilterRequest request, CancellationToken cancellationToken = default);
|
||||
|
||||
[Audit]
|
||||
ValueTask<Result> UpdateUserStatusAsync(Guid userId, ActivityStatus status, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<User>> GetUserAsync(Guid userId, CancellationToken cancellationToken);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using PostFundManagement.Application.Abstractions;
|
||||
using PostFundManagement.Application.Abstractions.Services;
|
||||
using PostFundManagement.Application.Interceptors;
|
||||
using PostFundManagement.Application.Services.Communications;
|
||||
using PostFundManagement.Application.Services.Evidence;
|
||||
using PostFundManagement.Application.Services.Execution;
|
||||
@@ -15,32 +16,60 @@ namespace PostFundManagement.Application.Extensions;
|
||||
|
||||
public static class Services
|
||||
{
|
||||
public static IServiceCollection AddAuditedCachedScoped<TInterface, TImplementation, TCacheDecorator>(this IServiceCollection services)
|
||||
where TInterface : class
|
||||
where TImplementation : class, TInterface
|
||||
where TCacheDecorator : class, TInterface
|
||||
{
|
||||
services.AddScoped<TImplementation>();
|
||||
services.AddScoped<TCacheDecorator>();
|
||||
|
||||
services.AddScoped(sp =>
|
||||
{
|
||||
var proxyGenerator = sp.GetRequiredService<ProxyGenerator>();
|
||||
var interceptor = sp.GetRequiredService<AuditInterceptor>();
|
||||
|
||||
var actual = sp.GetRequiredService<TImplementation>();
|
||||
|
||||
var cached = ActivatorUtilities.CreateInstance<TCacheDecorator>(sp, actual);
|
||||
|
||||
return proxyGenerator.CreateInterfaceProxyWithTarget<TInterface>(cached, interceptor.ToInterceptor());
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddAuditedScoped<TInterface, TImplementation>(this IServiceCollection services)
|
||||
where TInterface : class
|
||||
where TImplementation : class, TInterface
|
||||
{
|
||||
services.AddScoped<TImplementation>();
|
||||
services.AddScoped(sp =>
|
||||
{
|
||||
var proxyGenerator = sp.GetRequiredService<ProxyGenerator>();
|
||||
var actual = sp.GetRequiredService<TImplementation>();
|
||||
var interceptor = sp.GetRequiredService<AuditInterceptor>();
|
||||
|
||||
return proxyGenerator.CreateInterfaceProxyWithTarget<TInterface>(actual, interceptor.ToInterceptor());
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddRegisterServicesWithCaching(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddScoped<ICommunicationsService, CommunicationsService>();
|
||||
services.Decorate<ICommunicationsService, CommunicationsCacheService>();
|
||||
services.AddScoped<AuditInterceptor>();
|
||||
services.AddSingleton<ProxyGenerator>();
|
||||
|
||||
services.AddScoped<IEvidenceService, EvidenceService>();
|
||||
services.Decorate<IEvidenceService, EvidenceCacheService>();
|
||||
services.AddAuditedCachedScoped<ICommunicationsService, CommunicationsService, CommunicationsCacheService>();
|
||||
services.AddAuditedCachedScoped<IEvidenceService, EvidenceService, EvidenceCacheService>();
|
||||
services.AddAuditedCachedScoped<IUserService, UserService, UserCacheService>();
|
||||
services.AddAuditedCachedScoped<IIdentityService, IdentityService, IdentityCacheService>();
|
||||
services.AddAuditedCachedScoped<IExecutionService, ExecutionService, ExecutionCacheService>();
|
||||
services.AddAuditedCachedScoped<IGovernanceService, GovernanceService, GovernanceCacheService>();
|
||||
services.AddAuditedCachedScoped<IGrantsService, GrantsService, GrantsCacheService>();
|
||||
services.AddAuditedCachedScoped<IProjectService, ProjectService, ProjectCacheService>();
|
||||
|
||||
services.AddScoped<IUserService, UserService>();
|
||||
services.Decorate<IUserService, UserCacheService>();
|
||||
|
||||
services.AddScoped<IIdentityService, IdentityService>();
|
||||
services.Decorate<IIdentityService, IdentityCacheService>();
|
||||
|
||||
services.AddScoped<IExecutionService, ExecutionService>();
|
||||
services.Decorate<IExecutionService, ExecutionCacheService>();
|
||||
|
||||
services.AddScoped<IGovernanceService, GovernanceService>();
|
||||
services.Decorate<IGovernanceService, GovernanceCacheService>();
|
||||
|
||||
services.AddScoped<IGrantsService, GrantsService>();
|
||||
services.Decorate<IGrantsService, GrantsCacheService>();
|
||||
|
||||
services.AddScoped<IProjectService, ProjectService>();
|
||||
services.Decorate<IProjectService, ProjectCacheService>();
|
||||
|
||||
services.AddScoped<IToolService, ToolService>();
|
||||
services.Decorate<IToolService, ToolCacheService>();
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ public sealed class ActiveUserFilter(IIdentityService identityService) : IEndpoi
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(path) && !path!.EndsWith("integration/sync", StringComparison.InvariantCultureIgnoreCase))
|
||||
if ((await identityService.UserIsAuthorisedAsync(userId, context.HttpContext.RequestAborted)).IsFailed)
|
||||
return (object?)Results.Json(new { message = "Account approval is pending or inactive." }, statusCode: StatusCodes.Status403Forbidden);
|
||||
return Results.Json(new { message = "Account approval is pending or inactive." }, statusCode: StatusCodes.Status403Forbidden);
|
||||
|
||||
context.HttpContext.Items[UserIdItemKey] = userId;
|
||||
context.HttpContext.Items[EmailtemKey] = email;
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
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<TResult>(IInvocation invocation) =>
|
||||
invocation.ReturnValue = InternalInterceptAsynchronous<TResult>(invocation);
|
||||
|
||||
private static async Task InternalInterceptAsynchronous(IInvocation invocation)
|
||||
{
|
||||
invocation.Proceed();
|
||||
|
||||
var task = (Task)invocation.ReturnValue;
|
||||
|
||||
await task.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<TResult> InternalInterceptAsynchronous<TResult>(IInvocation invocation)
|
||||
{
|
||||
invocation.Proceed();
|
||||
|
||||
var task = (Task<TResult>)invocation.ReturnValue;
|
||||
var resultObj = await task.ConfigureAwait(false);
|
||||
|
||||
var method = invocation.MethodInvocationTarget ?? invocation.Method;
|
||||
var auditAttr = method.GetCustomAttribute<AuditAttribute>()
|
||||
?? invocation.Method.GetCustomAttribute<AuditAttribute>();
|
||||
|
||||
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(ValueTask<>))
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
<!-- API Documentation -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Castle.Core.AsyncInterceptor" Version="2.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.12" />
|
||||
<PackageReference Include="Microsoft.Extensions.Validation" Version="10.0.12" />
|
||||
<PackageReference Include="Scalar.AspNetCore" Version="2.17.4" />
|
||||
@@ -36,6 +37,7 @@
|
||||
<!-- Shared Usings -->
|
||||
<ItemGroup>
|
||||
<Using Include="Humanizer" />
|
||||
<Using Include="Castle.DynamicProxy" />
|
||||
<Using Include="Microsoft.FeatureManagement" />
|
||||
<Using Include="System.ComponentModel.DataAnnotations" />
|
||||
<Using Include="Microsoft.Extensions.Caching.Distributed" />
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using PostFundManagement.Domain.Abstractions;
|
||||
using PostFundManagement.Domain.Errors;
|
||||
using PostFundManagement.Infrastructure.Database;
|
||||
|
||||
namespace PostFundManagement.Application.Services.Shared;
|
||||
|
||||
public sealed class AuditService(IDbContextFactory<ApplicationDbContext> contextFactory, ITenantContextService tenantContextService) : IService
|
||||
{
|
||||
public async ValueTask<Result> LogActionAsync(string entityId, string entityName, string action, string[] changes, Guid performedBy, CancellationToken cancellationToken = default) =>
|
||||
await Result.Try((Func<ValueTask<Result>>)(async () =>
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
context.Add(new Domain.Entities.AuditLog
|
||||
{
|
||||
EntityId = entityId,
|
||||
Action = action,
|
||||
Changes = changes,
|
||||
EntityName = entityName,
|
||||
PerformedBy = performedBy,
|
||||
Timestamp = DateTime.UtcNow,
|
||||
TenantId = tenantContextService.TenantId > 0 ? tenantContextService.TenantId : null,
|
||||
});
|
||||
|
||||
return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New);
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user