From fa427e51c703caa076c3f82ced5a1f4d0e5f8fe7 Mon Sep 17 00:00:00 2001 From: RafaelKC Date: Tue, 2 Dec 2025 22:33:50 -0300 Subject: [PATCH 1/4] FIN-74 wip adding audit logs --- Fin-Backend.sln.DotSettings.user | 1 + Fin.Api/appsettings.json | 3 +- Fin.Infrastructure/Audits/AuditEntry.cs | 17 ++++ Fin.Infrastructure/Audits/AuditLogDocument.cs | 30 ++++++ .../Audits/AuditLogExtensions.cs | 21 ++++ .../Audits/AuditLogInterceptor.cs | 98 +++++++++++++++++++ .../Audits/Enums/AuditLogAction.cs | 8 ++ .../Audits/Enums/MongoAuditLogService.cs | 17 ++++ .../Audits/Interfaces/IAuditLogService.cs | 6 ++ .../Audits/Interfaces/ILoggable.cs | 7 ++ .../Extensions/AddDatabaseExtension.cs | 7 +- .../Extensions/AddInfrastructureExtension.cs | 2 + Fin.Infrastructure/Fin.Infrastructure.csproj | 1 + docker-compose.yml | 9 ++ 14 files changed, 224 insertions(+), 3 deletions(-) create mode 100644 Fin.Infrastructure/Audits/AuditEntry.cs create mode 100644 Fin.Infrastructure/Audits/AuditLogDocument.cs create mode 100644 Fin.Infrastructure/Audits/AuditLogExtensions.cs create mode 100644 Fin.Infrastructure/Audits/AuditLogInterceptor.cs create mode 100644 Fin.Infrastructure/Audits/Enums/AuditLogAction.cs create mode 100644 Fin.Infrastructure/Audits/Enums/MongoAuditLogService.cs create mode 100644 Fin.Infrastructure/Audits/Interfaces/IAuditLogService.cs create mode 100644 Fin.Infrastructure/Audits/Interfaces/ILoggable.cs diff --git a/Fin-Backend.sln.DotSettings.user b/Fin-Backend.sln.DotSettings.user index 18eaaeb..90bbb0a 100644 --- a/Fin-Backend.sln.DotSettings.user +++ b/Fin-Backend.sln.DotSettings.user @@ -20,6 +20,7 @@ <Project Location="/home/rafaelchicovis/git/fin-backend/Fin.Test" Presentation="&lt;Fin.Test&gt;" /> </SessionState> True + False True False diff --git a/Fin.Api/appsettings.json b/Fin.Api/appsettings.json index b2ab7e9..6a2c99d 100644 --- a/Fin.Api/appsettings.json +++ b/Fin.Api/appsettings.json @@ -6,7 +6,8 @@ } }, "ConnectionStrings": { - "DefaultConnection": "Host=localhost;Port=5432;Database=fin_app;Username=fin_app;Password=fin_app" + "DefaultConnection": "Host=localhost;Port=5432;Database=fin_app;Username=fin_app;Password=fin_app", + "MongoDbConnection": "mongodb://localhost:27017" }, "AllowedHosts": "*", "ApiSettings": { diff --git a/Fin.Infrastructure/Audits/AuditEntry.cs b/Fin.Infrastructure/Audits/AuditEntry.cs new file mode 100644 index 0000000..beeb0f2 --- /dev/null +++ b/Fin.Infrastructure/Audits/AuditEntry.cs @@ -0,0 +1,17 @@ +using Fin.Infrastructure.AmbientDatas; +using Fin.Infrastructure.Audits.Interfaces; +using Microsoft.EntityFrameworkCore.ChangeTracking; + +namespace Fin.Infrastructure.Audits; + +public class AuditEntry(EntityEntry entry, IAmbientData ambientData) +{ + public EntityEntry Entry { get; } = entry; + public Guid UserId { get; } = ambientData.UserId.GetValueOrDefault(); + public string TableName { get; } = entry.Entity.GetType().Name; + + public Dictionary KeyValues { get; } = new(); + public object Snapshot { get; } = entry.Entity.GetLogSnapshot(); + public Dictionary PreviousValues { get; } = new(); + public List TemporaryProperties { get; } = new(); +} \ No newline at end of file diff --git a/Fin.Infrastructure/Audits/AuditLogDocument.cs b/Fin.Infrastructure/Audits/AuditLogDocument.cs new file mode 100644 index 0000000..5caa856 --- /dev/null +++ b/Fin.Infrastructure/Audits/AuditLogDocument.cs @@ -0,0 +1,30 @@ +using Fin.Infrastructure.Audits.Enums; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace Fin.Infrastructure.Audits; + +public class AuditLogDocument +{ + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string InternalId { get; set; } + + public string EntityName { get; set; } + public Dictionary KeyValues { get; set; } = new(); + public AuditLogAction Action { get; set; } + public Guid UserId { get; set; } + public DateTime DateTime { get; set; } + + public object Snapshot { get; set; } + public Dictionary PreviousValues { get; set; } + + public string EntityIdString + { + get + { + if (KeyValues == null || !KeyValues.Any()) return null; + return KeyValues.Count == 1 ? KeyValues.Values.First().ToString() : string.Join("|", KeyValues.Select(kvp => $"{kvp.Key}:{kvp.Value}")); + } + } +} \ No newline at end of file diff --git a/Fin.Infrastructure/Audits/AuditLogExtensions.cs b/Fin.Infrastructure/Audits/AuditLogExtensions.cs new file mode 100644 index 0000000..4c46205 --- /dev/null +++ b/Fin.Infrastructure/Audits/AuditLogExtensions.cs @@ -0,0 +1,21 @@ +using Fin.Infrastructure.Audits.Enums; +using Fin.Infrastructure.Audits.Interfaces; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Driver; + +namespace Fin.Infrastructure.Audits; + +public static class AuditLogExtensions +{ + public static IServiceCollection AddAuditLog(this IServiceCollection services, IConfiguration configuration) + { + services.AddSingleton(_ => new MongoClient(configuration.GetConnectionString("MongoDbConnection"))); + services.AddScoped(sp => sp.GetRequiredService().GetDatabase("LogsDB")); + services.AddScoped(); + + services.AddScoped(); + + return services; + } +} \ No newline at end of file diff --git a/Fin.Infrastructure/Audits/AuditLogInterceptor.cs b/Fin.Infrastructure/Audits/AuditLogInterceptor.cs new file mode 100644 index 0000000..bcd05f1 --- /dev/null +++ b/Fin.Infrastructure/Audits/AuditLogInterceptor.cs @@ -0,0 +1,98 @@ +using Fin.Infrastructure.AmbientDatas; +using Fin.Infrastructure.Audits.Enums; +using Fin.Infrastructure.Audits.Interfaces; +using Fin.Infrastructure.DateTimes; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; + +namespace Fin.Infrastructure.Audits; + +public class AuditLogInterceptor( + IAuditLogService auditService, + IAmbientData ambientData, + IDateTimeProvider dateTimeProvider + ) : SaveChangesInterceptor +{ + private readonly List _temporaryAuditEntries = new(); + + public override ValueTask> SavingChangesAsync( + DbContextEventData eventData, + InterceptionResult result, + CancellationToken cancellationToken = default) + { + _temporaryAuditEntries.Clear(); + var context = eventData.Context; + if (context == null) return base.SavingChangesAsync(eventData, result, cancellationToken); + + context.ChangeTracker.DetectChanges(); + + foreach (var entry in context.ChangeTracker.Entries()) + { + if (entry.State is EntityState.Detached or EntityState.Unchanged) continue; + + var auditEntry = new AuditEntry(entry, ambientData); + _temporaryAuditEntries.Add(auditEntry); + + foreach (var property in entry.Properties) + { + if (property.IsTemporary) + { + auditEntry.TemporaryProperties.Add(property); + continue; + } + + if (entry.State == EntityState.Modified && property.IsModified) + { + auditEntry.PreviousValues[property.Metadata.Name] = property.OriginalValue; + } + } + } + + return base.SavingChangesAsync(eventData, result, cancellationToken); + } + + public override async ValueTask SavedChangesAsync( + SaveChangesCompletedEventData eventData, + int result, + CancellationToken cancellationToken = default) + { + if (_temporaryAuditEntries == null || _temporaryAuditEntries.Count == 0) + return await base.SavedChangesAsync(eventData, result, cancellationToken); + + var mongoLogs = new List(); + + foreach (var entry in _temporaryAuditEntries) + { + foreach (var prop in entry.TemporaryProperties.Where(prop => prop.Metadata.IsPrimaryKey())) + { + entry.KeyValues[prop.Metadata.Name] = prop.CurrentValue; + } + + var action = AuditLogAction.Updated; + switch (entry.Entry.State) + { + case EntityState.Deleted: + action = AuditLogAction.Deleted; + break; + case EntityState.Added: + action = AuditLogAction.Created; + break; + } + + mongoLogs.Add(new AuditLogDocument + { + EntityName = entry.TableName, + Action = action, + UserId = entry.UserId, + DateTime = dateTimeProvider.UtcNow(), + Snapshot = entry.Snapshot, + PreviousValues = entry.PreviousValues.Any() ? entry.PreviousValues : null, + KeyValues = entry.KeyValues + }); + } + + await auditService.LogAsync(mongoLogs); + _temporaryAuditEntries.Clear(); + return await base.SavedChangesAsync(eventData, result, cancellationToken); + } +} \ No newline at end of file diff --git a/Fin.Infrastructure/Audits/Enums/AuditLogAction.cs b/Fin.Infrastructure/Audits/Enums/AuditLogAction.cs new file mode 100644 index 0000000..fd38f9e --- /dev/null +++ b/Fin.Infrastructure/Audits/Enums/AuditLogAction.cs @@ -0,0 +1,8 @@ +namespace Fin.Infrastructure.Audits.Enums; + +public enum AuditLogAction +{ + Created = 1, + Updated = 2, + Deleted = 3 +} \ No newline at end of file diff --git a/Fin.Infrastructure/Audits/Enums/MongoAuditLogService.cs b/Fin.Infrastructure/Audits/Enums/MongoAuditLogService.cs new file mode 100644 index 0000000..aa7ae18 --- /dev/null +++ b/Fin.Infrastructure/Audits/Enums/MongoAuditLogService.cs @@ -0,0 +1,17 @@ +using Fin.Infrastructure.Audits.Interfaces; +using MongoDB.Driver; + +namespace Fin.Infrastructure.Audits.Enums; + +public class MongoAuditLogService(IMongoDatabase database) : IAuditLogService +{ + private readonly IMongoCollection _collection = database.GetCollection("audit_logs"); + + public async Task LogAsync(List logs) + { + if (logs != null && logs.Any()) + { + await _collection.InsertManyAsync(logs); + } + } +} \ No newline at end of file diff --git a/Fin.Infrastructure/Audits/Interfaces/IAuditLogService.cs b/Fin.Infrastructure/Audits/Interfaces/IAuditLogService.cs new file mode 100644 index 0000000..a2829b7 --- /dev/null +++ b/Fin.Infrastructure/Audits/Interfaces/IAuditLogService.cs @@ -0,0 +1,6 @@ +namespace Fin.Infrastructure.Audits.Interfaces; + +public interface IAuditLogService +{ + Task LogAsync(List logs); +} \ No newline at end of file diff --git a/Fin.Infrastructure/Audits/Interfaces/ILoggable.cs b/Fin.Infrastructure/Audits/Interfaces/ILoggable.cs new file mode 100644 index 0000000..da74d4b --- /dev/null +++ b/Fin.Infrastructure/Audits/Interfaces/ILoggable.cs @@ -0,0 +1,7 @@ +namespace Fin.Infrastructure.Audits.Interfaces; + +public interface ILoggable +{ + object GetLogSnapshot(); + string GetLogId(); +} \ No newline at end of file diff --git a/Fin.Infrastructure/Database/Extensions/AddDatabaseExtension.cs b/Fin.Infrastructure/Database/Extensions/AddDatabaseExtension.cs index ecd6708..aaadffb 100644 --- a/Fin.Infrastructure/Database/Extensions/AddDatabaseExtension.cs +++ b/Fin.Infrastructure/Database/Extensions/AddDatabaseExtension.cs @@ -1,4 +1,5 @@ -using Fin.Infrastructure.Database.Interceptors; +using Fin.Infrastructure.Audits; +using Fin.Infrastructure.Database.Interceptors; using Fin.Infrastructure.Database.Repositories; using Microsoft.AspNetCore.Builder; using Microsoft.EntityFrameworkCore; @@ -19,11 +20,13 @@ public static IServiceCollection AddDatabase(this IServiceCollection services, I { var auditedEntityInterceptor = serviceProvider.GetRequiredService(); var tenantEntityInterceptor = serviceProvider.GetRequiredService(); + var auditLogInterceptor = serviceProvider.GetRequiredService(); op .UseNpgsql(configuration.GetConnectionString("DefaultConnection")) .AddInterceptors(auditedEntityInterceptor) - .AddInterceptors(tenantEntityInterceptor); + .AddInterceptors(tenantEntityInterceptor) + .AddInterceptors(auditLogInterceptor); }) .AddScoped(typeof(IRepository<>), typeof(Repository<>));; diff --git a/Fin.Infrastructure/Extensions/AddInfrastructureExtension.cs b/Fin.Infrastructure/Extensions/AddInfrastructureExtension.cs index e95fb8c..b25320a 100644 --- a/Fin.Infrastructure/Extensions/AddInfrastructureExtension.cs +++ b/Fin.Infrastructure/Extensions/AddInfrastructureExtension.cs @@ -1,4 +1,5 @@ using Fin.Infrastructure.AmbientDatas; +using Fin.Infrastructure.Audits; using Fin.Infrastructure.Authentications; using Fin.Infrastructure.AutoServices.Extensions; using Fin.Infrastructure.BackgroundJobs; @@ -26,6 +27,7 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi .AddScoped() .AddScoped() .AddScoped() + .AddAuditLog(configuration) .AddDatabase(configuration) .AddFirebase(configuration) .AddSeeders() diff --git a/Fin.Infrastructure/Fin.Infrastructure.csproj b/Fin.Infrastructure/Fin.Infrastructure.csproj index 7d78e06..d10c34f 100644 --- a/Fin.Infrastructure/Fin.Infrastructure.csproj +++ b/Fin.Infrastructure/Fin.Infrastructure.csproj @@ -26,6 +26,7 @@ + diff --git a/docker-compose.yml b/docker-compose.yml index baa4cf6..4baca1f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,6 +32,15 @@ services: interval: 10s timeout: 3s retries: 5 + + mongodb: + image: mongo:latest + container_name: mongodb + restart: always + ports: + - "27017:27017" + volumes: + - mongo-data:/data/db volumes: postgres_data: From 0b7c2ba005cd778c6cece28a921aa01defbde2c2 Mon Sep 17 00:00:00 2001 From: RafaelKC Date: Tue, 2 Dec 2025 23:14:08 -0300 Subject: [PATCH 2/4] FIN-74 WIP audit --- Fin.Api/Program.cs | 9 +++++++ Fin.Domain/Global/Interfaces/ILoggable.cs | 6 +++++ Fin.Domain/Titles/Entities/Title.cs | 27 ++++++++++++++++++- Fin.Infrastructure/Audits/AuditEntry.cs | 2 ++ Fin.Infrastructure/Audits/AuditLogDocument.cs | 7 +++-- .../Audits/AuditLogExtensions.cs | 6 ++++- .../Audits/AuditLogInterceptor.cs | 17 ++++++++++-- .../Audits/Interfaces/ILoggable.cs | 7 ----- 8 files changed, 68 insertions(+), 13 deletions(-) create mode 100644 Fin.Domain/Global/Interfaces/ILoggable.cs delete mode 100644 Fin.Infrastructure/Audits/Interfaces/ILoggable.cs diff --git a/Fin.Api/Program.cs b/Fin.Api/Program.cs index c6baa27..f2ff6b3 100644 --- a/Fin.Api/Program.cs +++ b/Fin.Api/Program.cs @@ -4,8 +4,17 @@ using Fin.Infrastructure.Extensions; using Fin.Infrastructure.Seeders.Extensions; using Hangfire; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; using NSwag; +try +{ + BsonSerializer.RegisterSerializer(new GuidSerializer(BsonType.String)); +} +catch (BsonSerializationException) {} + var builder = WebApplication.CreateBuilder(args); var frontEndUrl = builder.Configuration.GetSection(AppConstants.FrontUrlConfigKey).Get(); diff --git a/Fin.Domain/Global/Interfaces/ILoggable.cs b/Fin.Domain/Global/Interfaces/ILoggable.cs new file mode 100644 index 0000000..d79d891 --- /dev/null +++ b/Fin.Domain/Global/Interfaces/ILoggable.cs @@ -0,0 +1,6 @@ +namespace Fin.Domain.Global.Interfaces; + +public interface ILoggable +{ + object GetLogSnapshot(); +} \ No newline at end of file diff --git a/Fin.Domain/Titles/Entities/Title.cs b/Fin.Domain/Titles/Entities/Title.cs index 63c17a7..e5791ff 100644 --- a/Fin.Domain/Titles/Entities/Title.cs +++ b/Fin.Domain/Titles/Entities/Title.cs @@ -9,7 +9,7 @@ namespace Fin.Domain.Titles.Entities; -public class Title: IAuditedTenantEntity +public class Title: IAuditedTenantEntity, ILoggable { public decimal Value { get; set; } public TitleType Type { get; set; } @@ -143,4 +143,29 @@ public List SyncPeopleAndReturnToRemove(List tit return titlePeopleToDelete; } + + public object GetLogSnapshot() + { + return new + { + Id = Id, + Date = Date, + Description = Description, + + Type = Type, + TypeDescription = Type.ToString(), // TODO implement a to description + OriginalValue = Value, + EffectiveValue = EffectiveValue, + PreviousBalance = PreviousBalance, + ResultingBalance = ResultingBalance, + + WalletId = WalletId, + TenantId = TenantId, + + Categories = TitleCategories?.Select(c => new { c.Id, c.Name }).ToList(), + People = TitlePeople?.Select(p => new { p.Percentage, Id = p.PersonId, p.Person?.Name }).ToList(), + CreatedBy = CreatedBy, + CreatedAt = CreatedAt + }; + } } \ No newline at end of file diff --git a/Fin.Infrastructure/Audits/AuditEntry.cs b/Fin.Infrastructure/Audits/AuditEntry.cs index beeb0f2..eb4b560 100644 --- a/Fin.Infrastructure/Audits/AuditEntry.cs +++ b/Fin.Infrastructure/Audits/AuditEntry.cs @@ -1,3 +1,4 @@ +using Fin.Domain.Global.Interfaces; using Fin.Infrastructure.AmbientDatas; using Fin.Infrastructure.Audits.Interfaces; using Microsoft.EntityFrameworkCore.ChangeTracking; @@ -8,6 +9,7 @@ public class AuditEntry(EntityEntry entry, IAmbientData ambientData) { public EntityEntry Entry { get; } = entry; public Guid UserId { get; } = ambientData.UserId.GetValueOrDefault(); + public Guid TenantId { get; } = ambientData.TenantId.GetValueOrDefault(); public string TableName { get; } = entry.Entity.GetType().Name; public Dictionary KeyValues { get; } = new(); diff --git a/Fin.Infrastructure/Audits/AuditLogDocument.cs b/Fin.Infrastructure/Audits/AuditLogDocument.cs index 5caa856..37c83c1 100644 --- a/Fin.Infrastructure/Audits/AuditLogDocument.cs +++ b/Fin.Infrastructure/Audits/AuditLogDocument.cs @@ -11,13 +11,16 @@ public class AuditLogDocument public string InternalId { get; set; } public string EntityName { get; set; } + [BsonIgnore] public Dictionary KeyValues { get; set; } = new(); public AuditLogAction Action { get; set; } - public Guid UserId { get; set; } public DateTime DateTime { get; set; } + public Guid UserId { get; set; } + public Guid TenantId { get; set; } + public object Snapshot { get; set; } - public Dictionary PreviousValues { get; set; } + public BsonDocument PreviousValues { get; set; } public string EntityIdString { diff --git a/Fin.Infrastructure/Audits/AuditLogExtensions.cs b/Fin.Infrastructure/Audits/AuditLogExtensions.cs index 4c46205..91f35a0 100644 --- a/Fin.Infrastructure/Audits/AuditLogExtensions.cs +++ b/Fin.Infrastructure/Audits/AuditLogExtensions.cs @@ -10,12 +10,16 @@ public static class AuditLogExtensions { public static IServiceCollection AddAuditLog(this IServiceCollection services, IConfiguration configuration) { - services.AddSingleton(_ => new MongoClient(configuration.GetConnectionString("MongoDbConnection"))); + var settings = MongoClientSettings.FromConnectionString(configuration.GetConnectionString("MongoDbConnection")); + var mongoClient = new MongoClient(settings); + + services.AddSingleton(_ => mongoClient); services.AddScoped(sp => sp.GetRequiredService().GetDatabase("LogsDB")); services.AddScoped(); services.AddScoped(); + return services; } } \ No newline at end of file diff --git a/Fin.Infrastructure/Audits/AuditLogInterceptor.cs b/Fin.Infrastructure/Audits/AuditLogInterceptor.cs index bcd05f1..0918bdd 100644 --- a/Fin.Infrastructure/Audits/AuditLogInterceptor.cs +++ b/Fin.Infrastructure/Audits/AuditLogInterceptor.cs @@ -1,9 +1,11 @@ +using Fin.Domain.Global.Interfaces; using Fin.Infrastructure.AmbientDatas; using Fin.Infrastructure.Audits.Enums; using Fin.Infrastructure.Audits.Interfaces; using Fin.Infrastructure.DateTimes; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; +using MongoDB.Bson; namespace Fin.Infrastructure.Audits; @@ -43,7 +45,7 @@ public override ValueTask> SavingChangesAsync( if (entry.State == EntityState.Modified && property.IsModified) { - auditEntry.PreviousValues[property.Metadata.Name] = property.OriginalValue; + auditEntry.PreviousValues[property.Metadata.Name] = ConvertToBsonValid(property.OriginalValue); } } } @@ -84,9 +86,10 @@ public override async ValueTask SavedChangesAsync( EntityName = entry.TableName, Action = action, UserId = entry.UserId, + TenantId = entry.TenantId, DateTime = dateTimeProvider.UtcNow(), Snapshot = entry.Snapshot, - PreviousValues = entry.PreviousValues.Any() ? entry.PreviousValues : null, + PreviousValues = entry.PreviousValues.Any() ? entry.PreviousValues.ToBsonDocument() : null, KeyValues = entry.KeyValues }); } @@ -95,4 +98,14 @@ public override async ValueTask SavedChangesAsync( _temporaryAuditEntries.Clear(); return await base.SavedChangesAsync(eventData, result, cancellationToken); } + + private static object ConvertToBsonValid(object value) + { + return value switch + { + null => null, + Guid guid => guid.ToString(), + _ => value + }; + } } \ No newline at end of file diff --git a/Fin.Infrastructure/Audits/Interfaces/ILoggable.cs b/Fin.Infrastructure/Audits/Interfaces/ILoggable.cs deleted file mode 100644 index da74d4b..0000000 --- a/Fin.Infrastructure/Audits/Interfaces/ILoggable.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Fin.Infrastructure.Audits.Interfaces; - -public interface ILoggable -{ - object GetLogSnapshot(); - string GetLogId(); -} \ No newline at end of file From 0b081db35265e281be8686885590215e33f32d17 Mon Sep 17 00:00:00 2001 From: RafaelKC Date: Wed, 3 Dec 2025 14:15:46 -0300 Subject: [PATCH 3/4] FIN-74 refact log --- Fin-Backend.sln.DotSettings.user | 1 + Fin.Api/Program.cs | 9 - Fin.Domain/Global/Interfaces/ILoggable.cs | 2 +- Fin.Domain/Titles/Entities/Title.cs | 4 +- Fin.Infrastructure/Audits/AuditEntry.cs | 18 +- Fin.Infrastructure/Audits/AuditLogDocument.cs | 19 +- .../Audits/AuditLogExtensions.cs | 10 +- .../Audits/AuditLogInterceptor.cs | 189 +++++++++++------- .../Audits/Enums/AuditLogAction.cs | 6 +- .../Audits/Enums/MongoAuditLogService.cs | 17 -- .../Audits/Interfaces/IAuditLogService.cs | 3 +- .../Audits/MongoAuditLogService.cs | 64 ++++++ 12 files changed, 212 insertions(+), 130 deletions(-) delete mode 100644 Fin.Infrastructure/Audits/Enums/MongoAuditLogService.cs create mode 100644 Fin.Infrastructure/Audits/MongoAuditLogService.cs diff --git a/Fin-Backend.sln.DotSettings.user b/Fin-Backend.sln.DotSettings.user index 90bbb0a..1def2b8 100644 --- a/Fin-Backend.sln.DotSettings.user +++ b/Fin-Backend.sln.DotSettings.user @@ -4,6 +4,7 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded diff --git a/Fin.Api/Program.cs b/Fin.Api/Program.cs index f2ff6b3..c6baa27 100644 --- a/Fin.Api/Program.cs +++ b/Fin.Api/Program.cs @@ -4,17 +4,8 @@ using Fin.Infrastructure.Extensions; using Fin.Infrastructure.Seeders.Extensions; using Hangfire; -using MongoDB.Bson; -using MongoDB.Bson.Serialization; -using MongoDB.Bson.Serialization.Serializers; using NSwag; -try -{ - BsonSerializer.RegisterSerializer(new GuidSerializer(BsonType.String)); -} -catch (BsonSerializationException) {} - var builder = WebApplication.CreateBuilder(args); var frontEndUrl = builder.Configuration.GetSection(AppConstants.FrontUrlConfigKey).Get(); diff --git a/Fin.Domain/Global/Interfaces/ILoggable.cs b/Fin.Domain/Global/Interfaces/ILoggable.cs index d79d891..bd466e6 100644 --- a/Fin.Domain/Global/Interfaces/ILoggable.cs +++ b/Fin.Domain/Global/Interfaces/ILoggable.cs @@ -2,5 +2,5 @@ namespace Fin.Domain.Global.Interfaces; public interface ILoggable { - object GetLogSnapshot(); + object GetLog(); } \ No newline at end of file diff --git a/Fin.Domain/Titles/Entities/Title.cs b/Fin.Domain/Titles/Entities/Title.cs index e5791ff..a5a53c6 100644 --- a/Fin.Domain/Titles/Entities/Title.cs +++ b/Fin.Domain/Titles/Entities/Title.cs @@ -144,7 +144,7 @@ public List SyncPeopleAndReturnToRemove(List tit return titlePeopleToDelete; } - public object GetLogSnapshot() + public object GetLog() { return new { @@ -162,8 +162,6 @@ public object GetLogSnapshot() WalletId = WalletId, TenantId = TenantId, - Categories = TitleCategories?.Select(c => new { c.Id, c.Name }).ToList(), - People = TitlePeople?.Select(p => new { p.Percentage, Id = p.PersonId, p.Person?.Name }).ToList(), CreatedBy = CreatedBy, CreatedAt = CreatedAt }; diff --git a/Fin.Infrastructure/Audits/AuditEntry.cs b/Fin.Infrastructure/Audits/AuditEntry.cs index eb4b560..06b4235 100644 --- a/Fin.Infrastructure/Audits/AuditEntry.cs +++ b/Fin.Infrastructure/Audits/AuditEntry.cs @@ -1,19 +1,19 @@ using Fin.Domain.Global.Interfaces; using Fin.Infrastructure.AmbientDatas; +using Fin.Infrastructure.Audits.Enums; using Fin.Infrastructure.Audits.Interfaces; using Microsoft.EntityFrameworkCore.ChangeTracking; namespace Fin.Infrastructure.Audits; -public class AuditEntry(EntityEntry entry, IAmbientData ambientData) +public class AuditEntry(IAmbientData ambientData) { - public EntityEntry Entry { get; } = entry; - public Guid UserId { get; } = ambientData.UserId.GetValueOrDefault(); - public Guid TenantId { get; } = ambientData.TenantId.GetValueOrDefault(); - public string TableName { get; } = entry.Entity.GetType().Name; + public string EntityName { get; set; } + public string EntityId { get; set; } + public object NewValue { get; set; } + public object OldValue { get; set; } - public Dictionary KeyValues { get; } = new(); - public object Snapshot { get; } = entry.Entity.GetLogSnapshot(); - public Dictionary PreviousValues { get; } = new(); - public List TemporaryProperties { get; } = new(); + public AuditLogAction Action { get; set; } + public Guid UserId { get; set; } = ambientData.UserId.GetValueOrDefault(); + public Guid TenantId { get; set; } = ambientData.TenantId.GetValueOrDefault(); } \ No newline at end of file diff --git a/Fin.Infrastructure/Audits/AuditLogDocument.cs b/Fin.Infrastructure/Audits/AuditLogDocument.cs index 37c83c1..e20a572 100644 --- a/Fin.Infrastructure/Audits/AuditLogDocument.cs +++ b/Fin.Infrastructure/Audits/AuditLogDocument.cs @@ -11,23 +11,14 @@ public class AuditLogDocument public string InternalId { get; set; } public string EntityName { get; set; } - [BsonIgnore] - public Dictionary KeyValues { get; set; } = new(); + public string EntityId { get; set; } + public AuditLogAction Action { get; set; } public DateTime DateTime { get; set; } + public object NewValue { get; set; } + public object OldValue { get; set; } + public Guid UserId { get; set; } public Guid TenantId { get; set; } - - public object Snapshot { get; set; } - public BsonDocument PreviousValues { get; set; } - - public string EntityIdString - { - get - { - if (KeyValues == null || !KeyValues.Any()) return null; - return KeyValues.Count == 1 ? KeyValues.Values.First().ToString() : string.Join("|", KeyValues.Select(kvp => $"{kvp.Key}:{kvp.Value}")); - } - } } \ No newline at end of file diff --git a/Fin.Infrastructure/Audits/AuditLogExtensions.cs b/Fin.Infrastructure/Audits/AuditLogExtensions.cs index 91f35a0..4ca34d0 100644 --- a/Fin.Infrastructure/Audits/AuditLogExtensions.cs +++ b/Fin.Infrastructure/Audits/AuditLogExtensions.cs @@ -1,7 +1,9 @@ -using Fin.Infrastructure.Audits.Enums; using Fin.Infrastructure.Audits.Interfaces; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; using MongoDB.Driver; namespace Fin.Infrastructure.Audits; @@ -10,6 +12,12 @@ public static class AuditLogExtensions { public static IServiceCollection AddAuditLog(this IServiceCollection services, IConfiguration configuration) { + try + { + BsonSerializer.RegisterSerializer(new GuidSerializer(BsonType.String)); + } + catch (BsonSerializationException) {} + var settings = MongoClientSettings.FromConnectionString(configuration.GetConnectionString("MongoDbConnection")); var mongoClient = new MongoClient(settings); diff --git a/Fin.Infrastructure/Audits/AuditLogInterceptor.cs b/Fin.Infrastructure/Audits/AuditLogInterceptor.cs index 0918bdd..1ebf06a 100644 --- a/Fin.Infrastructure/Audits/AuditLogInterceptor.cs +++ b/Fin.Infrastructure/Audits/AuditLogInterceptor.cs @@ -1,111 +1,156 @@ +using System.Text.Json; using Fin.Domain.Global.Interfaces; using Fin.Infrastructure.AmbientDatas; using Fin.Infrastructure.Audits.Enums; using Fin.Infrastructure.Audits.Interfaces; -using Fin.Infrastructure.DateTimes; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Diagnostics; -using MongoDB.Bson; namespace Fin.Infrastructure.Audits; public class AuditLogInterceptor( IAuditLogService auditService, - IAmbientData ambientData, - IDateTimeProvider dateTimeProvider - ) : SaveChangesInterceptor + IAmbientData ambientData +) : SaveChangesInterceptor { - private readonly List _temporaryAuditEntries = new(); + private List _pendingLogs; - public override ValueTask> SavingChangesAsync( - DbContextEventData eventData, - InterceptionResult result, + public override InterceptionResult SavingChanges( + DbContextEventData eventData, + InterceptionResult result) + { + CaptureChanges(eventData.Context); + return base.SavingChanges(eventData, result); + } + + public override async ValueTask> SavingChangesAsync( + DbContextEventData eventData, + InterceptionResult result, + CancellationToken cancellationToken = default) + { + CaptureChanges(eventData.Context); + return await base.SavingChangesAsync(eventData, result, cancellationToken); + } + + public override int SavedChanges(SaveChangesCompletedEventData eventData, int result) + { + SaveLogs(); + return base.SavedChanges(eventData, result); + } + + public override async ValueTask SavedChangesAsync( + SaveChangesCompletedEventData eventData, + int result, CancellationToken cancellationToken = default) { - _temporaryAuditEntries.Clear(); - var context = eventData.Context; - if (context == null) return base.SavingChangesAsync(eventData, result, cancellationToken); + await SaveLogsAsync(); + return await base.SavedChangesAsync(eventData, result, cancellationToken); + } + + private void CaptureChanges(DbContext context) + { + _pendingLogs = new List(); - context.ChangeTracker.DetectChanges(); - - foreach (var entry in context.ChangeTracker.Entries()) + var entries = context.ChangeTracker.Entries() + .Where(e => e.Entity is ILoggable && + (e.State == EntityState.Added || + e.State == EntityState.Modified || + e.State == EntityState.Deleted)) + .ToList(); + + foreach (var entry in entries) { - if (entry.State is EntityState.Detached or EntityState.Unchanged) continue; + if (entry.Entity is not ILoggable loggable) continue; + + var entityType = entry.Entity.GetType(); + var entityName = entityType.Name; - var auditEntry = new AuditEntry(entry, ambientData); - _temporaryAuditEntries.Add(auditEntry); + var logEntry = new AuditEntry(ambientData) + { + EntityName = entityName, + EntityId = GetEntityId(entry), + }; - foreach (var property in entry.Properties) + switch (entry.State) { - if (property.IsTemporary) - { - auditEntry.TemporaryProperties.Add(property); - continue; - } - - if (entry.State == EntityState.Modified && property.IsModified) - { - auditEntry.PreviousValues[property.Metadata.Name] = ConvertToBsonValid(property.OriginalValue); - } + case EntityState.Added: + logEntry.Action = AuditLogAction.Created; + logEntry.NewValue = loggable.GetLog(); + logEntry.OldValue = null; + break; + + case EntityState.Modified: + logEntry.Action = AuditLogAction.Updated; + logEntry.NewValue = loggable.GetLog(); + logEntry.OldValue = GetOriginalValues(entry); + break; + + case EntityState.Deleted: + logEntry.Action = AuditLogAction.Deleted; + logEntry.NewValue = null; + logEntry.OldValue = loggable.GetLog(); + break; } + + _pendingLogs.Add(logEntry); + } + } + + private string GetEntityId(EntityEntry entry) + { + var keyValues = entry.Properties + .Where(p => p.Metadata.IsKey()) + .Select(p => new { p.Metadata.Name, Value = p.CurrentValue }) + .ToList(); + + if (keyValues.Count == 1) + { + return keyValues[0].Value?.ToString() ?? string.Empty; } - return base.SavingChangesAsync(eventData, result, cancellationToken); + var compositeKey = keyValues.ToDictionary( + k => k.Name, + k => k.Value + ); + return JsonSerializer.Serialize(compositeKey); } - public override async ValueTask SavedChangesAsync( - SaveChangesCompletedEventData eventData, - int result, - CancellationToken cancellationToken = default) + private object GetOriginalValues(EntityEntry entry) { - if (_temporaryAuditEntries == null || _temporaryAuditEntries.Count == 0) - return await base.SavedChangesAsync(eventData, result, cancellationToken); + var loggable = entry.Entity as ILoggable; + if (loggable == null) return null; - var mongoLogs = new List(); + var originalEntity = Activator.CreateInstance(entry.Entity.GetType()); - foreach (var entry in _temporaryAuditEntries) + foreach (var property in entry.Properties) { - foreach (var prop in entry.TemporaryProperties.Where(prop => prop.Metadata.IsPrimaryKey())) + var propInfo = entry.Entity.GetType().GetProperty(property.Metadata.Name); + if (propInfo != null && propInfo.CanWrite) { - entry.KeyValues[prop.Metadata.Name] = prop.CurrentValue; + propInfo.SetValue(originalEntity, property.OriginalValue); } + } - var action = AuditLogAction.Updated; - switch (entry.Entry.State) - { - case EntityState.Deleted: - action = AuditLogAction.Deleted; - break; - case EntityState.Added: - action = AuditLogAction.Created; - break; - } - - mongoLogs.Add(new AuditLogDocument - { - EntityName = entry.TableName, - Action = action, - UserId = entry.UserId, - TenantId = entry.TenantId, - DateTime = dateTimeProvider.UtcNow(), - Snapshot = entry.Snapshot, - PreviousValues = entry.PreviousValues.Any() ? entry.PreviousValues.ToBsonDocument() : null, - KeyValues = entry.KeyValues - }); + if (originalEntity is ILoggable loggableOriginal) + { + return loggableOriginal.GetLog(); } - await auditService.LogAsync(mongoLogs); - _temporaryAuditEntries.Clear(); - return await base.SavedChangesAsync(eventData, result, cancellationToken); + return null; + } + + private async Task SaveLogsAsync() + { + if (_pendingLogs.Count == 0) return; + await auditService.LogAsync(_pendingLogs); + _pendingLogs.Clear(); } - private static object ConvertToBsonValid(object value) + private void SaveLogs() { - return value switch - { - null => null, - Guid guid => guid.ToString(), - _ => value - }; + if (_pendingLogs.Count == 0) return; + auditService.Log(_pendingLogs); + _pendingLogs.Clear(); } } \ No newline at end of file diff --git a/Fin.Infrastructure/Audits/Enums/AuditLogAction.cs b/Fin.Infrastructure/Audits/Enums/AuditLogAction.cs index fd38f9e..fc9cff8 100644 --- a/Fin.Infrastructure/Audits/Enums/AuditLogAction.cs +++ b/Fin.Infrastructure/Audits/Enums/AuditLogAction.cs @@ -2,7 +2,7 @@ namespace Fin.Infrastructure.Audits.Enums; public enum AuditLogAction { - Created = 1, - Updated = 2, - Deleted = 3 + Created = 0, + Updated = 1, + Deleted = 2 } \ No newline at end of file diff --git a/Fin.Infrastructure/Audits/Enums/MongoAuditLogService.cs b/Fin.Infrastructure/Audits/Enums/MongoAuditLogService.cs deleted file mode 100644 index aa7ae18..0000000 --- a/Fin.Infrastructure/Audits/Enums/MongoAuditLogService.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Fin.Infrastructure.Audits.Interfaces; -using MongoDB.Driver; - -namespace Fin.Infrastructure.Audits.Enums; - -public class MongoAuditLogService(IMongoDatabase database) : IAuditLogService -{ - private readonly IMongoCollection _collection = database.GetCollection("audit_logs"); - - public async Task LogAsync(List logs) - { - if (logs != null && logs.Any()) - { - await _collection.InsertManyAsync(logs); - } - } -} \ No newline at end of file diff --git a/Fin.Infrastructure/Audits/Interfaces/IAuditLogService.cs b/Fin.Infrastructure/Audits/Interfaces/IAuditLogService.cs index a2829b7..e6daf75 100644 --- a/Fin.Infrastructure/Audits/Interfaces/IAuditLogService.cs +++ b/Fin.Infrastructure/Audits/Interfaces/IAuditLogService.cs @@ -2,5 +2,6 @@ namespace Fin.Infrastructure.Audits.Interfaces; public interface IAuditLogService { - Task LogAsync(List logs); + Task LogAsync(List logs); + void Log(List logs); } \ No newline at end of file diff --git a/Fin.Infrastructure/Audits/MongoAuditLogService.cs b/Fin.Infrastructure/Audits/MongoAuditLogService.cs new file mode 100644 index 0000000..da55bbd --- /dev/null +++ b/Fin.Infrastructure/Audits/MongoAuditLogService.cs @@ -0,0 +1,64 @@ +using Fin.Infrastructure.Audits.Interfaces; +using Fin.Infrastructure.DateTimes; +using Microsoft.Extensions.Logging; +using MongoDB.Driver; + +namespace Fin.Infrastructure.Audits; + +public class MongoAuditLogService( + IMongoDatabase database, + IDateTimeProvider dateTimeProvider, + ILogger logger + ) : IAuditLogService +{ + private readonly IMongoCollection _collection = database.GetCollection("audit_logs"); + + public async Task LogAsync(List logs) + { + try + { + var entitiesLog = logs.Select(log => new AuditLogDocument + { + EntityName = log.EntityName, + EntityId = log.EntityId, + NewValue = log.NewValue, + OldValue = log.OldValue, + Action = log.Action, + DateTime = dateTimeProvider.UtcNow(), + UserId = log.UserId, + TenantId = log.TenantId + }); + + await _collection.InsertManyAsync(entitiesLog); + } + catch (Exception ex) + { + logger.LogError("Error on saving log: {ExMessage}", ex.Message); + } + } + + public void Log(List logs) + { + try + { + + var entitiesLog = logs.Select(log => new AuditLogDocument + { + EntityName = log.EntityName, + EntityId = log.EntityId, + NewValue = log.NewValue, + OldValue = log.OldValue, + Action = log.Action, + DateTime = dateTimeProvider.UtcNow(), + UserId = log.UserId, + TenantId = log.TenantId + }); + + _collection.InsertMany(entitiesLog); + } + catch (Exception ex) + { + logger.LogError("Error on saving log: {ExMessage}", ex.Message); + } + } +} \ No newline at end of file From ada0954f26e064cf1189ba4bd01cd1d7123805c5 Mon Sep 17 00:00:00 2001 From: RafaelKC Date: Thu, 4 Dec 2025 22:35:59 -0300 Subject: [PATCH 4/4] FIN-74 added Iloggable to entities --- Fin-Backend.sln.DotSettings.user | 1 + Fin.Domain/CardBrands/Entities/CardBrand.cs | 17 +++++++++- Fin.Domain/CreditCards/Entities/CreditCard.cs | 23 ++++++++++++- Fin.Domain/Fin.Domain.csproj | 4 --- .../Entities/FinancialInstitution.cs | 20 +++++++++++- .../Enums/FinancialInstitutionType.cs | 5 +++ .../Decorators/FrontTranslateKeyAttribute.cs | 32 +++++++++++++++++++ .../ILoggableAuditedTenantEntity.cs | 6 ++++ Fin.Domain/Menus/Entities/Menu.cs | 25 +++++++++++++-- Fin.Domain/Menus/Enums/MenuPosition.cs | 7 +++- .../Notifications/Entities/Notification.cs | 28 +++++++++++++++- .../Entities/NotificationUserDelivery.cs | 17 ++++++++-- .../Entities/UserNotificationSettings.cs | 23 +++++++++++-- .../Entities/UserRememberUseSetting.cs | 23 +++++++++++-- .../Enums/NotificationSeverity.cs | 7 ++++ .../Notifications/Enums/NotificationWay.cs | 8 ++++- Fin.Domain/People/Entities/Person.cs | 17 +++++++++- Fin.Domain/People/Entities/TitlePerson.cs | 17 +++++++++- Fin.Domain/Tenants/Entities/Tenant.cs | 16 ++++++++-- Fin.Domain/Tenants/Entities/TenantUser.cs | 15 +++++++-- .../TitleCategories/Entities/TitleCategory.cs | 22 ++++++++++++- .../Entities/TitleTitleCategory.cs | 12 ++++++- .../Enums/TitleCategoryType.cs | 4 +++ Fin.Domain/Titles/Entities/Title.cs | 4 ++- Fin.Domain/Titles/Enums/TitleType.cs | 6 ++++ Fin.Domain/Users/Entities/User.cs | 17 +++++++++- .../Users/Entities/UserDeleteRequest.cs | 19 ++++++++++- Fin.Domain/Wallets/Entities/Wallet.cs | 21 +++++++++++- 28 files changed, 386 insertions(+), 30 deletions(-) create mode 100644 Fin.Domain/Global/Decorators/FrontTranslateKeyAttribute.cs create mode 100644 Fin.Domain/Global/Interfaces/ILoggableAuditedTenantEntity.cs diff --git a/Fin-Backend.sln.DotSettings.user b/Fin-Backend.sln.DotSettings.user index 1def2b8..5036aa5 100644 --- a/Fin-Backend.sln.DotSettings.user +++ b/Fin-Backend.sln.DotSettings.user @@ -2,6 +2,7 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded diff --git a/Fin.Domain/CardBrands/Entities/CardBrand.cs b/Fin.Domain/CardBrands/Entities/CardBrand.cs index 48b8989..269e6dc 100644 --- a/Fin.Domain/CardBrands/Entities/CardBrand.cs +++ b/Fin.Domain/CardBrands/Entities/CardBrand.cs @@ -4,7 +4,7 @@ namespace Fin.Domain.CardBrands.Entities; -public class CardBrand: IAuditedEntity +public class CardBrand: IAuditedEntity, ILoggable { public string Name { get; set; } public string Icon { get; set; } @@ -36,4 +36,19 @@ public void Update(CardBrandInput input) Color = input.Color; } + + public object GetLog() + { + return new + { + Id, + CreatedAt, + CreatedBy, + UpdatedAt, + UpdatedBy, + Name, + Color, + Icon + }; + } } diff --git a/Fin.Domain/CreditCards/Entities/CreditCard.cs b/Fin.Domain/CreditCards/Entities/CreditCard.cs index 81f94fd..a7ed055 100644 --- a/Fin.Domain/CreditCards/Entities/CreditCard.cs +++ b/Fin.Domain/CreditCards/Entities/CreditCard.cs @@ -6,7 +6,7 @@ namespace Fin.Domain.CreditCards.Entities; -public class CreditCard: IAuditedTenantEntity +public class CreditCard: ILoggableAuditedTenantEntity { public string Name { get; private set; } public string Color { get; private set; } @@ -63,4 +63,25 @@ public void Update(CreditCardInput input) } public void ToggleInactivated() => Inactivated = !Inactivated; + + public object GetLog() + { + return new + { + Id, + CreatedAt, + CreatedBy, + UpdatedAt, + UpdatedBy, + TenantId, + Name, + Color, + Icon, + Limit, + DueDay, + ClosingDay, + DebitWalletId, + FinancialInstitutionId, + }; + } } \ No newline at end of file diff --git a/Fin.Domain/Fin.Domain.csproj b/Fin.Domain/Fin.Domain.csproj index 6005f43..7b8dbdc 100644 --- a/Fin.Domain/Fin.Domain.csproj +++ b/Fin.Domain/Fin.Domain.csproj @@ -5,8 +5,4 @@ enable - - - - diff --git a/Fin.Domain/FinancialInstitutions/Entities/FinancialInstitution.cs b/Fin.Domain/FinancialInstitutions/Entities/FinancialInstitution.cs index f7d2dd3..2af52d3 100644 --- a/Fin.Domain/FinancialInstitutions/Entities/FinancialInstitution.cs +++ b/Fin.Domain/FinancialInstitutions/Entities/FinancialInstitution.cs @@ -1,12 +1,13 @@ using Fin.Domain.CreditCards.Entities; using Fin.Domain.FinancialInstitutions.Dtos; using Fin.Domain.FinancialInstitutions.Enums; +using Fin.Domain.Global.Decorators; using Fin.Domain.Global.Interfaces; using Fin.Domain.Wallets.Entities; namespace Fin.Domain.FinancialInstitutions.Entities; -public class FinancialInstitution : IAuditedEntity +public class FinancialInstitution : IAuditedEntity, ILoggable { public string Name { get; set; } public string Code { get; set; } @@ -47,4 +48,21 @@ public void Update(FinancialInstitutionInput input) } public void ToggleInactive() => Inactive = !Inactive; + public object GetLog() + { + return new + { + Id, + CreatedAt, + CreatedBy, + UpdatedAt, + UpdatedBy, + Code, + Icon, + Color, + Inactive, + Type, + TypeDescription = Type.GetTranslateKey(), + }; + } } diff --git a/Fin.Domain/FinancialInstitutions/Enums/FinancialInstitutionType.cs b/Fin.Domain/FinancialInstitutions/Enums/FinancialInstitutionType.cs index a45eb91..b422d6d 100644 --- a/Fin.Domain/FinancialInstitutions/Enums/FinancialInstitutionType.cs +++ b/Fin.Domain/FinancialInstitutions/Enums/FinancialInstitutionType.cs @@ -1,9 +1,14 @@ +using Fin.Domain.Global.Decorators; + namespace Fin.Domain.FinancialInstitutions.Enums { public enum FinancialInstitutionType { + [FrontTranslateKey("finCore.features.financialInstitutions.type.bank")] Bank = 0, + [FrontTranslateKey("finCore.features.financialInstitutions.type.digitalBank")] DigitalBank = 1, + [FrontTranslateKey("finCore.features.financialInstitutions.type.foodCard")] FoodCard = 2, } } diff --git a/Fin.Domain/Global/Decorators/FrontTranslateKeyAttribute.cs b/Fin.Domain/Global/Decorators/FrontTranslateKeyAttribute.cs new file mode 100644 index 0000000..812cf17 --- /dev/null +++ b/Fin.Domain/Global/Decorators/FrontTranslateKeyAttribute.cs @@ -0,0 +1,32 @@ +using System.Reflection; + +namespace Fin.Domain.Global.Decorators; + +[AttributeUsage(AttributeTargets.Field)] +public class FrontTranslateKeyAttribute(string translateKey): Attribute +{ + public string TranslateKey { get; } = translateKey; +} + + +public static class FrontTranslateKeyExtension +{ + public static string GetTranslateKey(this T valeu, bool throwIfNotFoundMessage = true) + { + var type = valeu.GetType(); + var memberInfo = type.GetMember(valeu.ToString() ?? string.Empty); + + if (memberInfo.Length > 0) + { + var attribute = memberInfo[0].GetCustomAttribute(); + if (attribute != null) + { + return attribute.TranslateKey; + } + } + + return throwIfNotFoundMessage + ? throw new ArgumentException($"Cannot get translate key {valeu}") + : string.Empty; + } +} \ No newline at end of file diff --git a/Fin.Domain/Global/Interfaces/ILoggableAuditedTenantEntity.cs b/Fin.Domain/Global/Interfaces/ILoggableAuditedTenantEntity.cs new file mode 100644 index 0000000..92718bf --- /dev/null +++ b/Fin.Domain/Global/Interfaces/ILoggableAuditedTenantEntity.cs @@ -0,0 +1,6 @@ +namespace Fin.Domain.Global.Interfaces; + +public interface ILoggableAuditedTenantEntity: ILoggable, IAuditedEntity, ITenantEntity +{ + +} \ No newline at end of file diff --git a/Fin.Domain/Menus/Entities/Menu.cs b/Fin.Domain/Menus/Entities/Menu.cs index be250db..40145e1 100644 --- a/Fin.Domain/Menus/Entities/Menu.cs +++ b/Fin.Domain/Menus/Entities/Menu.cs @@ -1,10 +1,11 @@ -using Fin.Domain.Global.Interfaces; +using Fin.Domain.Global.Decorators; +using Fin.Domain.Global.Interfaces; using Fin.Domain.Menus.Dtos; using Fin.Domain.Menus.Enums; namespace Fin.Domain.Menus.Entities; -public class Menu: IAuditedEntity +public class Menu: IAuditedEntity, ILoggable { public string FrontRoute { get; set; } public string Name { get; set; } @@ -45,4 +46,24 @@ public void Update(MenuInput input) OnlyForAdmin = input.OnlyForAdmin; Position = input.Position; } + + public object GetLog() + { + return new + { + Id, + CreatedAt, + CreatedBy, + UpdatedAt, + UpdatedBy, + FrontRoute, + Name, + Icon, + Color, + KeyWords, + OnlyForAdmin, + Position, + PositionDescription = Position.GetTranslateKey() + }; + } } diff --git a/Fin.Domain/Menus/Enums/MenuPosition.cs b/Fin.Domain/Menus/Enums/MenuPosition.cs index 955a527..2a3674c 100644 --- a/Fin.Domain/Menus/Enums/MenuPosition.cs +++ b/Fin.Domain/Menus/Enums/MenuPosition.cs @@ -1,8 +1,13 @@ -namespace Fin.Domain.Menus.Enums; +using Fin.Domain.Global.Decorators; + +namespace Fin.Domain.Menus.Enums; public enum MenuPosition { + [FrontTranslateKey("finCore.features.menus.hide")] Hide = 0, + [FrontTranslateKey("finCore.features.menus.leftTop")] LeftTop = 1, + [FrontTranslateKey("finCore.features.menus.leftBottom")] LeftBottom = 2 } \ No newline at end of file diff --git a/Fin.Domain/Notifications/Entities/Notification.cs b/Fin.Domain/Notifications/Entities/Notification.cs index f12be5c..412efc8 100644 --- a/Fin.Domain/Notifications/Entities/Notification.cs +++ b/Fin.Domain/Notifications/Entities/Notification.cs @@ -1,4 +1,5 @@ using System.Collections.ObjectModel; +using Fin.Domain.Global.Decorators; using Fin.Domain.Global.Extensions; using Fin.Domain.Global.Interfaces; using Fin.Domain.Notifications.Dtos; @@ -6,7 +7,7 @@ namespace Fin.Domain.Notifications.Entities; -public class Notification: IAuditedEntity +public class Notification: IAuditedEntity, ILoggable { public List Ways { get; set; } = []; public string TextBody { get; set; } @@ -90,4 +91,29 @@ public List UpdateAndReturnToRemoveDeliveries(Notifica return deliveriesToDelete; } + + public object GetLog() + { + return new + { + Id, + CreatedAt, + CreatedBy, + UpdatedAt, + UpdatedBy, + Ways, + WaysDescriptions = Ways.Select(way => way.GetTranslateKey()), + TextBody, + HtmlBody, + NormalizedTextBody, + Title, + Continuous, + NormalizedTitle, + StartToDelivery, + StopToDelivery, + Link, + Severity, + SeverityDescriptions = Severity.GetTranslateKey(), + }; + } } \ No newline at end of file diff --git a/Fin.Domain/Notifications/Entities/NotificationUserDelivery.cs b/Fin.Domain/Notifications/Entities/NotificationUserDelivery.cs index 21cf2b4..e750299 100644 --- a/Fin.Domain/Notifications/Entities/NotificationUserDelivery.cs +++ b/Fin.Domain/Notifications/Entities/NotificationUserDelivery.cs @@ -1,8 +1,9 @@ -using Fin.Domain.Users.Entities; +using Fin.Domain.Global.Interfaces; +using Fin.Domain.Users.Entities; namespace Fin.Domain.Notifications.Entities; -public class NotificationUserDelivery +public class NotificationUserDelivery: ILoggable { public Guid NotificationId { get; set; } public Guid UserId { get; set; } @@ -33,4 +34,16 @@ public void MarkAsVisualized() { Visualized = true; } + + public object GetLog() + { + return new + { + NotificationId, + UserId, + Delivery, + Visualized, + BackgroundJobId + }; + } } \ No newline at end of file diff --git a/Fin.Domain/Notifications/Entities/UserNotificationSettings.cs b/Fin.Domain/Notifications/Entities/UserNotificationSettings.cs index 205121d..601b140 100644 --- a/Fin.Domain/Notifications/Entities/UserNotificationSettings.cs +++ b/Fin.Domain/Notifications/Entities/UserNotificationSettings.cs @@ -1,11 +1,12 @@ -using Fin.Domain.Global.Interfaces; +using Fin.Domain.Global.Decorators; +using Fin.Domain.Global.Interfaces; using Fin.Domain.Notifications.Dtos; using Fin.Domain.Notifications.Enums; using Fin.Domain.Users.Entities; namespace Fin.Domain.Notifications.Entities; -public class UserNotificationSettings: IAuditedTenantEntity +public class UserNotificationSettings: ILoggableAuditedTenantEntity { public Guid UserId { get; set; } public bool Enabled { get; set; } @@ -61,4 +62,22 @@ public void RemoveTokens(List tokens) { FirebaseTokens = FirebaseTokens.Except(tokens).ToList(); } + + public object GetLog() + { + return new + { + Id, + CreatedAt, + CreatedBy, + UpdatedAt, + UpdatedBy, + TenantId, + UserId, + Enabled, + AllowedWays, + AllowedWaysDesciptions = AllowedWays.Select(way => way.GetTranslateKey()), + FirebaseTokens + }; + } } \ No newline at end of file diff --git a/Fin.Domain/Notifications/Entities/UserRememberUseSetting.cs b/Fin.Domain/Notifications/Entities/UserRememberUseSetting.cs index 3a52436..43aeeef 100644 --- a/Fin.Domain/Notifications/Entities/UserRememberUseSetting.cs +++ b/Fin.Domain/Notifications/Entities/UserRememberUseSetting.cs @@ -1,11 +1,12 @@ -using Fin.Domain.Global.Interfaces; +using Fin.Domain.Global.Decorators; +using Fin.Domain.Global.Interfaces; using Fin.Domain.Notifications.Dtos; using Fin.Domain.Notifications.Enums; using Fin.Domain.Users.Entities; namespace Fin.Domain.Notifications.Entities; -public class UserRememberUseSetting : IAuditedTenantEntity +public class UserRememberUseSetting : ILoggableAuditedTenantEntity { public Guid UserId { get; set; } public List Ways { get; set; } = new(); @@ -42,4 +43,22 @@ public void Update(UserRememberUseSettingInput input) WeekDays = input.WeekDays; NotifyOn = input.NotifyOn; } + + public object GetLog() + { + return new + { + Id, + CreatedAt, + CreatedBy, + UpdatedAt, + UpdatedBy, + TenantId, + UserId, + Ways, + WaysDescriptions = Ways.Select(way => way.GetTranslateKey()), + NotifyOn, + WeekDays, + }; + } } \ No newline at end of file diff --git a/Fin.Domain/Notifications/Enums/NotificationSeverity.cs b/Fin.Domain/Notifications/Enums/NotificationSeverity.cs index 6964b79..6564bcc 100644 --- a/Fin.Domain/Notifications/Enums/NotificationSeverity.cs +++ b/Fin.Domain/Notifications/Enums/NotificationSeverity.cs @@ -1,10 +1,17 @@ +using Fin.Domain.Global.Decorators; + namespace Fin.Domain.Notifications.Enums; public enum NotificationSeverity { + [FrontTranslateKey("finCore.features.notifications.severity.default")] Default = 0, + [FrontTranslateKey("finCore.features.notifications.severity.success")] Success = 1, + [FrontTranslateKey("finCore.features.notifications.severity.error")] Error = 2, + [FrontTranslateKey("finCore.features.notifications.severity.warning")] Warning = 3, + [FrontTranslateKey("finCore.features.notifications.severity.info")] Info = 4, } \ No newline at end of file diff --git a/Fin.Domain/Notifications/Enums/NotificationWay.cs b/Fin.Domain/Notifications/Enums/NotificationWay.cs index 231b117..cf0dac4 100644 --- a/Fin.Domain/Notifications/Enums/NotificationWay.cs +++ b/Fin.Domain/Notifications/Enums/NotificationWay.cs @@ -1,9 +1,15 @@ -namespace Fin.Domain.Notifications.Enums; +using Fin.Domain.Global.Decorators; + +namespace Fin.Domain.Notifications.Enums; public enum NotificationWay { + [FrontTranslateKey("finCore.features.notifications.ways.snack")] Snack = 0, + [FrontTranslateKey("finCore.features.notifications.ways.message")] Message = 1, + [FrontTranslateKey("finCore.features.notifications.ways.push")] Push = 2, + [FrontTranslateKey("finCore.features.notifications.ways.email")] Email = 3 } \ No newline at end of file diff --git a/Fin.Domain/People/Entities/Person.cs b/Fin.Domain/People/Entities/Person.cs index bea8b17..cb46587 100644 --- a/Fin.Domain/People/Entities/Person.cs +++ b/Fin.Domain/People/Entities/Person.cs @@ -4,7 +4,7 @@ namespace Fin.Domain.People.Entities; -public class Person: IAuditedTenantEntity +public class Person: ILoggableAuditedTenantEntity { public string Name { get; private set; } public bool Inactivated { get; private set; } @@ -37,4 +37,19 @@ public void ToggleInactivated() { Inactivated = !Inactivated; } + + public object GetLog() + { + return new + { + Id, + CreatedAt, + CreatedBy, + UpdatedAt, + UpdatedBy, + TenantId, + Name, + Inactivated + }; + } } \ No newline at end of file diff --git a/Fin.Domain/People/Entities/TitlePerson.cs b/Fin.Domain/People/Entities/TitlePerson.cs index 11e2709..8d348e5 100644 --- a/Fin.Domain/People/Entities/TitlePerson.cs +++ b/Fin.Domain/People/Entities/TitlePerson.cs @@ -4,7 +4,7 @@ namespace Fin.Domain.People.Entities; -public class TitlePerson: ITenant, IAudited +public class TitlePerson: ITenant, IAudited, ILoggable { public Guid PersonId { get; private set; } public virtual Person Person { get; set; } @@ -35,4 +35,19 @@ public void Update(decimal percentage) public Guid UpdatedBy { get; set; } public DateTime CreatedAt { get; set; } public DateTime UpdatedAt { get; set; } + + public object GetLog() + { + return new + { + CreatedAt, + CreatedBy, + UpdatedAt, + UpdatedBy, + TenantId, + PersonId, + TitleId, + Percentage + }; + } } \ No newline at end of file diff --git a/Fin.Domain/Tenants/Entities/Tenant.cs b/Fin.Domain/Tenants/Entities/Tenant.cs index e838a60..1ad6ccf 100644 --- a/Fin.Domain/Tenants/Entities/Tenant.cs +++ b/Fin.Domain/Tenants/Entities/Tenant.cs @@ -3,7 +3,7 @@ namespace Fin.Domain.Tenants.Entities; -public class Tenant: IEntity +public class Tenant: IEntity, ILoggable { public Guid Id { get; set; } public DateTime CreatedAt { get; private set; } @@ -25,5 +25,17 @@ public Tenant(DateTime now, string timezone, string locale) UpdatedAt = now; Locale = locale ?? "pt-BR"; Timezone = timezone ?? "America/Sao_Paulo"; - } + } + + public object GetLog() + { + return new + { + Id, + CreatedAt, + UpdatedAt, + Locale, + Timezone + }; + } } \ No newline at end of file diff --git a/Fin.Domain/Tenants/Entities/TenantUser.cs b/Fin.Domain/Tenants/Entities/TenantUser.cs index b1ab27f..bd9f489 100644 --- a/Fin.Domain/Tenants/Entities/TenantUser.cs +++ b/Fin.Domain/Tenants/Entities/TenantUser.cs @@ -1,7 +1,18 @@ -namespace Fin.Domain.Tenants.Entities; +using Fin.Domain.Global.Interfaces; -public class TenantUser +namespace Fin.Domain.Tenants.Entities; + +public class TenantUser: ILoggable { public Guid TenantId { get; set; } public Guid UserId { get; set; } + + public object GetLog() + { + return new + { + TenantId, + UserId + }; + } } \ No newline at end of file diff --git a/Fin.Domain/TitleCategories/Entities/TitleCategory.cs b/Fin.Domain/TitleCategories/Entities/TitleCategory.cs index 94d18b6..a5210e6 100644 --- a/Fin.Domain/TitleCategories/Entities/TitleCategory.cs +++ b/Fin.Domain/TitleCategories/Entities/TitleCategory.cs @@ -1,3 +1,4 @@ +using Fin.Domain.Global.Decorators; using Fin.Domain.Global.Interfaces; using Fin.Domain.TitleCategories.Dtos; using Fin.Domain.TitleCategories.Enums; @@ -5,7 +6,7 @@ namespace Fin.Domain.TitleCategories.Entities; -public class TitleCategory: IAuditedTenantEntity +public class TitleCategory: ILoggableAuditedTenantEntity { public bool Inactivated { get; private set; } public string Name { get; private set; } @@ -43,4 +44,23 @@ public void Update(TitleCategoryInput input) } public void ToggleInactivated() => Inactivated = !Inactivated; + + public object GetLog() + { + return new + { + Id, + CreatedAt, + CreatedBy, + UpdatedAt, + UpdatedBy, + TenantId, + Inactivated, + Name, + Icon, + Color, + Type, + TypeDescription = Type.GetTranslateKey(), + }; + } } \ No newline at end of file diff --git a/Fin.Domain/TitleCategories/Entities/TitleTitleCategory.cs b/Fin.Domain/TitleCategories/Entities/TitleTitleCategory.cs index a47326c..baca2ad 100644 --- a/Fin.Domain/TitleCategories/Entities/TitleTitleCategory.cs +++ b/Fin.Domain/TitleCategories/Entities/TitleTitleCategory.cs @@ -1,8 +1,9 @@ +using Fin.Domain.Global.Interfaces; using Fin.Domain.Titles.Entities; namespace Fin.Domain.TitleCategories.Entities; -public class TitleTitleCategory +public class TitleTitleCategory: ILoggable { public Guid TitleId { get; set; } public virtual Title Title { get; set; } @@ -19,4 +20,13 @@ public TitleTitleCategory(Guid categoryId, Guid titleId) TitleId = titleId; TitleCategoryId = categoryId; } + + public object GetLog() + { + return new + { + TitleId, + TitleCategoryId + }; + } } \ No newline at end of file diff --git a/Fin.Domain/TitleCategories/Enums/TitleCategoryType.cs b/Fin.Domain/TitleCategories/Enums/TitleCategoryType.cs index 630c43c..1758610 100644 --- a/Fin.Domain/TitleCategories/Enums/TitleCategoryType.cs +++ b/Fin.Domain/TitleCategories/Enums/TitleCategoryType.cs @@ -1,11 +1,15 @@ +using Fin.Domain.Global.Decorators; using Fin.Domain.Titles.Enums; namespace Fin.Domain.TitleCategories.Enums; public enum TitleCategoryType: byte { + [FrontTranslateKey("finCore.features.titleCategory.type.expense")] Expense = 0, + [FrontTranslateKey("finCore.features.titleCategory.type.income")] Income = 1, + [FrontTranslateKey("finCore.features.titleCategory.type.both")] Both = 2 } diff --git a/Fin.Domain/Titles/Entities/Title.cs b/Fin.Domain/Titles/Entities/Title.cs index a5a53c6..879891a 100644 --- a/Fin.Domain/Titles/Entities/Title.cs +++ b/Fin.Domain/Titles/Entities/Title.cs @@ -1,4 +1,5 @@ using System.Collections.ObjectModel; +using Fin.Domain.Global.Decorators; using Fin.Domain.Global.Interfaces; using Fin.Domain.People.Dtos; using Fin.Domain.People.Entities; @@ -12,6 +13,7 @@ namespace Fin.Domain.Titles.Entities; public class Title: IAuditedTenantEntity, ILoggable { public decimal Value { get; set; } + public TitleType Type { get; set; } public string Description { get; set; } @@ -153,7 +155,7 @@ public object GetLog() Description = Description, Type = Type, - TypeDescription = Type.ToString(), // TODO implement a to description + TypeDescription = Type.GetTranslateKey(), OriginalValue = Value, EffectiveValue = EffectiveValue, PreviousBalance = PreviousBalance, diff --git a/Fin.Domain/Titles/Enums/TitleType.cs b/Fin.Domain/Titles/Enums/TitleType.cs index 6beb346..e0d2cb7 100644 --- a/Fin.Domain/Titles/Enums/TitleType.cs +++ b/Fin.Domain/Titles/Enums/TitleType.cs @@ -1,7 +1,13 @@ +using System.ComponentModel; +using Fin.Domain.Global.Decorators; + namespace Fin.Domain.Titles.Enums; public enum TitleType: byte { + [FrontTranslateKey("finCore.features.title.type.expense")] Expense = 0, + + [FrontTranslateKey("finCore.features.title.type.income")] Income = 1 } \ No newline at end of file diff --git a/Fin.Domain/Users/Entities/User.cs b/Fin.Domain/Users/Entities/User.cs index 107bcb8..ecb9e10 100644 --- a/Fin.Domain/Users/Entities/User.cs +++ b/Fin.Domain/Users/Entities/User.cs @@ -5,7 +5,7 @@ namespace Fin.Domain.Users.Entities; -public class User: IEntity +public class User: IEntity, ILoggable { public Guid Id { get; set; } @@ -88,4 +88,19 @@ public void MakeAdmin() { IsAdmin = true; } + + public object GetLog() + { + return new + { + Id, + FirstName, + LastName, + DisplayName, + CreatedAt, + UpdatedAt, + IsAdmin, + IsActivity + }; + } } \ No newline at end of file diff --git a/Fin.Domain/Users/Entities/UserDeleteRequest.cs b/Fin.Domain/Users/Entities/UserDeleteRequest.cs index ffe18f3..3fdae09 100644 --- a/Fin.Domain/Users/Entities/UserDeleteRequest.cs +++ b/Fin.Domain/Users/Entities/UserDeleteRequest.cs @@ -2,7 +2,7 @@ namespace Fin.Domain.Users.Entities; -public class UserDeleteRequest : IAuditedEntity +public class UserDeleteRequest : IAuditedEntity, ILoggable { public Guid UserId { get; set; } public virtual User User { get; set; } @@ -44,4 +44,21 @@ public void Abort(Guid userAbortedId, DateTime abortedAt) if (!User.IsActivity) User.ToggleActivity(); } + public object GetLog() + { + return new + { + Id, + CreatedAt, + CreatedBy, + UpdatedAt, + UpdatedBy, + UserId, + UserAbortedId, + AbortedAt, + Aborted, + DeleteRequestedAt, + DeleteEffectivatedAt + }; + } } \ No newline at end of file diff --git a/Fin.Domain/Wallets/Entities/Wallet.cs b/Fin.Domain/Wallets/Entities/Wallet.cs index 4934bf7..a1abc06 100644 --- a/Fin.Domain/Wallets/Entities/Wallet.cs +++ b/Fin.Domain/Wallets/Entities/Wallet.cs @@ -7,7 +7,7 @@ namespace Fin.Domain.Wallets.Entities; -public class Wallet: IAuditedTenantEntity +public class Wallet: ILoggableAuditedTenantEntity { public Guid Id { get; set; } public Guid CreatedBy { get; set; } @@ -66,4 +66,23 @@ public decimal CalculateBalanceAt(DateTime dateTime) return lastTitle?.ResultingBalance ?? InitialBalance; } + + public object GetLog() + { + return new + { + Id, + CreatedBy, + CreatedAt, + UpdatedAt, + UpdatedBy, + TenantId, + Name, + Color, + Icon, + Inactivated, + FinancialInstitutionId, + InitialBalance + }; + } } \ No newline at end of file