Add deployment
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
/// <summary>
|
||||
/// One-shot bootstrap: creates the first Admin user when the ClinicalUsers
|
||||
/// table is empty. Refuses to run if any user already exists so it cannot
|
||||
/// be used as a backdoor after the system is in use.
|
||||
///
|
||||
/// Usage:
|
||||
/// dotnet VigilCareClinicalAPI.dll create-admin --username <u> --password <p> --display-name "<n>"
|
||||
/// </summary>
|
||||
public static class CreateAdminCommand
|
||||
{
|
||||
public static async Task<int> RunAsync(IServiceProvider services, string[] args)
|
||||
{
|
||||
var username = RequireArg(args, "--username");
|
||||
var password = RequireArg(args, "--password");
|
||||
var displayName = RequireArg(args, "--display-name");
|
||||
|
||||
if (username.Length > 100)
|
||||
{
|
||||
Console.Error.WriteLine("Username must be 100 characters or fewer.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (password.Length < 8)
|
||||
{
|
||||
Console.Error.WriteLine("Password must be at least 8 characters.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (displayName.Length > 200)
|
||||
{
|
||||
Console.Error.WriteLine("Display name must be 200 characters or fewer.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
using var scope = services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
|
||||
if (await db.ClinicalUsers.AnyAsync())
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"Refusing to create admin: ClinicalUsers already contains at least one row. " +
|
||||
"create-admin is a one-shot bootstrap and cannot be used as a backdoor.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var user = new ClinicalUser
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Username = username.Trim(),
|
||||
PasswordHash = BCrypt.Net.BCrypt.HashPassword(password),
|
||||
DisplayName = displayName.Trim(),
|
||||
Role = ClinicalRole.Admin,
|
||||
IsActive = true,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
|
||||
db.ClinicalUsers.Add(user);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine($"Created admin user '{user.Username}' (id={user.Id}).");
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static string RequireArg(string[] args, string name)
|
||||
{
|
||||
for (var i = 0; i < args.Length - 1; i++)
|
||||
{
|
||||
if (string.Equals(args[i], name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var value = args[i + 1];
|
||||
if (string.IsNullOrWhiteSpace(value) || value.StartsWith("--", StringComparison.Ordinal))
|
||||
break;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
throw new ArgumentException($"Missing required argument: {name}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
/// <summary>
|
||||
/// One-shot / idempotent registration of a clinical site + ward gateway with
|
||||
/// caller-supplied IDs. Production cannot rely on GatewayRegistrySeeder (demo
|
||||
/// data is gated off); the gateway authenticates using Gateway:GatewayId /
|
||||
/// Gateway:SiteId from its environment, so those GUIDs must exist in the API DB.
|
||||
///
|
||||
/// Usage:
|
||||
/// dotnet VigilCareClinicalAPI.dll register-gateway \
|
||||
/// --site-id <guid> --gateway-id <guid> \
|
||||
/// --site-code SITE-01 --site-name "General Hospital" \
|
||||
/// --gateway-code GW-ICU-1 --department ICU \
|
||||
/// [--address "123 Main St"]
|
||||
///
|
||||
/// Re-running with the same IDs is a no-op success. Conflicting codes or IDs fail.
|
||||
/// </summary>
|
||||
public static class RegisterGatewayCommand
|
||||
{
|
||||
public static async Task<int> RunAsync(IServiceProvider services, string[] args)
|
||||
{
|
||||
var siteId = RequireGuid(args, "--site-id");
|
||||
var gatewayId = RequireGuid(args, "--gateway-id");
|
||||
var siteCode = RequireArg(args, "--site-code").Trim();
|
||||
var siteName = RequireArg(args, "--site-name").Trim();
|
||||
var gatewayCode = RequireArg(args, "--gateway-code").Trim();
|
||||
var department = RequireArg(args, "--department").Trim();
|
||||
var address = GetArg(args, "--address")?.Trim();
|
||||
|
||||
using var scope = services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
|
||||
var existingGateway = await db.WardGateways
|
||||
.Include(g => g.Site)
|
||||
.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))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"Gateway id {gatewayId} already exists with different site/code/department. Aborting.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
Console.WriteLine(
|
||||
$"Gateway '{existingGateway.GatewayCode}' (id={gatewayId}) already registered — nothing to do.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
var existingSite = await db.ClinicalSites.FirstOrDefaultAsync(s => s.Id == siteId);
|
||||
if (existingSite is null)
|
||||
{
|
||||
var codeTaken = await db.ClinicalSites.AnyAsync(s => s.SiteCode == siteCode);
|
||||
if (codeTaken)
|
||||
{
|
||||
Console.Error.WriteLine($"Site code '{siteCode}' is already registered under a different id.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var site = new ClinicalSite(siteCode, siteName, address);
|
||||
db.Entry(site).Property(nameof(ClinicalSite.Id)).CurrentValue = siteId;
|
||||
db.ClinicalSites.Add(site);
|
||||
Console.WriteLine($"Created site '{siteCode}' (id={siteId}).");
|
||||
}
|
||||
else if (!string.Equals(existingSite.SiteCode, siteCode, StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"Site id {siteId} already exists as code '{existingSite.SiteCode}', " +
|
||||
$"not '{siteCode}'. Aborting.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var duplicateCode = await db.WardGateways.AnyAsync(g =>
|
||||
g.SiteId == siteId && g.GatewayCode == gatewayCode);
|
||||
if (duplicateCode)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"Gateway code '{gatewayCode}' already exists for site {siteId} under a different id.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var gateway = new WardGateway(siteId, gatewayCode, department);
|
||||
db.Entry(gateway).Property(nameof(WardGateway.Id)).CurrentValue = gatewayId;
|
||||
db.WardGateways.Add(gateway);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine($"Registered gateway '{gatewayCode}' (id={gatewayId}) on site {siteId}.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static Guid RequireGuid(string[] args, string name)
|
||||
{
|
||||
var raw = RequireArg(args, name);
|
||||
if (!Guid.TryParse(raw, out var id) || id == Guid.Empty)
|
||||
throw new ArgumentException($"{name} must be a non-empty GUID.");
|
||||
return id;
|
||||
}
|
||||
|
||||
private static string RequireArg(string[] args, string name)
|
||||
{
|
||||
var value = GetArg(args, name);
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
throw new ArgumentException($"Missing required argument: {name}");
|
||||
return value;
|
||||
}
|
||||
|
||||
private static string? GetArg(string[] args, string name)
|
||||
{
|
||||
for (var i = 0; i < args.Length - 1; i++)
|
||||
{
|
||||
if (string.Equals(args[i], name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var value = args[i + 1];
|
||||
if (string.IsNullOrWhiteSpace(value) || value.StartsWith("--", StringComparison.Ordinal))
|
||||
return null;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user