using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using StackExchange.Redis; /// /// 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. /// 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); } }