using System.Security.Claims; using WhatApi.Extras; using WhatApi.Tables; namespace WhatApi; public class Database(DbContextOptions options, IHttpContextAccessor httpContextAccessor, IConfiguration configuration) : DbContext(options) { public DbSet Content => Set(); public DbSet Places => Set(); public DbSet Users => Set(); public DbSet AuditTrails => Set(); protected override void OnModelCreating(ModelBuilder b) { b.HasPostgresExtension("postgis"); b.ApplyConfiguration(new AuditTrailConfiguration()); b.ApplyConfiguration(new PlaceConfiguration()); b.ApplyConfiguration(new UserConfiguration()); b.ApplyConfiguration(new ContentConfiguration()); base.OnModelCreating(b); } public override int SaveChanges() { if (configuration.GetValue("DISABLE_AUDIT_TRAILS")) return base.SaveChanges(); SetAuditableProperties(); var auditEntries = GetActiveAuditTrails(); if (auditEntries.Count != 0) AuditTrails.AddRange(auditEntries); return base.SaveChanges(); } public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) { if (configuration.GetValue("DISABLE_AUDIT_TRAILS")) return await base.SaveChangesAsync(cancellationToken); SetAuditableProperties(); var auditEntries = GetActiveAuditTrails(); if (auditEntries.Count != 0) await AuditTrails.AddRangeAsync(auditEntries, cancellationToken); return await base.SaveChangesAsync(cancellationToken); } private List GetActiveAuditTrails() { var userId = GetUserId(); var entries = ChangeTracker.Entries() .Where(e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted); var auditTrails = new List(); foreach (var entry in entries) { var audit = new AuditTrail { Id = Guid.NewGuid(), UserId = userId, EntityName = entry.Entity.GetType().Name, DateUtc = DateTimeOffset.UtcNow }; foreach (var prop in entry.Properties) { if (prop.Metadata.IsPrimaryKey()) { audit.PrimaryKey = prop.CurrentValue?.ToString(); continue; } if (prop.Metadata.PropertyInfo?.CustomAttributes.FirstOrDefault(c => c.AttributeType == typeof(AuditTrailIgnoreAttribute)) != null) continue; var name = prop.Metadata.Name; switch (entry.State) { case EntityState.Added: audit.TrailType = TrailType.Create; audit.NewValues[name] = prop.CurrentValue; break; case EntityState.Deleted: audit.TrailType = TrailType.Delete; audit.OldValues[name] = prop.OriginalValue; break; case EntityState.Modified: if (!Equals(prop.OriginalValue, prop.CurrentValue)) { audit.TrailType = TrailType.Update; audit.ChangedColumns.Add(name); audit.OldValues[name] = prop.OriginalValue; audit.NewValues[name] = prop.CurrentValue; } break; } } if (audit.TrailType != TrailType.None) auditTrails.Add(audit); } return auditTrails; } private Guid GetUserId() { var system = new Guid("e87ab078-55bc-4655-86d9-c5b2ecad7162"); var userIdString = httpContextAccessor.HttpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier); return string.IsNullOrWhiteSpace(userIdString) ? system : new Guid(userIdString); } private void SetAuditableProperties() { var actor = GetUserId(); foreach (var entry in ChangeTracker.Entries()) { switch (entry.State) { case EntityState.Added: entry.Entity.CreatedAtUtc = DateTimeOffset.UtcNow; entry.Entity.CreatedBy = actor; break; case EntityState.Modified: entry.Entity.UpdatedAtUtc = DateTimeOffset.UtcNow; entry.Entity.UpdatedBy = actor; break; } } } }