Commit 8b30b5ed authored by Eric Dieckman's avatar Eric Dieckman
Browse files

Audit log working

parent ee08ac4f
Loading
Loading
Loading
Loading
+0 −1
Original line number Diff line number Diff line
@@ -98,7 +98,6 @@ builder.Services.AddSwaggerGen(options =>

    // integrate xml comments
    options.IncludeXmlComments(XmlCommentsFilePath());

});

//// ############# AUTHORIZATION ################
+61 −69
Original line number Diff line number Diff line
@@ -3,7 +3,9 @@ using Cals.Visor.Application;
using Cals.Visor.Application.Identity;
using Cals.Visor.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Metadata;

namespace Cals.Visor.Infrastructure.Contexts;

@@ -12,102 +14,92 @@ public class AuditSaveChangesInterceptor : SaveChangesInterceptor
    private readonly ICurrentUser _currentUser;
    private readonly ICorrelationIdAccessor _correlationIdAccessor;

    // Store audit logs created before SaveChanges
    private readonly List<(EntityEntry Entry, Dictionary<string, object?> Changes, EntityState OriginalState)> _pendingAuditLogs = new();

    public AuditSaveChangesInterceptor(ICurrentUser currentUser, ICorrelationIdAccessor correlationIdAccessor)
    {
        _currentUser = currentUser;
        _correlationIdAccessor = correlationIdAccessor;
    }

    private long? GetUserId()
        => _currentUser.CurrentUser?.Id;

    private string? GetCorrelationId()
        => _correlationIdAccessor.CorrelationId;

    public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
        DbContextEventData eventData,
        InterceptionResult<int> result,
        CancellationToken cancellationToken = default)
    {
        var context = eventData.Context;
        DbContext? context = eventData.Context;
        if (context == null) return base.SavingChangesAsync(eventData, result, cancellationToken);

        var auditLogs = BuildAuditLogs(context);
        _pendingAuditLogs.Clear();

        if (auditLogs.Count > 0)
            context.Set<AuditLog>().AddRange(auditLogs);
        foreach (EntityEntry? entry in context.ChangeTracker.Entries()
            .Where(e => e.Entity is IAuditableEntity &&
                        e.Entity is not AuditLog &&
                        e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted))
        {
            Dictionary<string, object?> changes = CollectChanges(entry);
            if (changes.Count > 0)
                _pendingAuditLogs.Add((entry,changes, entry.State));
        }

        return base.SavingChangesAsync(eventData, result, cancellationToken);
    }
    public override async ValueTask<int> SavedChangesAsync(
        SaveChangesCompletedEventData eventData,
        int result,
        CancellationToken cancellationToken = default)
    {
        DbContext? context = eventData.Context;
        if (context == null || _pendingAuditLogs.Count == 0)
            return await base.SavedChangesAsync(eventData, result, cancellationToken);

    private List<AuditLog> BuildAuditLogs(DbContext context)
        foreach ((EntityEntry? entry, Dictionary<string, object?>? changes, EntityState originalState) in _pendingAuditLogs)
        {
        var entries = context.ChangeTracker.Entries()
            .Where(e => e.Entity is not AuditLog &&
                        e.Entity is IAuditableEntity &&
                        e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted)
            .ToList();
            IEntityType entityType = entry.Metadata;
            PropertyEntry keyProp = entry.Properties.First(p => p.Metadata.IsPrimaryKey());
            string? keyValue = keyProp.CurrentValue?.ToString();

        var userId = GetUserId();
        var correlationId = GetCorrelationId();
            var audit = new AuditLog
            {
                TableName = entityType.GetTableName()!,
                EntityId = keyValue ?? string.Empty,
                Action = originalState.ToString().ToUpperInvariant(),
                AppUserId = _currentUser.CurrentUser?.Id,
                CorrelationId = _correlationIdAccessor.CorrelationId,
                CreatedAt = DateTime.UtcNow,
                ChangesJson = JsonSerializer.Serialize(changes)
            };

        var logs = new List<AuditLog>();
            context.Add(audit);
        }

        foreach (var entry in entries)
        {
            var tableName = context.Model.FindEntityType(entry.Entity.GetType())?.GetTableName()!;
            var keyName = entry.Properties.First(p => p.Metadata.IsPrimaryKey()).Metadata.Name;
            var keyValue = entry.Property(keyName).CurrentValue?.ToString() ?? "";
        // This ensures audit logs are saved with the real key value
        await context.SaveChangesAsync(cancellationToken);

        return await base.SavedChangesAsync(eventData, result, cancellationToken);
    }

    private static Dictionary<string, object?> CollectChanges(EntityEntry entry)
    {
        var changes = new Dictionary<string, object?>();

            foreach (var prop in entry.Properties)
        foreach (PropertyEntry prop in entry.Properties)
        {
                if (prop.IsTemporary) continue;
                if (prop.Metadata.IsPrimaryKey()) continue;
            if (prop.IsTemporary || prop.Metadata.IsPrimaryKey())
                continue;

            if (entry.State == EntityState.Added)
                {
                changes[prop.Metadata.Name] = new { Old = (object?)null, New = prop.CurrentValue };
                }

            else if (entry.State == EntityState.Modified && prop.IsModified)
                {
                    changes[prop.Metadata.Name] = new
                    {
                        Old = prop.OriginalValue,
                        New = prop.CurrentValue
                    };
                }
                changes[prop.Metadata.Name] = new { Old = prop.OriginalValue, New = prop.CurrentValue };

            else if (entry.State == EntityState.Deleted)
                {
                changes[prop.Metadata.Name] = new { Old = prop.OriginalValue, New = (object?)null };
        }
            }

            if (changes.Count == 0)
                continue;

            logs.Add(new AuditLog
            {
                TableName = tableName,
                EntityId = keyValue,
                Action = entry.State.ToString().ToUpper(),
                AppUserId = userId,
                CorrelationId = correlationId,
                CreatedAt = DateTime.UtcNow,
                ChangesJson = JsonSerializer.Serialize(changes)
            });

            //// Update AuditableEntity fields
            //if (entry.Entity is AuditableEntity auditable &&
            //    entry.State == EntityState.Modified)
            //{
            //    auditable.LastModifiedBy = userId;
            //    auditable.LastModifiedUtc = DateTime.UtcNow;
            //}
        }

        return logs;
        return changes;
    }
}
+1 −1
Original line number Diff line number Diff line
namespace Cals.Visor.Models;

public class Area : ICreatedAt, IUpdatedAt
public class Area : ICreatedAt, IUpdatedAt, IAuditableEntity
{
    public long? AreaId { get; set; }

+1 −1
Original line number Diff line number Diff line
namespace Cals.Visor.Models;

public class InfobloxDomain : ICreatedAt, IUpdatedAt
public class InfobloxDomain : ICreatedAt, IUpdatedAt, IAuditableEntity
{
    public long? InfobloxDomainId { get; set; }

+1 −1
Original line number Diff line number Diff line
namespace Cals.Visor.Models;

public class InfobloxGroup : ICreatedAt
public class InfobloxGroup : ICreatedAt, IAuditableEntity
{
    public long? InfobloxGroupId { get; set; }

Loading