first commit
CI/CD / CI · Admin API (dotnet build) (push) Successful in 41s
CI/CD / CI · Admin Web (tsc) (push) Failing after 5s
CI/CD / CI · Website (tsc) (push) Failing after 4s
CI/CD / CI · Koja (tsc) (push) Failing after 5s
CI/CD / CI · API (dotnet build + test) (push) Successful in 1m13s
CI/CD / CI · Dashboard (tsc) (push) Failing after 2m32s
CI/CD / Deploy · all services (push) Has been skipped
CI/CD / CI · Admin API (dotnet build) (push) Successful in 41s
CI/CD / CI · Admin Web (tsc) (push) Failing after 5s
CI/CD / CI · Website (tsc) (push) Failing after 4s
CI/CD / CI · Koja (tsc) (push) Failing after 5s
CI/CD / CI · API (dotnet build + test) (push) Successful in 1m13s
CI/CD / CI · Dashboard (tsc) (push) Failing after 2m32s
CI/CD / Deploy · all services (push) Has been skipped
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
using System.Text.Json;
|
||||
using Meezi.Core.Entities;
|
||||
using Meezi.Core.Interfaces;
|
||||
using Meezi.Infrastructure.Data;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Meezi.API.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Persists audit entries on a fresh, isolated <see cref="AppDbContext"/> so the
|
||||
/// write never participates in (or rolls back with) the caller's transaction, and
|
||||
/// swallows all failures — auditing must never break the recorded operation.
|
||||
/// </summary>
|
||||
public sealed class AuditLogService : IAuditLogService
|
||||
{
|
||||
private static readonly JsonSerializerOptions DetailsJsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
private readonly ITenantContext _tenant;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly ILogger<AuditLogService> _logger;
|
||||
|
||||
public AuditLogService(
|
||||
ITenantContext tenant,
|
||||
IServiceScopeFactory scopeFactory,
|
||||
ILogger<AuditLogService> logger)
|
||||
{
|
||||
_tenant = tenant;
|
||||
_scopeFactory = scopeFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task LogAsync(AuditEntry entry, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var cafeId = _tenant.CafeId;
|
||||
if (string.IsNullOrEmpty(cafeId))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Skipping audit log '{Category}/{Action}' — no cafe context.",
|
||||
entry.Category, entry.Action);
|
||||
return;
|
||||
}
|
||||
|
||||
var log = new AuditLog
|
||||
{
|
||||
CafeId = cafeId,
|
||||
BranchId = entry.BranchId ?? _tenant.BranchId,
|
||||
Category = entry.Category,
|
||||
Action = entry.Action,
|
||||
EntityType = entry.EntityType,
|
||||
EntityId = entry.EntityId,
|
||||
ActorId = _tenant.UserId,
|
||||
ActorName = entry.ActorName,
|
||||
ActorRole = _tenant.Role?.ToString(),
|
||||
Summary = entry.Summary,
|
||||
DetailsJson = entry.Details is null
|
||||
? null
|
||||
: JsonSerializer.Serialize(entry.Details, DetailsJsonOptions)
|
||||
};
|
||||
|
||||
// Fresh scope → fresh DbContext (café-wide, unfiltered) so this write is
|
||||
// independent of the business operation's change-tracker and transaction.
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
db.AuditLogs.Add(log);
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex,
|
||||
"Failed to write audit log '{Category}/{Action}' for entity {EntityType}:{EntityId}.",
|
||||
entry.Category, entry.Action, entry.EntityType, entry.EntityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user