feat(infra): configure beta deployment to Test environment with hardened security and feature flags

This commit is contained in:
2026-05-26 19:55:47 +02:00
parent 72905aa119
commit 539ad79f18
10 changed files with 223 additions and 6 deletions
@@ -68,7 +68,8 @@ public static class DbInitializer
SecurityStamp = Guid.NewGuid().ToString()
};
adminUser.PasswordHash = passwordHasher.HashPassword(adminUser, "Admin123!");
var adminPassword = Environment.GetEnvironmentVariable("NEXUS_ADMIN_PASSWORD") ?? "Admin123!";
adminUser.PasswordHash = passwordHasher.HashPassword(adminUser, adminPassword);
dbContext.Users.Add(adminUser);
await dbContext.SaveChangesAsync();
@@ -56,9 +56,14 @@ public static class DependencyInjection
var qdrantUrl = configuration.GetConnectionString("QdrantConnection") ?? "http://localhost:6334";
services.AddSingleton<QdrantClient>(sp => new QdrantClient(new Uri(qdrantUrl)));
// Neo4j Driver registration
// Neo4j Driver registration (supports optional authentication)
var neo4jUrl = configuration.GetConnectionString("Neo4jConnection") ?? "bolt://localhost:7687";
services.AddSingleton<IDriver>(sp => GraphDatabase.Driver(neo4jUrl, AuthTokens.None));
var neo4jUser = configuration["Neo4j:Username"];
var neo4jPass = configuration["Neo4j:Password"];
var neo4jAuth = !string.IsNullOrEmpty(neo4jUser)
? AuthTokens.Basic(neo4jUser, neo4jPass ?? string.Empty)
: AuthTokens.None;
services.AddSingleton<IDriver>(sp => GraphDatabase.Driver(neo4jUrl, neo4jAuth));
// Hangfire registration
if (!string.IsNullOrEmpty(pgConnectionString))
@@ -7,6 +7,7 @@
@inject IIdentityService IdentityService
@inject NavigationManager NavigationManager
@inject IJSRuntime JS
@inject IConfiguration Configuration
<div class="login-page-container">
<div class="mesh-bg"></div>
@@ -80,8 +81,14 @@
</EditForm>
<div class="auth-footer">
<a href="/account/forgot-password" class="auth-link">Zapomniałem hasła?</a>
<p class="auth-switch">Nie masz konta? <a href="/account/register">Zarejestruj się</a></p>
@if (_allowPasswordReset)
{
<a href="/account/forgot-password" class="auth-link">Zapomniałem hasła?</a>
}
@if (_allowRegistration)
{
<p class="auth-switch">Nie masz konta? <a href="/account/register">Zarejestruj się</a></p>
}
</div>
<div class="auth-legal">
@@ -106,9 +113,14 @@
private string? _errorMessage;
private bool _isSubmitting;
private bool _showPassword;
private bool _allowRegistration;
private bool _allowPasswordReset;
protected override void OnInitialized()
{
_allowRegistration = Configuration.GetValue<bool?>("Features:AllowRegistration") ?? true;
_allowPasswordReset = Configuration.GetValue<bool?>("Features:AllowPasswordReset") ?? true;
if (!string.IsNullOrEmpty(ErrorCode))
{
_errorMessage = ErrorCode switch
@@ -118,6 +130,7 @@
"UserAlreadyExists" => "Użytkownik o tym adresie e-mail już istnieje. Zaloguj się tradycyjnie hasłem.",
"LockedOut" => "Twoje konto zostało zablokowane. Spróbuj ponownie później.",
"InvalidCredentials" => "Nieprawidłowy e-mail lub hasło.",
"RegistrationDisabled" => "Rejestracja jest wyłączona w tym środowisku.",
_ => "Wystąpił nieoczekiwany błąd podczas logowania."
};
}
@@ -7,6 +7,7 @@
@inject IIdentityService IdentityService
@inject NavigationManager NavigationManager
@inject IJSRuntime JS
@inject IConfiguration Configuration
<div class="login-page-container">
<div class="mesh-bg"></div>
@@ -81,6 +82,15 @@
private string? _errorMessage;
private bool _isSubmitting;
protected override void OnInitialized()
{
var allowRegistration = Configuration.GetValue<bool?>("Features:AllowRegistration") ?? true;
if (!allowRegistration)
{
NavigationManager.NavigateTo("/account/login?error=RegistrationDisabled", replace: true);
}
}
private async Task HandleRegister()
{
_isSubmitting = true;
+1
View File
@@ -16,6 +16,7 @@
@using NexusReader.UI.Shared.Components.Organisms
@using NexusReader.UI.Shared.Services
@using Microsoft.Extensions.Logging
@using Microsoft.Extensions.Configuration
@using NexusReader.Application.Abstractions.Services
@using NexusReader.Application.DTOs.User
@using NexusReader.Application.Queries.Reader
+36 -1
View File
@@ -252,6 +252,35 @@ app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery();
// Feature flags: block registration & password reset in restricted environments (e.g. Test)
var allowRegistration = app.Configuration.GetValue<bool?>("Features:AllowRegistration") ?? true;
var allowPasswordReset = app.Configuration.GetValue<bool?>("Features:AllowPasswordReset") ?? true;
if (!allowRegistration || !allowPasswordReset)
{
app.Use(async (context, next) =>
{
var path = context.Request.Path.Value?.ToLowerInvariant();
if (!allowRegistration && path is "/identity/register" or "/account/register")
{
context.Response.StatusCode = StatusCodes.Status403Forbidden;
await context.Response.WriteAsync("Registration is disabled in this environment.");
return;
}
if (!allowPasswordReset && path is "/identity/forgotpassword" or "/account/forgot-password")
{
context.Response.StatusCode = StatusCodes.Status403Forbidden;
await context.Response.WriteAsync("Password reset is disabled in this environment.");
return;
}
await next();
});
}
app.MapStaticAssets();
app.MapHub<NexusReader.Infrastructure.RealTime.SyncHub>("/synchub");
@@ -520,7 +549,13 @@ app.MapGet("/identity/callback/google", async (
return Results.Redirect("/account/login?error=LockedOut");
}
// New user provisioning
// New user provisioning (blocked when registration is disabled)
if (!allowRegistration)
{
logger.LogWarning("Google provisioning blocked: registration is disabled in this environment.");
return Results.Redirect("/account/login?error=RegistrationDisabled");
}
var email = info.Principal.FindFirstValue(ClaimTypes.Email);
if (email != null)
{
+13
View File
@@ -0,0 +1,13 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"Features": {
"AllowRegistration": false,
"AllowPasswordReset": false
},
"ApiBaseUrl": "http://localhost:5000"
}