WBS-11: Hardening BFF Architecture, fix /Index page redirect and JsonDocument ObjectDisposedException in operational-report API
Quant Engine CI/CD Pipeline / validate-core (push) Failing after 12s
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (push) Failing after 8s
Quant Engine CI/CD Pipeline / validate-ui-and-storage (push) Has been skipped
Deploy to Production / Build & Deploy to Production (push) Failing after 2m21s

This commit is contained in:
2026-07-06 10:02:54 +09:00
parent 92fc3ecbab
commit 5e22844a4a
4 changed files with 27 additions and 15 deletions
@@ -8,18 +8,28 @@ namespace QuantEngine.Infrastructure.Data
IDbConnection CreateConnection(); IDbConnection CreateConnection();
} }
public class DbConnectionFactory : IDbConnectionFactory public class DbConnectionFactory : IDbConnectionFactory, IDisposable
{ {
private readonly string _connectionString; private readonly NpgsqlDataSource _dataSource;
public DbConnectionFactory(string connectionString) public DbConnectionFactory(string connectionString)
{ {
_connectionString = connectionString; _dataSource = NpgsqlDataSource.Create(connectionString);
}
public DbConnectionFactory(NpgsqlDataSource dataSource)
{
_dataSource = dataSource;
} }
public IDbConnection CreateConnection() public IDbConnection CreateConnection()
{ {
return new NpgsqlConnection(_connectionString); return _dataSource.CreateConnection();
}
public void Dispose()
{
_dataSource.Dispose();
} }
} }
} }
@@ -223,6 +223,7 @@
} }
<form method="post" class="login-form"> <form method="post" class="login-form">
@Html.AntiForgeryToken()
<div class="form-group"> <div class="form-group">
<label for="username" class="form-label">관리자 아이디</label> <label for="username" class="form-label">관리자 아이디</label>
<input <input
@@ -42,7 +42,8 @@ namespace QuantEngine.Web.Pages.Account
try try
{ {
var loginRequest = new { Username = username, Password = password }; var loginRequest = new { Username = username, Password = password };
var response = await _httpClient.PostAsJsonAsync("/api/auth/login", loginRequest); var requestUri = $"{Request.Scheme}://{Request.Host}/api/auth/login";
var response = await _httpClient.PostAsJsonAsync(requestUri, loginRequest);
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
{ {
@@ -64,7 +65,7 @@ namespace QuantEngine.Web.Pages.Account
Response.Cookies.Delete("quant_admin_username"); Response.Cookies.Delete("quant_admin_username");
} }
return RedirectToPage("/Index"); return Redirect("/");
} }
else else
{ {
+9 -9
View File
@@ -2,6 +2,7 @@ using QuantEngine.Web.Components;
using QuantEngine.Infrastructure.Data; using QuantEngine.Infrastructure.Data;
using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Authorization;
using QuantEngine.Web.Infrastructure; using QuantEngine.Web.Infrastructure;
using Npgsql;
using QuantEngine.Infrastructure.Repositories; using QuantEngine.Infrastructure.Repositories;
using QuantEngine.Infrastructure.Services; using QuantEngine.Infrastructure.Services;
using QuantEngine.Core.Interfaces; using QuantEngine.Core.Interfaces;
@@ -37,7 +38,6 @@ builder.Host.UseSerilog();
// Add services to the container. // Add services to the container.
builder.Services.AddRazorPages(); builder.Services.AddRazorPages();
builder.Services.AddRazorComponents() builder.Services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddInteractiveWebAssemblyComponents(); .AddInteractiveWebAssemblyComponents();
// Authentication and Custom State Provider (Shared client components) // Authentication and Custom State Provider (Shared client components)
@@ -62,7 +62,9 @@ if (!string.Equals(configuredDatabase, "quantenginedb", StringComparison.Ordinal
{ {
throw new InvalidOperationException("QuantEngine must use the quantenginedb PostgreSQL database."); throw new InvalidOperationException("QuantEngine must use the quantenginedb PostgreSQL database.");
} }
builder.Services.AddSingleton<IDbConnectionFactory>(new DbConnectionFactory(connectionString)); var dataSource = NpgsqlDataSource.Create(connectionString);
builder.Services.AddSingleton(dataSource);
builder.Services.AddSingleton<IDbConnectionFactory>(new DbConnectionFactory(dataSource));
builder.Services.AddSingleton<DbMigrator>(); builder.Services.AddSingleton<DbMigrator>();
builder.Services.AddScoped<IWorkspaceRepository, WorkspaceRepository>(); builder.Services.AddScoped<IWorkspaceRepository, WorkspaceRepository>();
builder.Services.AddScoped<IPostgresqlHistoryStore, PostgresqlHistoryStore>(); builder.Services.AddScoped<IPostgresqlHistoryStore, PostgresqlHistoryStore>();
@@ -248,7 +250,7 @@ app.MapPost("/api/auth/login", async (JsonElement payload, HttpContext httpConte
new Microsoft.AspNetCore.Http.CookieOptions new Microsoft.AspNetCore.Http.CookieOptions
{ {
HttpOnly = true, HttpOnly = true,
Secure = httpContext.Request.IsHttps, Secure = true,
SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax, SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax,
Expires = devExpiresAt, Expires = devExpiresAt,
Path = "/" Path = "/"
@@ -303,8 +305,8 @@ app.MapPost("/api/auth/login", async (JsonElement payload, HttpContext httpConte
new Microsoft.AspNetCore.Http.CookieOptions new Microsoft.AspNetCore.Http.CookieOptions
{ {
HttpOnly = true, HttpOnly = true,
Secure = httpContext.Request.IsHttps, // Only secure on HTTPS Secure = true, // Force Secure Cookie for SSL standard
SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax, // Lax for localhost SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax,
Expires = expiresAt, Expires = expiresAt,
Path = "/" Path = "/"
} }
@@ -443,7 +445,6 @@ app.MapPost("/api/auth/admin/reset-password", async (HttpContext context, JsonEl
}); });
}).DisableAntiforgery(); }).DisableAntiforgery();
// Operational Report serving API (WASM safe file loading substitute)
app.MapGet("/api/operational-report", async (IWebHostEnvironment env) => app.MapGet("/api/operational-report", async (IWebHostEnvironment env) =>
{ {
var path = Path.GetFullPath(Path.Combine(env.ContentRootPath, "..", "..", "..", "Temp", "operational_report.json")); var path = Path.GetFullPath(Path.Combine(env.ContentRootPath, "..", "..", "..", "Temp", "operational_report.json"));
@@ -452,8 +453,8 @@ app.MapGet("/api/operational-report", async (IWebHostEnvironment env) =>
return Results.NotFound(new { gate = "FAIL", error = "operational_report_missing" }); return Results.NotFound(new { gate = "FAIL", error = "operational_report_missing" });
} }
var json = await File.ReadAllTextAsync(path); var json = await File.ReadAllTextAsync(path);
using var doc = JsonDocument.Parse(json); // Directly return raw JSON string with correct Content-Type to bypass using-disposed JSON document serializer issue
return Results.Ok(doc.RootElement); return Results.Content(json, "application/json");
}); });
app.MapGet("/api/history/{domain}", async (string domain, int? limit, IPostgresqlHistorySnapshotReader reader) => app.MapGet("/api/history/{domain}", async (string domain, int? limit, IPostgresqlHistorySnapshotReader reader) =>
@@ -505,7 +506,6 @@ app.MapRazorPages();
// Map Blazor Components - catches all remaining routes // Map Blazor Components - catches all remaining routes
app.MapRazorComponents<App>() app.MapRazorComponents<App>()
.AddInteractiveWebAssemblyRenderMode() .AddInteractiveWebAssemblyRenderMode()
.AddInteractiveServerRenderMode()
.AddAdditionalAssemblies(typeof(QuantEngine.Web.Client._Imports).Assembly); .AddAdditionalAssemblies(typeof(QuantEngine.Web.Client._Imports).Assembly);
app.Run(); app.Run();