Add deployment
This commit is contained in:
@@ -39,7 +39,7 @@ public class EsIndexerService : BackgroundService
|
||||
// Consumers must be idempotent. See idempotency contract above.
|
||||
EnableAutoCommit = false,
|
||||
EnablePartitionEof = false
|
||||
};
|
||||
}.ApplySecurity(_kafkaOptions);
|
||||
|
||||
using var consumer = new ConsumerBuilder<string, string>(config).Build();
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ public class GcsScoringService : BackgroundService
|
||||
GroupId = "gcs-scoring",
|
||||
AutoOffsetReset = AutoOffsetReset.Earliest,
|
||||
EnableAutoCommit = false
|
||||
};
|
||||
}.ApplySecurity(_kafkaOptions);
|
||||
|
||||
using var consumer = new ConsumerBuilder<string, string>(config).Build();
|
||||
consumer.Subscribe(_kafkaOptions.Topics.ObservationRecorded);
|
||||
|
||||
@@ -18,7 +18,7 @@ public class KafkaTopicProvisioner : IHostedService
|
||||
using var admin = new AdminClientBuilder(new AdminClientConfig
|
||||
{
|
||||
BootstrapServers = _options.BootstrapServers
|
||||
}).Build();
|
||||
}.ApplySecurity(_options)).Build();
|
||||
|
||||
var topicNames = new[]
|
||||
{
|
||||
|
||||
@@ -48,7 +48,8 @@ public sealed class KafkaConsumerLagCollector : BackgroundService
|
||||
private async Task CollectGroupLagAsync(string groupId, CancellationToken ct)
|
||||
{
|
||||
var adminConfig = new AdminClientConfig
|
||||
{ BootstrapServers = _kafkaOptions.BootstrapServers };
|
||||
{ BootstrapServers = _kafkaOptions.BootstrapServers }
|
||||
.ApplySecurity(_kafkaOptions);
|
||||
|
||||
using var admin = new AdminClientBuilder(adminConfig).Build();
|
||||
|
||||
@@ -69,7 +70,7 @@ public sealed class KafkaConsumerLagCollector : BackgroundService
|
||||
{
|
||||
BootstrapServers = _kafkaOptions.BootstrapServers,
|
||||
GroupId = $"__lag-probe",
|
||||
}).Build();
|
||||
}.ApplySecurity(_kafkaOptions)).Build();
|
||||
|
||||
long totalLag = 0;
|
||||
foreach (var tpo in partitions)
|
||||
|
||||
@@ -26,7 +26,7 @@ public class News2ScoringService : BackgroundService
|
||||
GroupId = "news2-scoring",
|
||||
AutoOffsetReset = AutoOffsetReset.Earliest,
|
||||
EnableAutoCommit = false
|
||||
};
|
||||
}.ApplySecurity(_kafkaOptions);
|
||||
|
||||
using var consumer = new ConsumerBuilder<string, string>(config).Build();
|
||||
consumer.Subscribe(_kafkaOptions.Topics.ObservationRecorded);
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ public sealed class NotificationPublisherService : BackgroundService
|
||||
AutoOffsetReset = Enum.Parse<AutoOffsetReset>(
|
||||
_kafkaOptions.NotificationPublisherAutoOffsetReset, ignoreCase: true),
|
||||
EnableAutoCommit = false,
|
||||
};
|
||||
}.ApplySecurity(_kafkaOptions);
|
||||
|
||||
using var consumer = new ConsumerBuilder<string, string>(consumerConfig).Build();
|
||||
consumer.Subscribe(new[]
|
||||
|
||||
@@ -43,7 +43,7 @@ public class OutboxRelayService : BackgroundService
|
||||
EnableIdempotence = true,
|
||||
MessageSendMaxRetries = 3,
|
||||
RetryBackoffMs = 100
|
||||
}).Build();
|
||||
}.ApplySecurity(_options)).Build();
|
||||
|
||||
var factory = RabbitMqConnectionFactory.Create(_rabbitOpts);
|
||||
_rabbitConnection = factory.CreateConnection("outbox-relay");
|
||||
|
||||
@@ -31,7 +31,7 @@ public class SepsisEngineService : BackgroundService
|
||||
GroupId = "sepsis-engine",
|
||||
AutoOffsetReset = AutoOffsetReset.Earliest,
|
||||
EnableAutoCommit = false
|
||||
};
|
||||
}.ApplySecurity(_kafkaOptions);
|
||||
|
||||
using var consumer = new ConsumerBuilder<string, string>(config).Build();
|
||||
consumer.Subscribe(_kafkaOptions.Topics.ObservationRecorded);
|
||||
|
||||
@@ -26,7 +26,7 @@ public class SofaScoringService : BackgroundService
|
||||
GroupId = "sofa-scoring",
|
||||
AutoOffsetReset = AutoOffsetReset.Earliest,
|
||||
EnableAutoCommit = false
|
||||
};
|
||||
}.ApplySecurity(_kafkaOptions);
|
||||
|
||||
using var consumer = new ConsumerBuilder<string, string>(config).Build();
|
||||
consumer.Subscribe(new[]
|
||||
|
||||
@@ -26,7 +26,7 @@ public class TrendAnalyzerService : BackgroundService
|
||||
GroupId = "trend-analyzer",
|
||||
AutoOffsetReset = AutoOffsetReset.Earliest,
|
||||
EnableAutoCommit = false
|
||||
};
|
||||
}.ApplySecurity(_kafkaOptions);
|
||||
|
||||
using var consumer = new ConsumerBuilder<string, string>(config).Build();
|
||||
consumer.Subscribe(_kafkaOptions.Topics.ObservationRecorded);
|
||||
|
||||
@@ -26,7 +26,7 @@ public class WarningAlertService : BackgroundService
|
||||
GroupId = "warning-evaluator",
|
||||
AutoOffsetReset = AutoOffsetReset.Earliest,
|
||||
EnableAutoCommit = false
|
||||
};
|
||||
}.ApplySecurity(_kafkaOptions);
|
||||
|
||||
using var consumer = new ConsumerBuilder<string, string>(config).Build();
|
||||
consumer.Subscribe(_kafkaOptions.Topics.ObservationRecorded);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -11,4 +11,11 @@ public class KafkaOptions
|
||||
public int MaxPoisonRetries { get; set; } = 5;
|
||||
public string NotificationPublisherGroupId { get; set; } = "notification-publisher";
|
||||
public string NotificationPublisherAutoOffsetReset { get; set; } = "Earliest";
|
||||
|
||||
/// <summary>Plaintext for local compose; SaslSsl (etc.) for production.</summary>
|
||||
public string SecurityProtocol { get; set; } = "Plaintext";
|
||||
public string? SaslMechanism { get; set; }
|
||||
public string? SaslUsername { get; set; }
|
||||
public string? SaslPassword { get; set; }
|
||||
public string? SslCaLocation { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using Confluent.Kafka;
|
||||
|
||||
public static class KafkaSecurityExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies the configured security protocol and SASL credentials to any
|
||||
/// Confluent client config. Called by every producer, consumer, and admin
|
||||
/// client so credentials are configured in exactly one place.
|
||||
/// </summary>
|
||||
public static T ApplySecurity<T>(this T config, KafkaOptions options)
|
||||
where T : ClientConfig
|
||||
{
|
||||
if (Enum.TryParse<SecurityProtocol>(options.SecurityProtocol, true, out var protocol))
|
||||
config.SecurityProtocol = protocol;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(options.SaslMechanism)
|
||||
&& Enum.TryParse<SaslMechanism>(options.SaslMechanism, true, out var mechanism))
|
||||
{
|
||||
config.SaslMechanism = mechanism;
|
||||
config.SaslUsername = options.SaslUsername;
|
||||
config.SaslPassword = options.SaslPassword;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(options.SslCaLocation))
|
||||
config.SslCaLocation = options.SslCaLocation;
|
||||
|
||||
return config;
|
||||
}
|
||||
}
|
||||
@@ -9,4 +9,10 @@ public sealed class RabbitMqOptions
|
||||
// Drives both the paging worker poll timeout and the DLQ x-message-ttl.
|
||||
// In production: 300000 (5 min). In tests: 5000 (5 sec).
|
||||
public int PagingAckTimeoutMs { get; init; } = 300000;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When true, enables TLS on the AMQP connection (typical production port 5671).
|
||||
/// Local compose uses plaintext on 5674/5672 — leave false.
|
||||
/// </summary>
|
||||
public bool UseSsl { get; init; } = false;
|
||||
}
|
||||
|
||||
@@ -3,4 +3,12 @@ public class ElasticsearchOptions
|
||||
public const string Section = "Elasticsearch";
|
||||
public string Uri { get; set; } = null!;
|
||||
public ElasticIndexOptions Indices { get; set; } = null!;
|
||||
}
|
||||
|
||||
// Production clusters run with xpack.security enabled. Supply either an
|
||||
// API key (preferred) or basic credentials; leave all null for the
|
||||
// security-disabled development cluster.
|
||||
public string? ApiKey { get; set; }
|
||||
public string? Username { get; set; }
|
||||
public string? Password { get; set; }
|
||||
public bool DisableCertificateValidation { get; set; }
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ public sealed class DataLakeWriterService : BackgroundService
|
||||
GroupId = "data-lake-writer",
|
||||
AutoOffsetReset = AutoOffsetReset.Earliest,
|
||||
EnableAutoCommit = false,
|
||||
};
|
||||
}.ApplySecurity(_kafkaOptions);
|
||||
|
||||
using var consumer = new ConsumerBuilder<string, string>(consumerConfig).Build();
|
||||
consumer.Subscribe(new[]
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
WORKDIR /src
|
||||
|
||||
# Restore layer — copy only project files so NuGet restore is cached
|
||||
# independently of source changes. ClinicalContracts must be present
|
||||
# because VigilCareClinicalAPI.csproj ProjectReferences it.
|
||||
COPY VigilCareClinical.sln ./
|
||||
COPY VigilCare.ClinicalContracts/VigilCare.ClinicalContracts.csproj VigilCare.ClinicalContracts/
|
||||
COPY VigilCareClinicalAPI/VigilCareClinicalAPI.csproj VigilCareClinicalAPI/
|
||||
RUN dotnet restore VigilCareClinicalAPI/VigilCareClinicalAPI.csproj
|
||||
|
||||
COPY VigilCare.ClinicalContracts/ VigilCare.ClinicalContracts/
|
||||
COPY VigilCareClinicalAPI/ VigilCareClinicalAPI/
|
||||
RUN dotnet publish VigilCareClinicalAPI/VigilCareClinicalAPI.csproj \
|
||||
-c Release -o /app/publish --no-restore
|
||||
|
||||
# Scrub development secrets from the published appsettings.json (Step 4).
|
||||
# Production supplies Jwt / PHI / API keys via environment variables only.
|
||||
RUN sed -i \
|
||||
-e 's/"SigningKey": "[^"]*"/"SigningKey": ""/' \
|
||||
-e 's/"SearchTokenKey": "[^"]*"/"SearchTokenKey": ""/' \
|
||||
-e 's/"Gateway": "dev-[^"]*"/"Gateway": ""/' \
|
||||
-e 's/"ApiKey": "dev-integration[^"]*"/"ApiKey": ""/' \
|
||||
/app/publish/appsettings.json
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
|
||||
WORKDIR /app
|
||||
|
||||
# curl is required by the container healthcheck; the aspnet image does not ship it.
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=build /app/publish .
|
||||
|
||||
# The keyring directory must be owned by the runtime user — see Step 10.
|
||||
# The aspnet:8.0 image ships a non-root `app` user (uid 1654).
|
||||
RUN mkdir -p /app/data-protection-keys && chown -R app:app /app
|
||||
USER app
|
||||
|
||||
ENV ASPNETCORE_URLS=http://+:8080
|
||||
EXPOSE 8080
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \
|
||||
CMD curl -fsS http://localhost:8080/health/live || exit 1
|
||||
|
||||
ENTRYPOINT ["dotnet", "VigilCareClinicalAPI.dll"]
|
||||
@@ -14,7 +14,7 @@ public sealed class KafkaHealthCheck : IHealthCheck
|
||||
using var admin = new AdminClientBuilder(new AdminClientConfig
|
||||
{
|
||||
BootstrapServers = _options.BootstrapServers
|
||||
}).Build();
|
||||
}.ApplySecurity(_options)).Build();
|
||||
|
||||
var metadata = await Task.Run(
|
||||
() => admin.GetMetadata(TimeSpan.FromSeconds(5)), cancellationToken);
|
||||
|
||||
@@ -2,13 +2,27 @@ using RabbitMQ.Client;
|
||||
|
||||
public static class RabbitMqConnectionFactory
|
||||
{
|
||||
public static ConnectionFactory Create(RabbitMqOptions o, bool dispatchConsumersAsync = false) => new()
|
||||
public static ConnectionFactory Create(RabbitMqOptions o, bool dispatchConsumersAsync = false)
|
||||
{
|
||||
HostName = o.Host,
|
||||
Port = o.Port,
|
||||
UserName = o.Username,
|
||||
Password = o.Password,
|
||||
VirtualHost = o.VirtualHost,
|
||||
DispatchConsumersAsync = dispatchConsumersAsync,
|
||||
};
|
||||
var factory = new ConnectionFactory
|
||||
{
|
||||
HostName = o.Host,
|
||||
Port = o.Port,
|
||||
UserName = o.Username,
|
||||
Password = o.Password,
|
||||
VirtualHost = o.VirtualHost,
|
||||
DispatchConsumersAsync = dispatchConsumersAsync,
|
||||
};
|
||||
|
||||
if (o.UseSsl)
|
||||
{
|
||||
factory.Ssl = new SslOption
|
||||
{
|
||||
Enabled = true,
|
||||
ServerName = o.Host,
|
||||
};
|
||||
}
|
||||
|
||||
return factory;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,8 +94,21 @@ try
|
||||
.GetSection(ElasticsearchOptions.Section)
|
||||
.Get<ElasticsearchOptions>()!;
|
||||
|
||||
builder.Services.AddSingleton(
|
||||
new ElasticsearchClient(new Uri(esOptions.Uri)));
|
||||
builder.Services.AddSingleton(_ =>
|
||||
{
|
||||
var settings = new ElasticsearchClientSettings(new Uri(esOptions.Uri));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(esOptions.ApiKey))
|
||||
settings = settings.Authentication(new Elastic.Transport.ApiKey(esOptions.ApiKey));
|
||||
else if (!string.IsNullOrWhiteSpace(esOptions.Username))
|
||||
settings = settings.Authentication(
|
||||
new Elastic.Transport.BasicAuthentication(esOptions.Username, esOptions.Password ?? ""));
|
||||
|
||||
if (esOptions.DisableCertificateValidation)
|
||||
settings = settings.ServerCertificateValidationCallback((_, _, _, _) => true);
|
||||
|
||||
return new ElasticsearchClient(settings);
|
||||
});
|
||||
|
||||
builder.Services.Configure<RabbitMqOptions>(
|
||||
builder.Configuration.GetSection(RabbitMqOptions.Section));
|
||||
@@ -326,7 +339,21 @@ try
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
if (!app.Environment.IsEnvironment("Testing"))
|
||||
// One-shot CLI verbs exit before hosting. Skip demo seeding for them so
|
||||
// create-admin / register-gateway never race the demo UserSeeder.
|
||||
var isCliCommand = args.Contains("encrypt-phi")
|
||||
|| args.Contains("create-admin")
|
||||
|| args.Contains("register-gateway");
|
||||
|
||||
// Demo data — including the seeded demo users with well-known passwords —
|
||||
// must never be created in production. Seeding:EnableDemoData defaults to
|
||||
// true so local development and the existing verification scripts are
|
||||
// unaffected; appsettings.Production.json sets it to false.
|
||||
var enableDemoData = builder.Configuration.GetValue("Seeding:EnableDemoData", true)
|
||||
&& !app.Environment.IsEnvironment("Testing")
|
||||
&& !isCliCommand;
|
||||
|
||||
if (enableDemoData)
|
||||
{
|
||||
using var scope = app.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
@@ -345,11 +372,14 @@ try
|
||||
});
|
||||
}
|
||||
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI(options =>
|
||||
if (builder.Configuration.GetValue("Swagger:Enabled", !app.Environment.IsProduction()))
|
||||
{
|
||||
options.SwaggerEndpoint("/swagger/v1/swagger.json", "VigilCare Clinical API v1");
|
||||
});
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI(options =>
|
||||
{
|
||||
options.SwaggerEndpoint("/swagger/v1/swagger.json", "VigilCare Clinical API v1");
|
||||
});
|
||||
}
|
||||
|
||||
app.UseMiddleware<CorrelationIdMiddleware>();
|
||||
app.UseMiddleware<FhirApiKeyOrJwtMiddleware>();
|
||||
@@ -372,7 +402,7 @@ try
|
||||
ResponseWriter = HealthCheckResponseWriter.WriteAsync
|
||||
}).AllowAnonymous();
|
||||
|
||||
app.MapMetrics("/metrics");
|
||||
app.MapMetrics("/metrics").AllowAnonymous();
|
||||
app.MapControllers();
|
||||
|
||||
if (args.Contains("encrypt-phi"))
|
||||
@@ -380,7 +410,35 @@ try
|
||||
await EncryptPhiCommand.RunAsync(app.Services);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (args.Contains("create-admin"))
|
||||
{
|
||||
try
|
||||
{
|
||||
Environment.ExitCode = await CreateAdminCommand.RunAsync(app.Services, args);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
Console.Error.WriteLine(ex.Message);
|
||||
Environment.ExitCode = 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Contains("register-gateway"))
|
||||
{
|
||||
try
|
||||
{
|
||||
Environment.ExitCode = await RegisterGatewayCommand.RunAsync(app.Services, args);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
Console.Error.WriteLine(ex.Message);
|
||||
Environment.ExitCode = 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
app.Run();
|
||||
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
|
||||
<PackageReference Include="Serilog.Enrichers.Environment" Version="2.3.0" />
|
||||
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
|
||||
<PackageReference Include="Serilog.Formatting.Compact" Version="3.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
|
||||
<PackageReference Include="Serilog.Sinks.Seq" Version="9.1.0" />
|
||||
<PackageReference Include="StackExchange.Redis" Version="3.0.0" />
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"Serilog": {
|
||||
"Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.Seq" ],
|
||||
"MinimumLevel": {
|
||||
"Default": "Information",
|
||||
"Override": {
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Microsoft.EntityFrameworkCore.Database.Command": "Warning"
|
||||
}
|
||||
},
|
||||
"WriteTo": [
|
||||
{ "Name": "Console", "Args": { "formatter": "Serilog.Formatting.Compact.CompactJsonFormatter, Serilog.Formatting.Compact" } },
|
||||
{ "Name": "Seq", "Args": { "serverUrl": "http://localhost:5341" } }
|
||||
],
|
||||
"Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ],
|
||||
"Properties": {
|
||||
"Application": "VigilCareClinicalAPI"
|
||||
}
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Microsoft.EntityFrameworkCore.Database.Command": "Warning"
|
||||
}
|
||||
},
|
||||
"Kafka": {
|
||||
"ReplicationFactor": 3,
|
||||
"NumPartitions": 6,
|
||||
"SecurityProtocol": "SaslSsl"
|
||||
},
|
||||
"Minio": {
|
||||
"UseSSL": true
|
||||
},
|
||||
"RabbitMq": {
|
||||
"UseSsl": true
|
||||
},
|
||||
"PhiEncryption": {
|
||||
"LogListAccess": true
|
||||
},
|
||||
"Swagger": { "Enabled": false },
|
||||
"Seeding": { "EnableDemoData": false }
|
||||
}
|
||||
@@ -48,6 +48,8 @@
|
||||
"GcsScored": "gcs.scored"
|
||||
},
|
||||
"NumPartitions": 6,
|
||||
"ReplicationFactor": 1,
|
||||
"SecurityProtocol": "Plaintext",
|
||||
"OutboxBatchSize": 100,
|
||||
"OutboxPollIntervalMs": 1000
|
||||
},
|
||||
@@ -64,7 +66,8 @@
|
||||
"Port": 5674,
|
||||
"Username": "guest",
|
||||
"Password": "guest",
|
||||
"PagingAckTimeoutMs": 300000
|
||||
"PagingAckTimeoutMs": 300000,
|
||||
"UseSsl": false
|
||||
},
|
||||
"Minio": {
|
||||
"Endpoint": "localhost:9005",
|
||||
@@ -219,5 +222,11 @@
|
||||
"AlertQuality": {
|
||||
"IntervalMinutes": 60,
|
||||
"WindowHours": 1
|
||||
},
|
||||
"Swagger": {
|
||||
"Enabled": true
|
||||
},
|
||||
"Seeding": {
|
||||
"EnableDemoData": true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user