diff --git a/src/Application/Application.csproj b/src/Application/Application.csproj index 4dbbb2e6..06f20aa7 100644 --- a/src/Application/Application.csproj +++ b/src/Application/Application.csproj @@ -74,6 +74,11 @@ True ExportApprovalDatasQueryHandler.resx + + True + True + ExportAuditTrailsQuery.resx + True True @@ -111,6 +116,10 @@ ResXFileCodeGenerator ExportApprovalDatasQueryHandler.Designer.cs + + ResXFileCodeGenerator + ExportAuditTrailsQuery.Designer.cs + ResXFileCodeGenerator ImportKeyValuesCommandHandler.Designer.cs diff --git a/src/Application/Common/Interfaces/IApplicationDbContext.cs b/src/Application/Common/Interfaces/IApplicationDbContext.cs index 1e38c63f..17605ae9 100644 --- a/src/Application/Common/Interfaces/IApplicationDbContext.cs +++ b/src/Application/Common/Interfaces/IApplicationDbContext.cs @@ -1,6 +1,7 @@ using System.Threading; using System.Threading.Tasks; using CleanArchitecture.Razor.Domain.Entities; +using CleanArchitecture.Razor.Domain.Entities.Audit; using CleanArchitecture.Razor.Domain.Entities.Worflow; using Microsoft.EntityFrameworkCore; @@ -8,6 +9,7 @@ namespace CleanArchitecture.Razor.Application.Common.Interfaces { public interface IApplicationDbContext { + DbSet AuditTrails { get; set; } DbSet Customers { get; set; } DbSet DocumentTypes { get; set; } DbSet Documents { get; set; } diff --git a/src/Application/Features/AuditTrails/DTOs/AuditTrailDto.cs b/src/Application/Features/AuditTrails/DTOs/AuditTrailDto.cs new file mode 100644 index 00000000..4f981e84 --- /dev/null +++ b/src/Application/Features/AuditTrails/DTOs/AuditTrailDto.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using AutoMapper; +using CleanArchitecture.Razor.Application.Common.Mappings; +using CleanArchitecture.Razor.Domain.Entities.Audit; + +namespace CleanArchitecture.Razor.Application.Features.AuditTrails.DTOs +{ + public class AuditTrailDto : IMapFrom + { + public void Mapping(Profile profile) + { + profile.CreateMap() + .ForMember(x => x.AuditType, s => s.MapFrom(y => y.AuditType.ToString())) + .ForMember(x => x.OldValues, s => s.MapFrom(y => JsonSerializer.Serialize(y.OldValues, null))) + .ForMember(x => x.NewValues, s => s.MapFrom(y => JsonSerializer.Serialize(y.NewValues, null))) + .ForMember(x => x.PrimaryKey, s => s.MapFrom(y => JsonSerializer.Serialize(y.PrimaryKey, null))) + .ForMember(x => x.AffectedColumns, s => s.MapFrom(y => JsonSerializer.Serialize(y.AffectedColumns, null))) + ; + + } + public int Id { get; set; } + public string UserId { get; set; } + public string AuditType { get; set; } + public string TableName { get; set; } + public DateTime DateTime { get; set; } + public string OldValues { get; set; } + public string NewValues { get; set; } + public string AffectedColumns { get; set; } + public string PrimaryKey { get; set; } + } +} diff --git a/src/Application/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.cs b/src/Application/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.cs new file mode 100644 index 00000000..9d212c4f --- /dev/null +++ b/src/Application/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AutoMapper; +using CleanArchitecture.Razor.Application.Common.Extensions; +using CleanArchitecture.Razor.Application.Common.Interfaces; +using CleanArchitecture.Razor.Domain.Entities; +using System.Linq.Dynamic.Core; +using MediatR; +using Microsoft.EntityFrameworkCore; +using AutoMapper.QueryableExtensions; +using Microsoft.Extensions.Localization; +using CleanArchitecture.Razor.Application.Features.AuditTrails.DTOs; +using CleanArchitecture.Razor.Domain.Entities.Worflow; +using CleanArchitecture.Razor.Domain.Entities.Audit; + +namespace CleanArchitecture.Razor.Application.Features.AuditTrails.Queries.Export +{ + public class ExportAuditTrailsQuery : IRequest + { + public string filterRules { get; set; } + public string sort { get; set; } = "Id"; + public string order { get; set; } = "desc"; + } + + public class ExportAuditTrailsQueryHandler : + IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + private readonly IExcelService _excelService; + private readonly IStringLocalizer _localizer; + + public ExportAuditTrailsQueryHandler( + IApplicationDbContext context, + IMapper mapper, + IExcelService excelService, + IStringLocalizer localizer + ) + { + _context = context; + _mapper = mapper; + _excelService = excelService; + _localizer = localizer; + } + + public async Task Handle(ExportAuditTrailsQuery request, CancellationToken cancellationToken) + { + var filters = PredicateBuilder.FromFilter(request.filterRules); + var data = await _context.AuditTrails + .Where(filters) + .OrderBy($"{request.sort} {request.order}") + .ProjectTo(_mapper.ConfigurationProvider) + .ToListAsync(cancellationToken); + var result = await _excelService.ExportAsync(data, + new Dictionary>() + { + //{ _localizer["Id"], item => item.Id }, + { _localizer["Date Time"], item => item.DateTime.ToString("yyyy-MM-dd HH:mm:ss") }, + { _localizer["Table Name"], item => item.TableName }, + { _localizer["Audit Type"], item => item.AuditType }, + { _localizer["Old Values"], item => item.OldValues }, + { _localizer["New Values"], item => item.NewValues }, + { _localizer["Primary Key"], item => item.PrimaryKey }, + }, _localizer["AuditTrails"] + ); + return result; + } + } +} + diff --git a/src/Application/Features/AuditTrails/Queries/PaginationQuery/AuditTrailsWithPaginationQuery.cs b/src/Application/Features/AuditTrails/Queries/PaginationQuery/AuditTrailsWithPaginationQuery.cs new file mode 100644 index 00000000..daba2915 --- /dev/null +++ b/src/Application/Features/AuditTrails/Queries/PaginationQuery/AuditTrailsWithPaginationQuery.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using AutoMapper; +using CleanArchitecture.Razor.Application.Common.Extensions; +using CleanArchitecture.Razor.Application.Common.Interfaces; +using CleanArchitecture.Razor.Application.Common.Models; +using CleanArchitecture.Razor.Application.Models; +using CleanArchitecture.Razor.Domain.Entities; +using System.Linq.Dynamic.Core; +using MediatR; +using CleanArchitecture.Razor.Application.Common.Mappings; +using AutoMapper.QueryableExtensions; +using CleanArchitecture.Razor.Application.Common.Specification; +using CleanArchitecture.Razor.Application.Features.AuditTrails.DTOs; +using CleanArchitecture.Razor.Domain.Entities.Audit; + +namespace CleanArchitecture.Razor.Application.AuditTrails.Queries.PaginationQuery +{ + public class AuditTrailsWithPaginationQuery : PaginationRequest, IRequest> + { + + + } + public class AuditTrailsQueryHandler : IRequestHandler> + { + private readonly ICurrentUserService _currentUserService; + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public AuditTrailsQueryHandler( + ICurrentUserService currentUserService, + IApplicationDbContext context, + IMapper mapper + ) + { + _currentUserService = currentUserService; + _context = context; + _mapper = mapper; + } + public async Task> Handle(AuditTrailsWithPaginationQuery request, CancellationToken cancellationToken) + { + var filters = PredicateBuilder.FromFilter(request.FilterRules); + + var data = await _context.AuditTrails + .Where(filters) + .OrderBy($"{request.Sort} {request.Order}") + .ProjectTo(_mapper.ConfigurationProvider) + .PaginatedDataAsync(request.Page, request.Rows); + + return data; + } + + + } +} diff --git a/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.Designer.cs b/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.Designer.cs new file mode 100644 index 00000000..b1ca1cfa --- /dev/null +++ b/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.Designer.cs @@ -0,0 +1,118 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace CleanArchitecture.Razor.Application.Resources.Features.AuditTrails.Queries.Export { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class ExportAuditTrailsQuery { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal ExportAuditTrailsQuery() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("CleanArchitecture.Razor.Application.Resources.Features.AuditTrails.Queries.Export" + + ".ExportAuditTrailsQuery", typeof(ExportAuditTrailsQuery).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to AuditTrails. + /// + internal static string AuditTrails { + get { + return ResourceManager.GetString("AuditTrails", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Date Time. + /// + internal static string Date_Time { + get { + return ResourceManager.GetString("Date Time", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to New Values. + /// + internal static string New_Values { + get { + return ResourceManager.GetString("New Values", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Old Values. + /// + internal static string Old_Values { + get { + return ResourceManager.GetString("Old Values", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Primary Key. + /// + internal static string Primary_Key { + get { + return ResourceManager.GetString("Primary Key", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Table Name. + /// + internal static string Table_Name { + get { + return ResourceManager.GetString("Table Name", resourceCulture); + } + } + } +} diff --git a/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.de-DE.resx b/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.de-DE.resx new file mode 100644 index 00000000..2b12c9d4 --- /dev/null +++ b/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.de-DE.resx @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Buchungsprotokolle + + + Terminzeit + + + Neue Werte + + + Alte Werte + + + Primärschlüssel + + + Tabellenname + + \ No newline at end of file diff --git a/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.en.resx b/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.en.resx new file mode 100644 index 00000000..d5f7260c --- /dev/null +++ b/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.en.resx @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + AuditTrails + + + Date Time + + + New Values + + + Old Values + + + Primary Key + + + Table Name + + \ No newline at end of file diff --git a/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.ja-JP.resx b/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.ja-JP.resx new file mode 100644 index 00000000..9965bed5 --- /dev/null +++ b/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.ja-JP.resx @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 監査証跡 + + + 日付時刻 + + + 新しい値 + + + 古い値 + + + 主キー + + + テーブル名 + + \ No newline at end of file diff --git a/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.resx b/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.resx new file mode 100644 index 00000000..d5f7260c --- /dev/null +++ b/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.resx @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + AuditTrails + + + Date Time + + + New Values + + + Old Values + + + Primary Key + + + Table Name + + \ No newline at end of file diff --git a/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.ru.resx b/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.ru.resx new file mode 100644 index 00000000..3d18b495 --- /dev/null +++ b/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.ru.resx @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + AuditTrails + + + Дата Время + + + Новые ценности + + + Старые ценности + + + Основной ключ + + + Имя таблицы + + \ No newline at end of file diff --git a/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.zh-CN.resx b/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.zh-CN.resx new file mode 100644 index 00000000..14ce030f --- /dev/null +++ b/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.zh-CN.resx @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 审计跟踪 + + + 记录时间 + + + 新值 + + + 旧值 + + + 主键 + + + 表名 + + \ No newline at end of file diff --git a/src/Domain/Common/IAuditTrial.cs b/src/Domain/Common/IAuditTrial.cs new file mode 100644 index 00000000..c3109178 --- /dev/null +++ b/src/Domain/Common/IAuditTrial.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CleanArchitecture.Razor.Domain.Common +{ + public interface IAuditTrial + { + } +} diff --git a/src/Domain/Domain.csproj b/src/Domain/Domain.csproj index 23098dbf..82714c00 100644 --- a/src/Domain/Domain.csproj +++ b/src/Domain/Domain.csproj @@ -6,4 +6,8 @@ CleanArchitecture.Razor.Domain + + + + diff --git a/src/Domain/Entities/Audit/AuditTrail.cs b/src/Domain/Entities/Audit/AuditTrail.cs new file mode 100644 index 00000000..7c269ee2 --- /dev/null +++ b/src/Domain/Entities/Audit/AuditTrail.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using CleanArchitecture.Razor.Domain.Common; +using CleanArchitecture.Razor.Domain.Enums; +using Microsoft.EntityFrameworkCore.ChangeTracking; + +namespace CleanArchitecture.Razor.Domain.Entities.Audit +{ + public class AuditTrail: IEntity + { + public int Id { get; set; } + public string UserId { get; set; } + public AuditType AuditType { get; set; } + public string TableName { get; set; } + public DateTime DateTime { get; set; } + public Dictionary OldValues { get; set; } = new(); + public Dictionary NewValues { get; set; } = new(); + public ICollection AffectedColumns { get; set; } + public Dictionary PrimaryKey { get; set; } = new(); + + public List TemporaryProperties { get; } = new(); + public bool HasTemporaryProperties => TemporaryProperties.Any(); + } +} diff --git a/src/Domain/Entities/Customer.cs b/src/Domain/Entities/Customer.cs index 94e094ba..14041e13 100644 --- a/src/Domain/Entities/Customer.cs +++ b/src/Domain/Entities/Customer.cs @@ -12,7 +12,7 @@ namespace CleanArchitecture.Razor.Domain.Entities { - public partial class Customer : AuditableEntity, IHasDomainEvent + public partial class Customer : AuditableEntity, IHasDomainEvent,IAuditTrial { public int Id { get; set; } public string Name { get; set; } diff --git a/src/Domain/Enums/AuditType.cs b/src/Domain/Enums/AuditType.cs new file mode 100644 index 00000000..fc565262 --- /dev/null +++ b/src/Domain/Enums/AuditType.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CleanArchitecture.Razor.Domain.Enums +{ + public enum AuditType + { + None, + Create, + Update, + Delete + } +} diff --git a/src/Infrastructure/Constants/Permission/Permissions.cs b/src/Infrastructure/Constants/Permission/Permissions.cs index 61538999..4f72e626 100644 --- a/src/Infrastructure/Constants/Permission/Permissions.cs +++ b/src/Infrastructure/Constants/Permission/Permissions.cs @@ -7,6 +7,14 @@ namespace CleanArchitecture.Razor.Infrastructure.Constants.Permission { public static class Permissions { + [DisplayName("AuditTrails")] + [Description("AuditTrails Permissions")] + public static class AuditTrails + { + public const string View = "Permissions.AuditTrails.View"; + public const string Search = "Permissions.AuditTrails.Search"; + public const string Export = "Permissions.AuditTrails.Export"; + } [DisplayName("Workflow")] [Description("Approval Permissions")] public static class Approval diff --git a/src/Infrastructure/Persistence/ApplicationDbContext.cs b/src/Infrastructure/Persistence/ApplicationDbContext.cs index ac901ff0..505c70d7 100644 --- a/src/Infrastructure/Persistence/ApplicationDbContext.cs +++ b/src/Infrastructure/Persistence/ApplicationDbContext.cs @@ -1,11 +1,14 @@ using CleanArchitecture.Razor.Application.Common.Interfaces; using CleanArchitecture.Razor.Domain.Common; using CleanArchitecture.Razor.Domain.Entities; +using CleanArchitecture.Razor.Domain.Entities.Audit; using CleanArchitecture.Razor.Domain.Entities.Worflow; +using CleanArchitecture.Razor.Domain.Enums; using CleanArchitecture.Razor.Infrastructure.Identity; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; +using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading; @@ -32,7 +35,7 @@ public ApplicationDbContext( _domainEventService = domainEventService; _dateTime = dateTime; } - + public DbSet AuditTrails { get; set; } public DbSet Customers { get; set; } public DbSet DocumentTypes { get; set; } public DbSet Documents { get; set; } @@ -42,6 +45,8 @@ public ApplicationDbContext( public override async Task SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken()) { + var auditEntries = OnBeforeSaveChanges(_currentUserService.UserId); + foreach (var entry in ChangeTracker.Entries()) { switch (entry.State) @@ -69,7 +74,7 @@ public ApplicationDbContext( var result = await base.SaveChangesAsync(cancellationToken); await DispatchEvents(); - + await OnAfterSaveChanges(auditEntries, cancellationToken); return result; } @@ -95,5 +100,95 @@ private async Task DispatchEvents() await _domainEventService.Publish(domainEventEntity); } } + + + private List OnBeforeSaveChanges(string userId) + { + ChangeTracker.DetectChanges(); + var auditEntries = new List(); + foreach (var entry in ChangeTracker.Entries()) + { + if (entry.Entity is AuditTrail || + entry.State == EntityState.Detached || + entry.State == EntityState.Unchanged) + continue; + + var auditEntry = new AuditTrail() + { + DateTime = _dateTime.Now, + TableName = entry.Entity.GetType().Name, + UserId = userId, + AffectedColumns = new List() + }; + auditEntries.Add(auditEntry); + foreach (var property in entry.Properties) + { + + if (property.IsTemporary) + { + auditEntry.TemporaryProperties.Add(property); + continue; + } + string propertyName = property.Metadata.Name; + if (property.Metadata.IsPrimaryKey()) + { + auditEntry.PrimaryKey[propertyName] = property.CurrentValue; + continue; + } + + switch (entry.State) + { + case EntityState.Added: + auditEntry.AuditType = AuditType.Create; + auditEntry.NewValues[propertyName] = property.CurrentValue; + break; + + case EntityState.Deleted: + auditEntry.AuditType = AuditType.Delete; + auditEntry.OldValues[propertyName] = property.OriginalValue; + break; + + case EntityState.Modified: + if (property.IsModified && property.OriginalValue?.Equals(property.CurrentValue) == false) + { + auditEntry.AffectedColumns.Add(propertyName); + auditEntry.AuditType = AuditType.Update; + auditEntry.OldValues[propertyName] = property.OriginalValue; + auditEntry.NewValues[propertyName] = property.CurrentValue; + } + break; + } + } + } + + foreach (var auditEntry in auditEntries.Where(_ => !_.HasTemporaryProperties)) + { + AuditTrails.Add(auditEntry); + } + return auditEntries.Where(_ => _.HasTemporaryProperties).ToList(); + } + + private Task OnAfterSaveChanges(List auditEntries, CancellationToken cancellationToken = new()) + { + if (auditEntries == null || auditEntries.Count == 0) + return Task.CompletedTask; + + foreach (var auditEntry in auditEntries) + { + foreach (var prop in auditEntry.TemporaryProperties) + { + if (prop.Metadata.IsPrimaryKey()) + { + auditEntry.PrimaryKey[prop.Metadata.Name] = prop.CurrentValue; + } + else + { + auditEntry.NewValues[prop.Metadata.Name] = prop.CurrentValue; + } + } + AuditTrails.Add(auditEntry); + } + return SaveChangesAsync(cancellationToken); + } } } diff --git a/src/Infrastructure/Persistence/Configurations/AuditTrailConfiguration.cs b/src/Infrastructure/Persistence/Configurations/AuditTrailConfiguration.cs new file mode 100644 index 00000000..a4850d1c --- /dev/null +++ b/src/Infrastructure/Persistence/Configurations/AuditTrailConfiguration.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using CleanArchitecture.Razor.Domain.Entities; +using CleanArchitecture.Razor.Domain.Entities.Audit; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace CleanArchitecture.Infrastructure.Persistence.Configurations +{ + public class AuditTrailConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.Property(t => t.AuditType) + .HasConversion(); + builder.Property(e => e.AffectedColumns) + .HasConversion( + v => JsonSerializer.Serialize(v, null), + v => JsonSerializer.Deserialize>(v, null), + new ValueComparer>( + (c1, c2) => c1.SequenceEqual(c2), + c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())), + c => (ICollection)c.ToList())); + + builder.Property(u => u.OldValues) + .HasConversion( + d => JsonSerializer.Serialize(d, null), + s => JsonSerializer.Deserialize>(s, null) + ); + builder.Property(u => u.NewValues) + .HasConversion( + d => JsonSerializer.Serialize(d, null), + s => JsonSerializer.Deserialize>(s, null) + ); + builder.Property(u => u.PrimaryKey) + .HasConversion( + d => JsonSerializer.Serialize(d, null), + s => JsonSerializer.Deserialize>(s, null) + ); + + builder.Ignore(x => x.TemporaryProperties); + builder.Ignore(x => x.HasTemporaryProperties); + } + } +} diff --git a/src/SmartAdmin.WebUI/Pages/Approval/Histories.cshtml b/src/SmartAdmin.WebUI/Pages/Approval/Histories.cshtml index 59fdfa75..6a52b82b 100644 --- a/src/SmartAdmin.WebUI/Pages/Approval/Histories.cshtml +++ b/src/SmartAdmin.WebUI/Pages/Approval/Histories.cshtml @@ -90,23 +90,6 @@ sortOrder: 'desc', pageSize: 15, pageList: [10, 15, 30, 50, 100, 1000], - onBeforeLoad: function () { - $('#approvalbutton').prop('disabled', true); - }, - onCheckAll: function (rows) { - const checked = $(this).datagrid('getChecked').length > 0; - $('#approvalbutton').prop('disabled', !checked); - }, - onUncheckAll: function () { - $('#approvalbutton').prop('disabled', true); - }, - onCheck: function () { - $('#approvalbutton').prop('disabled', false); - }, - onUncheck: function () { - const checked = $(this).datagrid('getChecked').length > 0; - $('#approvalbutton').prop('disabled', !checked); - }, columns: [[ { field: 'ck', checkbox: true }, { field: 'WorkflowName', title: '@_localizer["Workflow Name"]', sortable: true, width: 200 }, diff --git a/src/SmartAdmin.WebUI/Pages/AuditTrails/Index.cshtml b/src/SmartAdmin.WebUI/Pages/AuditTrails/Index.cshtml new file mode 100644 index 00000000..3234e6cc --- /dev/null +++ b/src/SmartAdmin.WebUI/Pages/AuditTrails/Index.cshtml @@ -0,0 +1,168 @@ +@page +@using CleanArchitecture.Razor.Infrastructure.Constants.Permission +@model SmartAdmin.WebUI.Pages.AuditTrails.IndexModel +@inject Microsoft.Extensions.Localization.IStringLocalizer _localizer +@inject Microsoft.AspNetCore.Authorization.IAuthorizationService _authorizationService +@{ + ViewData["Title"] = _localizer["Audit Trails"].Value; + ViewData["PageName"] = "audittrails_index"; + ViewData["Category1"] = _localizer["Authorization"].Value; + ViewData["Heading"] = _localizer["Audit Trails"].Value; + ViewData["PageDescription"] = _localizer["See all available options"].Value; + ViewData["PreemptiveClass"] = "Default"; + var _canSearch = await _authorizationService.AuthorizeAsync(User, null, Permissions.AuditTrails.Search); + var _canExport = await _authorizationService.AuthorizeAsync(User, null, Permissions.AuditTrails.Export); + +} +@section HeadBlock { + + + + + +} +
+
+

+ @_localizer["Audit Trails"] + @_localizer["See all available options"] +

+
+ @if (_canSearch.Succeeded) + { + + } + @if (_canExport.Succeeded) + { + + } + +
+
+
+
+
+ +
+
+
+
+
+ + +@section ScriptsBlock { + + + + + + + +} diff --git a/src/SmartAdmin.WebUI/Pages/AuditTrails/Index.cshtml.cs b/src/SmartAdmin.WebUI/Pages/AuditTrails/Index.cshtml.cs new file mode 100644 index 00000000..384517df --- /dev/null +++ b/src/SmartAdmin.WebUI/Pages/AuditTrails/Index.cshtml.cs @@ -0,0 +1,55 @@ +using System.Threading.Tasks; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Localization; +using Microsoft.AspNetCore.Authorization; +using CleanArchitecture.Razor.Application.Features.ApprovalDatas.Queries.Pagination; +using CleanArchitecture.Razor.Infrastructure.Constants.Permission; +using CleanArchitecture.Razor.Application.Features.ApprovalDatas.Queries.Export; +using CleanArchitecture.Razor.Application.AuditTrails.Queries.PaginationQuery; +using CleanArchitecture.Razor.Application.Features.AuditTrails.Queries.Export; + +namespace SmartAdmin.WebUI.Pages.AuditTrails +{ + [Authorize(policy: Permissions.AuditTrails.View)] + public class IndexModel : PageModel + { + private readonly ISender _mediator; + private readonly IStringLocalizer _localizer; + [BindProperty] + public string WorkflowId { get; set; } + [BindProperty] + public string Comments { get; set; } + [BindProperty] + public string Outcome { get; set; } + [BindProperty] + public string Approver { get; set; } + public IndexModel( + ISender mediator, + IStringLocalizer localizer + ) + { + _mediator = mediator; + _localizer = localizer; + } + public async Task OnGetAsync() + { + + } + public async Task OnGetDataAsync([FromQuery] AuditTrailsWithPaginationQuery command) + { + var result = await _mediator.Send(command); + return new JsonResult(result); + } + public async Task OnPostExportAsync([FromBody] ExportAuditTrailsQuery command) + { + var result = await _mediator.Send(command); + return File(result, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", _localizer["ApprovalHistories"] + ".xlsx"); + } + + + + + } +} diff --git a/src/SmartAdmin.WebUI/Program.cs b/src/SmartAdmin.WebUI/Program.cs index 89698ca6..37e351a4 100644 --- a/src/SmartAdmin.WebUI/Program.cs +++ b/src/SmartAdmin.WebUI/Program.cs @@ -15,7 +15,7 @@ namespace SmartAdmin.WebUI { public class Program { - public async static Task Main(string[] args) + public static async Task Main(string[] args) { var filePath = Path.Combine(Directory.GetCurrentDirectory(), "Files"); if (!Directory.Exists(filePath)) diff --git a/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.de-DE.resx b/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.de-DE.resx new file mode 100644 index 00000000..8fa717f3 --- /dev/null +++ b/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.de-DE.resx @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Buchungsprotokolle + + + Audit-Typ + + + Genehmigung + + + Terminzeit + + + Excel exportieren + + + Exportieren fehlgeschlagen + + + Exporterfolg + + + Neue Werte + + + Alte Werte + + + Primärschlüssel + + + Suche + + + Alle verfügbaren Optionen anzeigen + + + Tabellenname + + \ No newline at end of file diff --git a/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.en.resx b/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.en.resx new file mode 100644 index 00000000..f239779b --- /dev/null +++ b/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.en.resx @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Audit Trails + + + Audit Type + + + Authorization + + + Date Time + + + Export Excel + + + Export fail + + + Export Success + + + New Values + + + Old Values + + + Primary Key + + + Search + + + See all available options + + + Table Name + + \ No newline at end of file diff --git a/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.ja-JP.resx b/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.ja-JP.resx new file mode 100644 index 00000000..b7159ec9 --- /dev/null +++ b/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.ja-JP.resx @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 監査証跡 + + + 監査タイプ + + + 承認 + + + 日付時刻 + + + Excelをエクスポートする + + + エクスポートに失敗する + + + エクスポートの成功 + + + 新しい値 + + + 古い値 + + + 主キー + + + 検索 + + + 利用可能なすべてのオプションを表示 + + + テーブル名 + + \ No newline at end of file diff --git a/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.resx b/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.resx new file mode 100644 index 00000000..f239779b --- /dev/null +++ b/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.resx @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Audit Trails + + + Audit Type + + + Authorization + + + Date Time + + + Export Excel + + + Export fail + + + Export Success + + + New Values + + + Old Values + + + Primary Key + + + Search + + + See all available options + + + Table Name + + \ No newline at end of file diff --git a/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.ru.resx b/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.ru.resx new file mode 100644 index 00000000..fcf190aa --- /dev/null +++ b/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.ru.resx @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Журналы аудита + + + Тип аудита + + + Авторизация + + + Дата Время + + + Экспорт в Excel + + + Ошибка экспорта + + + Успех экспорта + + + Новые ценности + + + Старые ценности + + + Основной ключ + + + Поиск + + + Посмотреть все доступные варианты + + + Имя таблицы + + \ No newline at end of file diff --git a/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.zh-CN.resx b/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.zh-CN.resx new file mode 100644 index 00000000..db5ed90b --- /dev/null +++ b/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.zh-CN.resx @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 审计跟踪 + + + 审核类型 + + + 授权 + + + 记录时间 + + + 导出 Excel + + + 导出失败 + + + 出口成功 + + + 新值 + + + 旧值 + + + 主键 + + + 搜索 + + + 查看所有可用选项 + + + 表名 + + \ No newline at end of file diff --git a/src/SmartAdmin.WebUI/nav.json b/src/SmartAdmin.WebUI/nav.json index de780228..8ce18d13 100644 --- a/src/SmartAdmin.WebUI/nav.json +++ b/src/SmartAdmin.WebUI/nav.json @@ -81,6 +81,11 @@ "title": "Roles", "href": "authorization_roles.html", "roles": [] + }, + { + "title": "Audit Trails", + "href": "audittrails_index.html", + "roles": [] } ] },