Loading src/api/Controllers/AuditHistoryExampleController.cs 0 → 100644 +26 −0 Original line number Diff line number Diff line using Cals.Visor.Infrastructure.Contexts; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace Cals.Visor.Api.Controllers; [Route("api/[controller]")] [ApiController] public class AuditHistoryExampleController : ControllerBase { private readonly ApplicationDbContext _context; public AuditHistoryExampleController(ApplicationDbContext context) => _context = context; [HttpGet("{table}/{id}/history")] public async Task<IActionResult> GetHistory(string table, string id) { var logs = await _context.AuditLogs .Where(x => x.TableName == table && x.EntityId == id) .OrderByDescending(x => x.CreatedAt) .ToListAsync(); return Ok(logs); } } src/api/Identity/IdentityMiddleware.cs +26 −0 Original line number Diff line number Diff line using Cals.Authentication; using Cals.Visor.Identity; using Cals.Visor.Models.Identity; using Microsoft.Extensions.Options; namespace Cals.Visor.Api.Identity; Loading Loading @@ -51,4 +53,28 @@ public class IdentityMiddleware await _next(context); } private async Task<AppUser> EnsureUserIsRegistered(IApplicationUser user) { // unique is required to do anything with user registration if (user.UniqueId == null) throw new Exception("User must have an non-null UniqueId to be registered"); AppUser userToUpdate = user.ToAppUser(); // is this user already registered? AppUser? userAtApi = await _apiClient.AppUsers.GetUserAsync(user.UniqueId); if (userAtApi != null) { userToUpdate.AppUserId = userAtApi.AppUserId; await _apiClient.AppUsers.UpdateUserAsync(userToUpdate); return userToUpdate; } userAtApi = await _apiClient.AppUsers.RegisterUserAsync(userToUpdate); return userAtApi; } } src/api/Middleware/CorrelationIdMiddleware.cs 0 → 100644 +28 −0 Original line number Diff line number Diff line using Microsoft.Extensions.Primitives; namespace Cals.Visor.Api.Middleware; /// <summary> /// Middleware to attach a unique correlation ID to every request /// </summary> /// <remarks> /// All changes inside one API call can be grouped /// </remarks> public class CorrelationIdMiddleware { private const string HeaderName = "X-Correlation-ID"; private readonly RequestDelegate _next; public CorrelationIdMiddleware(RequestDelegate next) => _next = next; public async Task Invoke(HttpContext context) { if (!context.Request.Headers.TryGetValue(HeaderName, out StringValues correlationId)) correlationId = Guid.NewGuid().ToString(); context.Items["CorrelationId"] = correlationId; context.Response.Headers[HeaderName] = correlationId; await _next(context); } } src/api/Program.cs +3 −0 Original line number Diff line number Diff line Loading @@ -4,6 +4,7 @@ using Azure.Core; using Azure.Identity; using Cals.Visor.Api.Authorization; using Cals.Visor.Api.Identity; using Cals.Visor.Api.Middleware; using Cals.Visor.Application; using Cals.Visor.Application.Identity; using Cals.Visor.Infrastructure; Loading Loading @@ -113,6 +114,8 @@ builder.Host.UseDefaultServiceProvider(o => WebApplication app = builder.Build(); app.UseMiddleware<CorrelationIdMiddleware>(); app.UseStaticFiles(); // AFTER UseStaticFiles, UseExceptionHandler or UseStatusCodePagesWithReExecute but BEFORE UseAuthentication and UseAuthorization Loading src/infrastructure/Contexts/ApplicationDbContext.cs +19 −10 Original line number Diff line number Diff line Loading @@ -9,30 +9,38 @@ namespace Cals.Visor.Infrastructure.Contexts; // TODO - change to internal - only use repositories to talk to Context public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) private readonly AuditSaveChangesInterceptor _auditInterceptor; public ApplicationDbContext( DbContextOptions<ApplicationDbContext> options, AuditSaveChangesInterceptor auditInterceptor) : base(options) { // ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; _auditInterceptor = auditInterceptor; } public DbSet<AppUser> Users => Set<AppUser>(); public DbSet<Person> People => Set<Person>(); public DbSet<PaloAltoFirewall> PaloAltoFirewalls => Set<PaloAltoFirewall>(); public DbSet<Subnet> Subnets => Set<Subnet>(); public DbSet<Area> Areas => Set<Area>(); public DbSet<AuditLog> AuditLogs => Set<AuditLog>(); public DbSet<InfobloxDomain> InfobloxDomains => Set<InfobloxDomain>(); public DbSet<InfobloxGroup> InfobloxGroups => Set<InfobloxGroup>(); public DbSet<InfobloxNetwork> InfobloxNetworks => Set<InfobloxNetwork>(); public DbSet<InfobloxRole> InfobloxRoles => Set<InfobloxRole>(); public DbSet<InfobloxRoleAccess> InfobloxRoleAccess => Set<InfobloxRoleAccess>(); public DbSet<Location> Locations => Set<Location>(); public DbSet<LocationVlan> LocationVlans => Set<LocationVlan>(); public DbSet<PaloAltoAccessDomain> PaloAltoAccessDomains => Set<PaloAltoAccessDomain>(); public DbSet<PaloAltoFirewall> PaloAltoFirewalls => Set<PaloAltoFirewall>(); public DbSet<Person> People => Set<Person>(); public DbSet<Vlan> Vlans => Set<Vlan>(); public DbSet<VlanUseType> VlanUseTypes => Set<VlanUseType>(); public DbSet<PaloAltoVsys> PaloAltoVsyses => Set<PaloAltoVsys>(); public DbSet<Subnet> Subnets => Set<Subnet>(); public DbSet<WiscNicGroup> WiscNicGroups => Set<WiscNicGroup>(); public DbSet<WiscNicGroupMembershipType> WiscNicGroupMembershipTypes => Set<WiscNicGroupMembershipType>(); public DbSet<InfobloxDomain> InfobloxDomains => Set<InfobloxDomain>(); public DbSet<InfobloxGroup> InfobloxGroups => Set<InfobloxGroup>(); public DbSet<InfobloxNetwork> InfobloxNetworks => Set<InfobloxNetwork>(); public DbSet<InfobloxRole> InfobloxRoles => Set<InfobloxRole>(); public DbSet<InfobloxRoleAccess> InfobloxRoleAccess => Set<InfobloxRoleAccess>(); protected override void OnModelCreating(ModelBuilder modelBuilder) { Loading @@ -58,7 +66,8 @@ public class ApplicationDbContext : DbContext protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder .UseSnakeCaseNamingConvention(); .UseSnakeCaseNamingConvention() .AddInterceptors(_auditInterceptor); } public override int SaveChanges(bool acceptAllChangesOnSuccess) Loading Loading
src/api/Controllers/AuditHistoryExampleController.cs 0 → 100644 +26 −0 Original line number Diff line number Diff line using Cals.Visor.Infrastructure.Contexts; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace Cals.Visor.Api.Controllers; [Route("api/[controller]")] [ApiController] public class AuditHistoryExampleController : ControllerBase { private readonly ApplicationDbContext _context; public AuditHistoryExampleController(ApplicationDbContext context) => _context = context; [HttpGet("{table}/{id}/history")] public async Task<IActionResult> GetHistory(string table, string id) { var logs = await _context.AuditLogs .Where(x => x.TableName == table && x.EntityId == id) .OrderByDescending(x => x.CreatedAt) .ToListAsync(); return Ok(logs); } }
src/api/Identity/IdentityMiddleware.cs +26 −0 Original line number Diff line number Diff line using Cals.Authentication; using Cals.Visor.Identity; using Cals.Visor.Models.Identity; using Microsoft.Extensions.Options; namespace Cals.Visor.Api.Identity; Loading Loading @@ -51,4 +53,28 @@ public class IdentityMiddleware await _next(context); } private async Task<AppUser> EnsureUserIsRegistered(IApplicationUser user) { // unique is required to do anything with user registration if (user.UniqueId == null) throw new Exception("User must have an non-null UniqueId to be registered"); AppUser userToUpdate = user.ToAppUser(); // is this user already registered? AppUser? userAtApi = await _apiClient.AppUsers.GetUserAsync(user.UniqueId); if (userAtApi != null) { userToUpdate.AppUserId = userAtApi.AppUserId; await _apiClient.AppUsers.UpdateUserAsync(userToUpdate); return userToUpdate; } userAtApi = await _apiClient.AppUsers.RegisterUserAsync(userToUpdate); return userAtApi; } }
src/api/Middleware/CorrelationIdMiddleware.cs 0 → 100644 +28 −0 Original line number Diff line number Diff line using Microsoft.Extensions.Primitives; namespace Cals.Visor.Api.Middleware; /// <summary> /// Middleware to attach a unique correlation ID to every request /// </summary> /// <remarks> /// All changes inside one API call can be grouped /// </remarks> public class CorrelationIdMiddleware { private const string HeaderName = "X-Correlation-ID"; private readonly RequestDelegate _next; public CorrelationIdMiddleware(RequestDelegate next) => _next = next; public async Task Invoke(HttpContext context) { if (!context.Request.Headers.TryGetValue(HeaderName, out StringValues correlationId)) correlationId = Guid.NewGuid().ToString(); context.Items["CorrelationId"] = correlationId; context.Response.Headers[HeaderName] = correlationId; await _next(context); } }
src/api/Program.cs +3 −0 Original line number Diff line number Diff line Loading @@ -4,6 +4,7 @@ using Azure.Core; using Azure.Identity; using Cals.Visor.Api.Authorization; using Cals.Visor.Api.Identity; using Cals.Visor.Api.Middleware; using Cals.Visor.Application; using Cals.Visor.Application.Identity; using Cals.Visor.Infrastructure; Loading Loading @@ -113,6 +114,8 @@ builder.Host.UseDefaultServiceProvider(o => WebApplication app = builder.Build(); app.UseMiddleware<CorrelationIdMiddleware>(); app.UseStaticFiles(); // AFTER UseStaticFiles, UseExceptionHandler or UseStatusCodePagesWithReExecute but BEFORE UseAuthentication and UseAuthorization Loading
src/infrastructure/Contexts/ApplicationDbContext.cs +19 −10 Original line number Diff line number Diff line Loading @@ -9,30 +9,38 @@ namespace Cals.Visor.Infrastructure.Contexts; // TODO - change to internal - only use repositories to talk to Context public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) private readonly AuditSaveChangesInterceptor _auditInterceptor; public ApplicationDbContext( DbContextOptions<ApplicationDbContext> options, AuditSaveChangesInterceptor auditInterceptor) : base(options) { // ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; _auditInterceptor = auditInterceptor; } public DbSet<AppUser> Users => Set<AppUser>(); public DbSet<Person> People => Set<Person>(); public DbSet<PaloAltoFirewall> PaloAltoFirewalls => Set<PaloAltoFirewall>(); public DbSet<Subnet> Subnets => Set<Subnet>(); public DbSet<Area> Areas => Set<Area>(); public DbSet<AuditLog> AuditLogs => Set<AuditLog>(); public DbSet<InfobloxDomain> InfobloxDomains => Set<InfobloxDomain>(); public DbSet<InfobloxGroup> InfobloxGroups => Set<InfobloxGroup>(); public DbSet<InfobloxNetwork> InfobloxNetworks => Set<InfobloxNetwork>(); public DbSet<InfobloxRole> InfobloxRoles => Set<InfobloxRole>(); public DbSet<InfobloxRoleAccess> InfobloxRoleAccess => Set<InfobloxRoleAccess>(); public DbSet<Location> Locations => Set<Location>(); public DbSet<LocationVlan> LocationVlans => Set<LocationVlan>(); public DbSet<PaloAltoAccessDomain> PaloAltoAccessDomains => Set<PaloAltoAccessDomain>(); public DbSet<PaloAltoFirewall> PaloAltoFirewalls => Set<PaloAltoFirewall>(); public DbSet<Person> People => Set<Person>(); public DbSet<Vlan> Vlans => Set<Vlan>(); public DbSet<VlanUseType> VlanUseTypes => Set<VlanUseType>(); public DbSet<PaloAltoVsys> PaloAltoVsyses => Set<PaloAltoVsys>(); public DbSet<Subnet> Subnets => Set<Subnet>(); public DbSet<WiscNicGroup> WiscNicGroups => Set<WiscNicGroup>(); public DbSet<WiscNicGroupMembershipType> WiscNicGroupMembershipTypes => Set<WiscNicGroupMembershipType>(); public DbSet<InfobloxDomain> InfobloxDomains => Set<InfobloxDomain>(); public DbSet<InfobloxGroup> InfobloxGroups => Set<InfobloxGroup>(); public DbSet<InfobloxNetwork> InfobloxNetworks => Set<InfobloxNetwork>(); public DbSet<InfobloxRole> InfobloxRoles => Set<InfobloxRole>(); public DbSet<InfobloxRoleAccess> InfobloxRoleAccess => Set<InfobloxRoleAccess>(); protected override void OnModelCreating(ModelBuilder modelBuilder) { Loading @@ -58,7 +66,8 @@ public class ApplicationDbContext : DbContext protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder .UseSnakeCaseNamingConvention(); .UseSnakeCaseNamingConvention() .AddInterceptors(_auditInterceptor); } public override int SaveChanges(bool acceptAllChangesOnSuccess) Loading