diff --git a/docker-compose.stage.yml b/docker-compose.stage.yml index 66b9864..03bcec1 100644 --- a/docker-compose.stage.yml +++ b/docker-compose.stage.yml @@ -30,6 +30,7 @@ services: - ASPNETCORE_ENVIRONMENT=Staging - ConnectionStrings__PostgresConnection=Host=db;Database=${POSTGRES_DB:-nexus_stage_db};Username=${POSTGRES_USER:-nexus_user_stage};Password=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required} - ConnectionStrings__QdrantConnection=http://qdrant:6334 + - Qdrant__ApiKey=${QDRANT_API_KEY:-} - ConnectionStrings__Neo4jConnection=bolt://neo4j:7687 - Neo4j__Username=${NEO4J_USERNAME:-neo4j} - Neo4j__Password=${NEO4J_PASSWORD:?NEO4J_PASSWORD is required} diff --git a/run-stage.sh b/run-stage.sh index db787f5..f923401 100755 --- a/run-stage.sh +++ b/run-stage.sh @@ -37,6 +37,12 @@ if grep -q "CHANGE_ME_TO_SECURE_ADMIN_PASSWORD" "$ENV_FILE"; then sed -i "s/NEXUS_ADMIN_PASSWORD=CHANGE_ME_TO_SECURE_ADMIN_PASSWORD/NEXUS_ADMIN_PASSWORD=$ADMIN_PASS/g" "$ENV_FILE" fi +if grep -q "^QDRANT_API_KEY=$" "$ENV_FILE" || grep -q "^QDRANT_API_KEY=[[:space:]]*$" "$ENV_FILE"; then + echo "🔐 Generating secure random Qdrant API key in $ENV_FILE..." + QD_KEY=$(openssl rand -hex 16) + sed -i "s/^QDRANT_API_KEY=.*/QDRANT_API_KEY=$QD_KEY/g" "$ENV_FILE" +fi + # Load staging variables for local execution context (needed for ports/migrations) # Clean up carriage returns just in case POSTGRES_USER=$(grep "^POSTGRES_USER=" "$ENV_FILE" | cut -d'=' -f2- | tr -d '\r') diff --git a/src/NexusReader.Infrastructure/DependencyInjection.cs b/src/NexusReader.Infrastructure/DependencyInjection.cs index 63cdc37..aa44423 100644 --- a/src/NexusReader.Infrastructure/DependencyInjection.cs +++ b/src/NexusReader.Infrastructure/DependencyInjection.cs @@ -55,7 +55,15 @@ public static class DependencyInjection // Qdrant Client registration var qdrantUrl = configuration.GetConnectionString("QdrantConnection") ?? "http://localhost:6334"; - services.AddSingleton(sp => new QdrantClient(new Uri(qdrantUrl))); + var qdrantApiKey = configuration["Qdrant:ApiKey"]; + services.AddSingleton(sp => + { + if (!string.IsNullOrEmpty(qdrantApiKey)) + { + return new QdrantClient(new Uri(qdrantUrl), apiKey: qdrantApiKey); + } + return new QdrantClient(new Uri(qdrantUrl)); + }); // Neo4j Driver registration (supports optional authentication) var neo4jUrl = configuration.GetConnectionString("Neo4jConnection") ?? "bolt://localhost:7687"; diff --git a/src/NexusReader.Web/Program.cs b/src/NexusReader.Web/Program.cs index 8af4308..28bf8a7 100644 --- a/src/NexusReader.Web/Program.cs +++ b/src/NexusReader.Web/Program.cs @@ -91,6 +91,10 @@ builder.Services.AddCascadingAuthenticationState(); builder.Services.AddApplication(); builder.Services.AddInfrastructure(builder.Configuration); +builder.Services.AddHealthChecks() + .AddCheck("Database") + .AddCheck("Qdrant") + .AddCheck("Neo4j"); builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies( NexusReader.Application.DependencyInjection.Assembly, @@ -295,6 +299,7 @@ if (!allowRegistration || !allowPasswordReset) } app.MapStaticAssets(); +app.MapHealthChecks("/health"); app.MapHub("/synchub"); // API endpoint for WASM client to fetch EPUB content diff --git a/src/NexusReader.Web/Services/DatabaseHealthCheck.cs b/src/NexusReader.Web/Services/DatabaseHealthCheck.cs new file mode 100644 index 0000000..a7df82e --- /dev/null +++ b/src/NexusReader.Web/Services/DatabaseHealthCheck.cs @@ -0,0 +1,35 @@ +using Microsoft.Extensions.Diagnostics.HealthChecks; +using NexusReader.Data.Persistence; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace NexusReader.Web.Services; + +public class DatabaseHealthCheck : IHealthCheck +{ + private readonly AppDbContext _dbContext; + + public DatabaseHealthCheck(AppDbContext dbContext) + { + _dbContext = dbContext; + } + + public async Task CheckHealthAsync( + HealthCheckContext context, CancellationToken cancellationToken = default) + { + try + { + var canConnect = await _dbContext.Database.CanConnectAsync(cancellationToken); + if (canConnect) + { + return HealthCheckResult.Healthy("Database is accessible."); + } + return HealthCheckResult.Unhealthy("Cannot connect to the database."); + } + catch (Exception ex) + { + return HealthCheckResult.Unhealthy("Database health check failed with exception.", ex); + } + } +} diff --git a/src/NexusReader.Web/Services/Neo4jHealthCheck.cs b/src/NexusReader.Web/Services/Neo4jHealthCheck.cs new file mode 100644 index 0000000..5412c18 --- /dev/null +++ b/src/NexusReader.Web/Services/Neo4jHealthCheck.cs @@ -0,0 +1,31 @@ +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Neo4j.Driver; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace NexusReader.Web.Services; + +public class Neo4jHealthCheck : IHealthCheck +{ + private readonly IDriver _driver; + + public Neo4jHealthCheck(IDriver driver) + { + _driver = driver; + } + + public async Task CheckHealthAsync( + HealthCheckContext context, CancellationToken cancellationToken = default) + { + try + { + await _driver.VerifyConnectivityAsync(); + return HealthCheckResult.Healthy("Neo4j database is accessible."); + } + catch (Exception ex) + { + return HealthCheckResult.Unhealthy("Neo4j database connectivity check failed.", ex); + } + } +} diff --git a/src/NexusReader.Web/Services/QdrantHealthCheck.cs b/src/NexusReader.Web/Services/QdrantHealthCheck.cs new file mode 100644 index 0000000..76ee08d --- /dev/null +++ b/src/NexusReader.Web/Services/QdrantHealthCheck.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Qdrant.Client; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace NexusReader.Web.Services; + +public class QdrantHealthCheck : IHealthCheck +{ + private readonly QdrantClient _qdrantClient; + + public QdrantHealthCheck(QdrantClient qdrantClient) + { + _qdrantClient = qdrantClient; + } + + public async Task CheckHealthAsync( + HealthCheckContext context, CancellationToken cancellationToken = default) + { + try + { + // Simple check: query collection existence to verify connection is alive + _ = await _qdrantClient.CollectionExistsAsync("knowledge_units", cancellationToken); + return HealthCheckResult.Healthy("Qdrant database is accessible."); + } + catch (Exception ex) + { + return HealthCheckResult.Unhealthy("Qdrant database health check failed.", ex); + } + } +}