feature: RBAC + Clinical Audit Logging
This commit is contained in:
@@ -19,6 +19,8 @@ public class AppDbContext : DbContext
|
||||
public DbSet<GcsScore> GcsScores => Set<GcsScore>();
|
||||
public DbSet<SofaScore> SofaScores => Set<SofaScore>();
|
||||
public DbSet<ExternalResourceIdentifier> ExternalResourceIdentifiers => Set<ExternalResourceIdentifier>();
|
||||
public DbSet<ClinicalUser> ClinicalUsers => Set<ClinicalUser>();
|
||||
public DbSet<ClinicalAuditLog> ClinicalAuditLogs => Set<ClinicalAuditLog>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
public class ClinicalAuditLogConfiguration : IEntityTypeConfiguration<ClinicalAuditLog>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ClinicalAuditLog> builder)
|
||||
{
|
||||
builder.ToTable("clinical_audit_logs");
|
||||
builder.HasKey(a => a.Id);
|
||||
builder.Property(a => a.Id).HasColumnName("id").HasDefaultValueSql("gen_random_uuid()");
|
||||
builder.Property(a => a.Action).HasColumnName("action").HasMaxLength(50).IsRequired()
|
||||
.HasConversion(v => v.ToDbString(), v => AuditActionExtensions.FromDbString(v));
|
||||
builder.Property(a => a.EntityType).HasColumnName("entity_type").HasMaxLength(100).IsRequired();
|
||||
builder.Property(a => a.EntityId).HasColumnName("entity_id").IsRequired();
|
||||
builder.Property(a => a.UserId).HasColumnName("user_id");
|
||||
builder.Property(a => a.UserDisplayName).HasColumnName("user_display_name").HasMaxLength(200);
|
||||
builder.Property(a => a.PreviousValueJson).HasColumnName("previous_value_json").HasColumnType("jsonb");
|
||||
builder.Property(a => a.NewValueJson).HasColumnName("new_value_json").HasColumnType("jsonb");
|
||||
builder.Property(a => a.Reason).HasColumnName("reason");
|
||||
builder.Property(a => a.IpAddress).HasColumnName("ip_address").HasMaxLength(45);
|
||||
builder.Property(a => a.CorrelationId).HasColumnName("correlation_id").HasMaxLength(100);
|
||||
builder.Property(a => a.CreatedAt).HasColumnName("created_at").HasDefaultValueSql("NOW()");
|
||||
|
||||
// Append-only — no UPDATE/DELETE from application code
|
||||
builder.HasIndex(a => a.EntityType);
|
||||
builder.HasIndex(a => a.EntityId);
|
||||
builder.HasIndex(a => a.UserId);
|
||||
builder.HasIndex(a => a.CreatedAt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
public class ClinicalUserConfiguration : IEntityTypeConfiguration<ClinicalUser>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ClinicalUser> builder)
|
||||
{
|
||||
builder.ToTable("clinical_users");
|
||||
builder.HasKey(u => u.Id);
|
||||
builder.Property(u => u.Id).HasColumnName("id").HasDefaultValueSql("gen_random_uuid()");
|
||||
builder.Property(u => u.Username).HasColumnName("username").HasMaxLength(100).IsRequired();
|
||||
builder.Property(u => u.PasswordHash).HasColumnName("password_hash").HasMaxLength(500).IsRequired();
|
||||
builder.Property(u => u.DisplayName).HasColumnName("display_name").HasMaxLength(200).IsRequired();
|
||||
builder.Property(u => u.Role).HasColumnName("role").HasMaxLength(20).IsRequired()
|
||||
.HasConversion(v => v.ToDbString(), v => ClinicalRoleExtensions.FromDbString(v));
|
||||
builder.Property(u => u.IsActive).HasColumnName("is_active").HasDefaultValue(true);
|
||||
builder.Property(u => u.CreatedAt).HasColumnName("created_at").HasDefaultValueSql("NOW()");
|
||||
builder.Property(u => u.LastLoginAt).HasColumnName("last_login_at");
|
||||
|
||||
builder.HasIndex(u => u.Username).IsUnique();
|
||||
}
|
||||
}
|
||||
@@ -97,6 +97,7 @@ public static class DataSeeder
|
||||
|
||||
public static async Task SeedThresholdsOnlyAsync(AppDbContext db, IConnectionMultiplexer redis)
|
||||
{
|
||||
await db.Database.ExecuteSqlRawAsync("DELETE FROM alert_thresholds");
|
||||
var thresholds = BuildDefaultThresholds();
|
||||
db.AlertThresholds.AddRange(thresholds);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
public static class UserSeeder
|
||||
{
|
||||
public static async Task SeedAsync(AppDbContext db)
|
||||
{
|
||||
if (await db.ClinicalUsers.AnyAsync())
|
||||
return;
|
||||
|
||||
db.ClinicalUsers.AddRange(
|
||||
new ClinicalUser
|
||||
{
|
||||
Id = Guid.Parse("11111111-1111-1111-1111-111111111111"),
|
||||
Username = "nurse.demo",
|
||||
PasswordHash = BCrypt.Net.BCrypt.HashPassword("DemoNurse1!"),
|
||||
DisplayName = "Demo Nurse",
|
||||
Role = ClinicalRole.Nurse,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new ClinicalUser
|
||||
{
|
||||
Id = Guid.Parse("22222222-2222-2222-2222-222222222222"),
|
||||
Username = "physician.demo",
|
||||
PasswordHash = BCrypt.Net.BCrypt.HashPassword("DemoPhysician1!"),
|
||||
DisplayName = "Dr. Demo Physician",
|
||||
Role = ClinicalRole.Physician,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new ClinicalUser
|
||||
{
|
||||
Id = Guid.Parse("33333333-3333-3333-3333-333333333333"),
|
||||
Username = "admin.demo",
|
||||
PasswordHash = BCrypt.Net.BCrypt.HashPassword("DemoAdmin1!"),
|
||||
DisplayName = "Demo Admin",
|
||||
Role = ClinicalRole.Admin,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new ClinicalUser
|
||||
{
|
||||
Id = Guid.Parse("44444444-4444-4444-4444-444444444444"),
|
||||
Username = "integration.mirth",
|
||||
PasswordHash = BCrypt.Net.BCrypt.HashPassword("MirthIntegration1!"),
|
||||
DisplayName = "Mirth Connect",
|
||||
Role = ClinicalRole.Integration,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
});
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user