feat(infra): Docker-compose configuration and environment-specific security guards for Beta deployment to Test environment (#56)
This pull request introduces the dedicated containerized infrastructure and configuration for deploying NexusReader's beta version in the Test environment. ### Summary of Changes 1. **Docker Infrastructure & Secrets**: - **`docker-compose.test.yml`**: Configured dedicated database and auxiliary services (PostgreSQL 17, Qdrant, Neo4j) on isolated, non-standard ports to ensure zero conflict with the existing server configurations. - **`.env.test.template`**: Provided an environment variable template showing required setups, including mandatory database passwords, API keys, and admin custom passwords. - **`.gitignore`**: Excluded local `.env` files to prevent accidental commits of production or staging secrets. 2. **Database Hardening**: - Configured Neo4j with basic authentication (`IDriver` instantiation uses basic auth when credentials are provided in configuration). - Configured PostgreSQL to use mandatory authentication. - Configured the admin seeder (`DbInitializer.cs`) to dynamically use `NEXUS_ADMIN_PASSWORD` from environment variables, falling back to a default password in local Development only. 3. **Feature-Flagged Restrictions**: - **`appsettings.Test.json`**: Implemented `Features:AllowRegistration` and `Features:AllowPasswordReset` flags set to `false`. - **Middleware Enforcement (`Program.cs`)**: Intercepts requests to `/identity/register` and `/identity/forgotPassword` (and their MVC/form variations) and rejects them with a `403 Forbidden` response in restricted environments. - **OAuth Provisioning Guard (`Program.cs`)**: Blocks new account provisioning via Google OAuth callback by checking the `Features:AllowRegistration` configuration, redirecting users to the login page with a descriptive error. - **UI Protection (`Login.razor`, `Register.razor`)**: Conditionally hides registration/password reset links and intercepts manual navigation attempts to `/account/register` by redirecting to login with a warning. --------- Co-authored-by: Marek Jasiński <jasins.marek@gmail.com> Reviewed-on: #56 Co-authored-by: Antigravity <antigravity@google.com> Co-committed-by: Antigravity <antigravity@google.com>
This commit was merged in pull request #56.
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
@inject IIdentityService IdentityService
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject IJSRuntime JS
|
||||
@inject FeatureSettings FeatureSettings
|
||||
|
||||
<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">
|
||||
@@ -95,20 +102,33 @@
|
||||
<input type="hidden" name="email" value="@_loginModel.Email" />
|
||||
<input type="hidden" name="password" value="@_loginModel.Password" />
|
||||
<input type="hidden" name="rememberMe" value="@(_loginModel.RememberMe ? "true" : "false")" />
|
||||
<input type="hidden" name="returnUrl" value="@ReturnUrl" />
|
||||
</form>
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
private Task<AuthenticationState>? AuthStateTask { get; set; }
|
||||
|
||||
[Parameter]
|
||||
[SupplyParameterFromQuery(Name = "error")]
|
||||
public string? ErrorCode { get; set; }
|
||||
|
||||
[Parameter]
|
||||
[SupplyParameterFromQuery(Name = "returnUrl")]
|
||||
public string? ReturnUrl { get; set; }
|
||||
|
||||
private LoginModel _loginModel = new();
|
||||
private string? _errorMessage;
|
||||
private bool _isSubmitting;
|
||||
private bool _showPassword;
|
||||
private bool _allowRegistration;
|
||||
private bool _allowPasswordReset;
|
||||
|
||||
protected override void OnInitialized()
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_allowRegistration = FeatureSettings.AllowRegistration;
|
||||
_allowPasswordReset = FeatureSettings.AllowPasswordReset;
|
||||
|
||||
if (!string.IsNullOrEmpty(ErrorCode))
|
||||
{
|
||||
_errorMessage = ErrorCode switch
|
||||
@@ -118,9 +138,19 @@
|
||||
"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."
|
||||
};
|
||||
}
|
||||
|
||||
if (AuthStateTask != null)
|
||||
{
|
||||
var authState = await AuthStateTask;
|
||||
if (authState.User.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
NavigationManager.NavigateTo(string.IsNullOrEmpty(ReturnUrl) ? "/" : ReturnUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleLogin()
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
@inject IIdentityService IdentityService
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject IJSRuntime JS
|
||||
@inject FeatureSettings FeatureSettings
|
||||
|
||||
<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 = FeatureSettings.AllowRegistration;
|
||||
if (!allowRegistration)
|
||||
{
|
||||
NavigationManager.NavigateTo("/account/login?error=RegistrationDisabled", replace: true);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleRegister()
|
||||
{
|
||||
_isSubmitting = true;
|
||||
|
||||
@@ -528,3 +528,81 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Mobile Dashboard Overrides */
|
||||
@media (max-width: 768px) {
|
||||
.dashboard-content {
|
||||
padding: 1.25rem 0.75rem;
|
||||
}
|
||||
|
||||
.profile-header {
|
||||
padding: 1.5rem 1rem;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.profile-visual {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.avatar-wrapper {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.user-role {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.status-pills {
|
||||
width: 100%;
|
||||
margin-top: 0.5rem;
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
padding: 0.35rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.main-grid {
|
||||
grid-template-columns: 1fr !important;
|
||||
gap: 1.25rem !important;
|
||||
}
|
||||
|
||||
.secondary-grid {
|
||||
grid-template-columns: 1fr !important;
|
||||
gap: 1.25rem !important;
|
||||
}
|
||||
|
||||
/* Force all widgets to take 100% width and fit inside parent container nicely */
|
||||
.glass-panel {
|
||||
width: 100% !important;
|
||||
padding: 1.25rem !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Expand touch-targets to 48px min height for interactive elements */
|
||||
.btn-nexus, .quiz-option, .satellite, .logout-btn, .nav-item, .quiz-item {
|
||||
min-height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
@using NexusReader.Application.Abstractions.Services
|
||||
@using NexusReader.Application.DTOs.User
|
||||
@using NexusReader.UI.Shared.Components.Atoms
|
||||
@using NexusReader.UI.Shared.Models
|
||||
@using System.Net.Http.Json
|
||||
@inject HttpClient Http
|
||||
@inject IKnowledgeService KnowledgeService
|
||||
@@ -145,22 +146,7 @@
|
||||
private List<LastReadBookDto>? _books;
|
||||
private List<ChatMessage> _chatMessages = new();
|
||||
|
||||
public class ChatMessage
|
||||
{
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
public string Sender { get; set; } = string.Empty; // "User" or "AI"
|
||||
public string Text { get; set; } = string.Empty;
|
||||
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
||||
public List<ResponseSegment> Segments { get; set; } = new();
|
||||
public List<CitationDto> Citations { get; set; } = new();
|
||||
}
|
||||
|
||||
public class ResponseSegment
|
||||
{
|
||||
public string Text { get; set; } = string.Empty;
|
||||
public bool IsCitation { get; set; }
|
||||
public string CitationId { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
|
||||
@@ -2,104 +2,113 @@
|
||||
@inject ILogger<SerilogDemo> Logger
|
||||
@inject IJSRuntime JSRuntime
|
||||
|
||||
#if DEBUG
|
||||
<div class="serilog-demo-container">
|
||||
<div class="header-card glass-panel">
|
||||
<div class="header-content">
|
||||
<NexusIcon Name="cpu" Size="36" Class="header-icon" />
|
||||
<div class="header-text">
|
||||
<h1>Serilog Logging Infrastructure</h1>
|
||||
<p class="subtitle">Production-grade diagnostic pipeline for unified native & web logs</p>
|
||||
@if (_isDebug)
|
||||
{
|
||||
<div class="serilog-demo-container">
|
||||
<div class="header-card glass-panel">
|
||||
<div class="header-content">
|
||||
<NexusIcon Name="cpu" Size="36" Class="header-icon" />
|
||||
<div class="header-text">
|
||||
<h1>Serilog Logging Infrastructure</h1>
|
||||
<p class="subtitle">Production-grade diagnostic pipeline for unified native & web logs</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-badge">
|
||||
<span class="status-dot green"></span>
|
||||
<span class="status-text">Pipeline Active</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-badge">
|
||||
<span class="status-dot green"></span>
|
||||
<span class="status-text">Pipeline Active</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-grid">
|
||||
<!-- Native .NET Logging Panel -->
|
||||
<div class="control-card glass-panel">
|
||||
<div class="demo-grid">
|
||||
<!-- Native .NET Logging Panel -->
|
||||
<div class="control-card glass-panel">
|
||||
<div class="card-header">
|
||||
<NexusIcon Name="terminal" Size="20" Class="card-icon" />
|
||||
<h2>Native .NET Logs (C#)</h2>
|
||||
</div>
|
||||
<p class="card-desc">Trigger structured C# logs using Dependency Injected ILogger.</p>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-info" @onclick="LogInfo">
|
||||
<NexusIcon Name="info" Size="16" />
|
||||
Log Info
|
||||
</button>
|
||||
<button class="btn btn-warning" @onclick="LogWarning">
|
||||
<NexusIcon Name="alert-triangle" Size="16" />
|
||||
Log Warning
|
||||
</button>
|
||||
<button class="btn btn-error" @onclick="LogError">
|
||||
<NexusIcon Name="x-circle" Size="16" />
|
||||
Log Error Exception
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Blazor / JS Interop Bridge Panel -->
|
||||
<div class="control-card glass-panel">
|
||||
<div class="card-header">
|
||||
<NexusIcon Name="globe" Size="20" Class="card-icon js-icon" />
|
||||
<h2>Blazor / JS WebView Logs</h2>
|
||||
</div>
|
||||
<p class="card-desc">Trigger logs from JavaScript to verify the interop error capture bridge.</p>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-js-info" @onclick="TriggerJsLog">
|
||||
<NexusIcon Name="message-square" Size="16" />
|
||||
Trigger console.log()
|
||||
</button>
|
||||
<button class="btn btn-js-error" @onclick="TriggerJsException">
|
||||
<NexusIcon Name="zap" Size="16" />
|
||||
Trigger JS Exception
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Log Config Panel -->
|
||||
<div class="config-card glass-panel">
|
||||
<div class="card-header">
|
||||
<NexusIcon Name="terminal" Size="20" Class="card-icon" />
|
||||
<h2>Native .NET Logs (C#)</h2>
|
||||
<NexusIcon Name="settings" Size="20" Class="card-icon" />
|
||||
<h2>Pipeline Diagnostics</h2>
|
||||
</div>
|
||||
<p class="card-desc">Trigger structured C# logs using Dependency Injected ILogger.</p>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-info" @onclick="LogInfo">
|
||||
<NexusIcon Name="info" Size="16" />
|
||||
Log Info
|
||||
</button>
|
||||
<button class="btn btn-warning" @onclick="LogWarning">
|
||||
<NexusIcon Name="alert-triangle" Size="16" />
|
||||
Log Warning
|
||||
</button>
|
||||
<button class="btn btn-error" @onclick="LogError">
|
||||
<NexusIcon Name="x-circle" Size="16" />
|
||||
Log Error Exception
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Blazor / JS Interop Bridge Panel -->
|
||||
<div class="control-card glass-panel">
|
||||
<div class="card-header">
|
||||
<NexusIcon Name="globe" Size="20" Class="card-icon js-icon" />
|
||||
<h2>Blazor / JS WebView Logs</h2>
|
||||
</div>
|
||||
<p class="card-desc">Trigger logs from JavaScript to verify the interop error capture bridge.</p>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-js-info" @onclick="TriggerJsLog">
|
||||
<NexusIcon Name="message-square" Size="16" />
|
||||
Trigger console.log()
|
||||
</button>
|
||||
<button class="btn btn-js-error" @onclick="TriggerJsException">
|
||||
<NexusIcon Name="zap" Size="16" />
|
||||
Trigger JS Exception
|
||||
</button>
|
||||
<div class="config-grid">
|
||||
<div class="config-item">
|
||||
<span class="label">Rolling Daily File Sandbox Path</span>
|
||||
<span class="value code-value">AppDataDirectory/logs/log-*.txt</span>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Active Configuration Provider</span>
|
||||
<span class="value">Serilog.Settings.Configuration (appsettings.json)</span>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Native Apple Console Sink</span>
|
||||
<span class="value">Serilog.Sinks.Debug (conditional compilation)</span>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Native Android Logcat Sink</span>
|
||||
<span class="value">AndroidLogcatSink (direct JNI bindings)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Log Config Panel -->
|
||||
<div class="config-card glass-panel">
|
||||
<div class="card-header">
|
||||
<NexusIcon Name="settings" Size="20" Class="card-icon" />
|
||||
<h2>Pipeline Diagnostics</h2>
|
||||
</div>
|
||||
<div class="config-grid">
|
||||
<div class="config-item">
|
||||
<span class="label">Rolling Daily File Sandbox Path</span>
|
||||
<span class="value code-value">AppDataDirectory/logs/log-*.txt</span>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Active Configuration Provider</span>
|
||||
<span class="value">Serilog.Settings.Configuration (appsettings.json)</span>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Native Apple Console Sink</span>
|
||||
<span class="value">Serilog.Sinks.Debug (conditional compilation)</span>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Native Android Logcat Sink</span>
|
||||
<span class="value">AndroidLogcatSink (direct JNI bindings)</span>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="serilog-demo-container">
|
||||
<div class="glass-panel" style="text-align: center; padding: 3rem;">
|
||||
<h2>Diagnostics Unavailable</h2>
|
||||
<p>This page is only available in DEBUG builds.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
#else
|
||||
<div class="serilog-demo-container">
|
||||
<div class="glass-panel" style="text-align: center; padding: 3rem;">
|
||||
<h2>Diagnostics Unavailable</h2>
|
||||
<p>This page is only available in DEBUG builds.</p>
|
||||
</div>
|
||||
</div>
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
@code {
|
||||
// Compile-time check ensures _isDebug is baked as false in Release/Test/Production builds,
|
||||
// which completely bypasses/strips rendering of the diagnostic UI and avoids exposing internal controls.
|
||||
#if DEBUG
|
||||
private readonly bool _isDebug = true;
|
||||
#else
|
||||
private readonly bool _isDebug = false;
|
||||
#endif
|
||||
|
||||
private void LogInfo()
|
||||
{
|
||||
Logger.LogInformation("Structured native log triggered by user from SerilogDemo. Button: LogInfo");
|
||||
@@ -124,12 +133,32 @@
|
||||
|
||||
private async Task TriggerJsLog()
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("console.log", "Intercepted JS console statement from Blazor WebView interop trigger!");
|
||||
try
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("console.log", "Intercepted JS console statement from Blazor WebView interop trigger!");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed to execute console.log from diagnostic panel.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TriggerJsException()
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("eval", "throw new Error('Simulated runtime JS Exception triggered from Blazor UI button click!');");
|
||||
try
|
||||
{
|
||||
// Triggers a TypeError by invoking a non-existent method, which is completely CSP-compliant and works without eval()
|
||||
await JSRuntime.InvokeVoidAsync("window.nonExistentFunctionTriggeringException");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Simulated runtime JS Exception triggered and captured in Blazor UI");
|
||||
try
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("console.error", $"Simulated JS Exception: {ex.Message}");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user