161 lines
6.1 KiB
C#
161 lines
6.1 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Logging;
|
|
using StackExchange.Redis;
|
|
|
|
/// <summary>
|
|
/// Production-safe, idempotent bootstrap: default alert thresholds, optional role
|
|
/// accounts (passwords from config), and optional site/gateway matching env GUIDs.
|
|
/// Never creates patient PHI or demo credentials.
|
|
/// </summary>
|
|
public static class BootstrapSeeder
|
|
{
|
|
public static async Task SeedAsync(
|
|
AppDbContext db,
|
|
IConnectionMultiplexer redis,
|
|
SeedingOptions options,
|
|
ILogger logger)
|
|
{
|
|
await DataSeeder.EnsureDefaultThresholdsAsync(db, redis);
|
|
|
|
await EnsureBootstrapUserAsync(db, options.Admin, ClinicalRole.Admin, logger);
|
|
await EnsureBootstrapUserAsync(db, options.Nurse, ClinicalRole.Nurse, logger);
|
|
await EnsureBootstrapUserAsync(db, options.Physician, ClinicalRole.Physician, logger);
|
|
|
|
await EnsureSiteAndGatewayAsync(db, options, logger);
|
|
}
|
|
|
|
private static async Task EnsureBootstrapUserAsync(
|
|
AppDbContext db,
|
|
BootstrapUserOptions? user,
|
|
ClinicalRole role,
|
|
ILogger logger)
|
|
{
|
|
if (user is null
|
|
|| string.IsNullOrWhiteSpace(user.Username)
|
|
|| string.IsNullOrWhiteSpace(user.Password))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var username = user.Username.Trim();
|
|
if (await db.ClinicalUsers.AnyAsync(u => u.Username == username))
|
|
{
|
|
logger.LogInformation(
|
|
"Bootstrap user '{Username}' ({Role}) already exists — skipping.",
|
|
username, role);
|
|
return;
|
|
}
|
|
|
|
if (user.Password.Length < 8)
|
|
{
|
|
logger.LogWarning(
|
|
"Skipping bootstrap {Role} '{Username}': password must be at least 8 characters.",
|
|
role, username);
|
|
return;
|
|
}
|
|
|
|
var displayName = string.IsNullOrWhiteSpace(user.DisplayName)
|
|
? username
|
|
: user.DisplayName.Trim();
|
|
|
|
db.ClinicalUsers.Add(new ClinicalUser
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Username = username,
|
|
PasswordHash = BCrypt.Net.BCrypt.HashPassword(user.Password),
|
|
DisplayName = displayName,
|
|
Role = role,
|
|
IsActive = true,
|
|
CreatedAt = DateTimeOffset.UtcNow,
|
|
});
|
|
await db.SaveChangesAsync();
|
|
|
|
logger.LogInformation(
|
|
"Bootstrap user '{Username}' ({Role}) created.",
|
|
username, role);
|
|
}
|
|
|
|
private static async Task EnsureSiteAndGatewayAsync(
|
|
AppDbContext db,
|
|
SeedingOptions options,
|
|
ILogger logger)
|
|
{
|
|
if (options.SiteId is null || options.SiteId == Guid.Empty
|
|
|| options.GatewayId is null || options.GatewayId == Guid.Empty)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var siteId = options.SiteId.Value;
|
|
var gatewayId = options.GatewayId.Value;
|
|
var siteCode = string.IsNullOrWhiteSpace(options.SiteCode) ? "SITE-01" : options.SiteCode.Trim();
|
|
var siteName = string.IsNullOrWhiteSpace(options.SiteName) ? "Primary Site" : options.SiteName.Trim();
|
|
var gatewayCode = string.IsNullOrWhiteSpace(options.GatewayCode) ? "GW-ICU-1" : options.GatewayCode.Trim();
|
|
var department = string.IsNullOrWhiteSpace(options.GatewayDepartment) ? "ICU" : options.GatewayDepartment.Trim();
|
|
var address = string.IsNullOrWhiteSpace(options.SiteAddress) ? null : options.SiteAddress.Trim();
|
|
|
|
var existingGateway = await db.WardGateways
|
|
.AsNoTracking()
|
|
.FirstOrDefaultAsync(g => g.Id == gatewayId);
|
|
|
|
if (existingGateway is not null)
|
|
{
|
|
if (existingGateway.SiteId != siteId
|
|
|| !string.Equals(existingGateway.GatewayCode, gatewayCode, StringComparison.Ordinal)
|
|
|| !string.Equals(existingGateway.Department, department, StringComparison.Ordinal))
|
|
{
|
|
logger.LogWarning(
|
|
"Gateway id {GatewayId} already exists with different site/code/department — not modifying.",
|
|
gatewayId);
|
|
return;
|
|
}
|
|
|
|
logger.LogInformation(
|
|
"Gateway '{GatewayCode}' (id={GatewayId}) already registered — skipping.",
|
|
existingGateway.GatewayCode, gatewayId);
|
|
return;
|
|
}
|
|
|
|
var existingSite = await db.ClinicalSites.FirstOrDefaultAsync(s => s.Id == siteId);
|
|
if (existingSite is null)
|
|
{
|
|
if (await db.ClinicalSites.AnyAsync(s => s.SiteCode == siteCode))
|
|
{
|
|
logger.LogWarning(
|
|
"Site code '{SiteCode}' exists under a different id — cannot bootstrap site {SiteId}.",
|
|
siteCode, siteId);
|
|
return;
|
|
}
|
|
|
|
var site = new ClinicalSite(siteCode, siteName, address);
|
|
db.Entry(site).Property(nameof(ClinicalSite.Id)).CurrentValue = siteId;
|
|
db.ClinicalSites.Add(site);
|
|
logger.LogInformation("Bootstrap created site '{SiteCode}' (id={SiteId}).", siteCode, siteId);
|
|
}
|
|
else if (!string.Equals(existingSite.SiteCode, siteCode, StringComparison.Ordinal))
|
|
{
|
|
logger.LogWarning(
|
|
"Site id {SiteId} already exists as code '{Existing}', not '{Requested}' — aborting gateway bootstrap.",
|
|
siteId, existingSite.SiteCode, siteCode);
|
|
return;
|
|
}
|
|
|
|
if (await db.WardGateways.AnyAsync(g => g.SiteId == siteId && g.GatewayCode == gatewayCode))
|
|
{
|
|
logger.LogWarning(
|
|
"Gateway code '{GatewayCode}' already exists for site {SiteId} under a different id.",
|
|
gatewayCode, siteId);
|
|
return;
|
|
}
|
|
|
|
var gateway = new WardGateway(siteId, gatewayCode, department);
|
|
db.Entry(gateway).Property(nameof(WardGateway.Id)).CurrentValue = gatewayId;
|
|
db.WardGateways.Add(gateway);
|
|
await db.SaveChangesAsync();
|
|
|
|
logger.LogInformation(
|
|
"Bootstrap registered gateway '{GatewayCode}' (id={GatewayId}) on site {SiteId}.",
|
|
gatewayCode, gatewayId, siteId);
|
|
}
|
|
}
|