diff --git a/src/dotnet/QuantEngine.Web/Client/Infrastructure/CustomAuthenticationStateProvider.cs b/src/dotnet/QuantEngine.Web/Client/Infrastructure/CustomAuthenticationStateProvider.cs new file mode 100644 index 0000000..5e71f64 --- /dev/null +++ b/src/dotnet/QuantEngine.Web/Client/Infrastructure/CustomAuthenticationStateProvider.cs @@ -0,0 +1,63 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Components.Authorization; +using QuantEngine.Web.Client.Services; + +namespace QuantEngine.Web.Client.Infrastructure +{ + public class CustomAuthenticationStateProvider : AuthenticationStateProvider + { + private readonly LocalStorageService _localStorage; + private readonly ClaimsPrincipal _anonymous = new ClaimsPrincipal(new ClaimsIdentity()); + private const string StorageKey = "quant_admin_session"; + + public CustomAuthenticationStateProvider(LocalStorageService localStorage) + { + _localStorage = localStorage; + } + + public override async Task GetAuthenticationStateAsync() + { + try + { + var username = await _localStorage.GetAsync(StorageKey); + if (!string.IsNullOrEmpty(username)) + { + var identity = new ClaimsIdentity(new[] + { + new Claim(ClaimTypes.Name, username), + new Claim(ClaimTypes.Role, "Admin") + }, "QuantAdminAuth"); + + var user = new ClaimsPrincipal(identity); + return new AuthenticationState(user); + } + } + catch + { + // Return anonymous if localStorage isn't ready + } + + return new AuthenticationState(_anonymous); + } + + public async Task MarkUserAsAuthenticatedAsync(string username) + { + await _localStorage.SetAsync(StorageKey, username); + + var identity = new ClaimsIdentity(new[] + { + new Claim(ClaimTypes.Name, username), + new Claim(ClaimTypes.Role, "Admin") + }, "QuantAdminAuth"); + + var user = new ClaimsPrincipal(identity); + NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(user))); + } + + public async Task MarkUserAsLoggedOutAsync() + { + await _localStorage.DeleteAsync(StorageKey); + NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(_anonymous))); + } + } +} diff --git a/src/dotnet/QuantEngine.Web/Components/Layout/MainLayout.razor b/src/dotnet/QuantEngine.Web/Client/Layout/MainLayout.razor similarity index 61% rename from src/dotnet/QuantEngine.Web/Components/Layout/MainLayout.razor rename to src/dotnet/QuantEngine.Web/Client/Layout/MainLayout.razor index 011c427..2f03cbe 100644 --- a/src/dotnet/QuantEngine.Web/Components/Layout/MainLayout.razor +++ b/src/dotnet/QuantEngine.Web/Client/Layout/MainLayout.razor @@ -1,8 +1,10 @@ @inherits LayoutComponentBase -@inject Microsoft.AspNetCore.Hosting.IWebHostEnvironment WebHostEnvironment -@using System.IO -@using System.Text.Json +@inject HttpClient Http +@inject AuthenticationStateProvider AuthStateProvider +@inject NavigationManager NavigationManager +@using System.Net.Http.Json @using Microsoft.FluentUI.AspNetCore.Components +@using QuantEngine.Web.Client.Infrastructure @@ -15,6 +17,16 @@ ☰

QuantEngine v@appVersion

+ + +
+ 관리자 (@context.User.Identity?.Name) + + 로그아웃 + +
+
+
@@ -64,25 +76,15 @@ private string appVersion = "Local Debug"; private string buildTime = "N/A"; - protected override void OnInitialized() + protected override async Task OnInitializedAsync() { try { - var versionFilePath = Path.Combine(WebHostEnvironment.WebRootPath, "version.json"); - if (File.Exists(versionFilePath)) + var versionInfo = await Http.GetFromJsonAsync("version.json"); + if (versionInfo != null) { - var jsonContent = File.ReadAllText(versionFilePath); - using var doc = System.Text.Json.JsonDocument.Parse(jsonContent); - var root = doc.RootElement; - - if (root.TryGetProperty("version", out var versionProp)) - { - appVersion = versionProp.GetString() ?? "Local Debug"; - } - if (root.TryGetProperty("built", out var builtProp)) - { - buildTime = builtProp.GetString() ?? "N/A"; - } + appVersion = versionInfo.Version ?? "Local Debug"; + buildTime = versionInfo.Built ?? "N/A"; } } catch @@ -90,5 +92,18 @@ // Fail-safe default fallback values } } + + private async Task HandleLogoutAsync() + { + var customProvider = (CustomAuthenticationStateProvider)AuthStateProvider; + await customProvider.MarkUserAsLoggedOutAsync(); + NavigationManager.NavigateTo("login"); + } + + private class VersionInfo + { + public string? Version { get; set; } + public string? Built { get; set; } + } } diff --git a/src/dotnet/QuantEngine.Web/Components/Layout/MainLayout.razor.css b/src/dotnet/QuantEngine.Web/Client/Layout/MainLayout.razor.css similarity index 100% rename from src/dotnet/QuantEngine.Web/Components/Layout/MainLayout.razor.css rename to src/dotnet/QuantEngine.Web/Client/Layout/MainLayout.razor.css diff --git a/src/dotnet/QuantEngine.Web/Components/Layout/NavMenu.razor b/src/dotnet/QuantEngine.Web/Client/Layout/NavMenu.razor similarity index 100% rename from src/dotnet/QuantEngine.Web/Components/Layout/NavMenu.razor rename to src/dotnet/QuantEngine.Web/Client/Layout/NavMenu.razor diff --git a/src/dotnet/QuantEngine.Web/Components/Layout/NavMenu.razor.css b/src/dotnet/QuantEngine.Web/Client/Layout/NavMenu.razor.css similarity index 100% rename from src/dotnet/QuantEngine.Web/Components/Layout/NavMenu.razor.css rename to src/dotnet/QuantEngine.Web/Client/Layout/NavMenu.razor.css diff --git a/src/dotnet/QuantEngine.Web/Client/Pages/Collection.razor b/src/dotnet/QuantEngine.Web/Client/Pages/Collection.razor index 06ab307..1c70cfd 100644 --- a/src/dotnet/QuantEngine.Web/Client/Pages/Collection.razor +++ b/src/dotnet/QuantEngine.Web/Client/Pages/Collection.razor @@ -1,5 +1,6 @@ @page "/collection" -@using QuantEngine.Web.Services +@attribute [Authorize] +@using QuantEngine.Web.Client.Services @inject ApiClient ApiClient @inject ILogger Logger diff --git a/src/dotnet/QuantEngine.Web/Components/Pages/Dashboard.razor b/src/dotnet/QuantEngine.Web/Client/Pages/Dashboard.razor similarity index 85% rename from src/dotnet/QuantEngine.Web/Components/Pages/Dashboard.razor rename to src/dotnet/QuantEngine.Web/Client/Pages/Dashboard.razor index d924141..d22ffce 100644 --- a/src/dotnet/QuantEngine.Web/Components/Pages/Dashboard.razor +++ b/src/dotnet/QuantEngine.Web/Client/Pages/Dashboard.razor @@ -1,6 +1,7 @@ @page "/" +@attribute [Authorize] @using QuantEngine.Core.Infrastructure -@inject IWebHostEnvironment Environment +@inject HttpClient Http Quant Engine - Dashboard @@ -96,15 +97,26 @@ private string RawFeedLabel = "DISCONNECTED"; private string ReportPath = "n/a"; - protected override void OnInitialized() + protected override async Task OnInitializedAsync() { - ReportPath = Path.GetFullPath(Path.Combine(Environment.ContentRootPath, "..", "..", "..", "Temp", "operational_report.json")); - var report = OperationalReportLoader.Load(ReportPath); - Sections.AddRange(report.Sections); - SectionCountLabel = report.SectionCount.ToString(); - GeneratedAtLabel = report.GeneratedAt; - SourceLabel = report.SourceJson; - ReportStateLabel = Sections.Count > 0 ? "READY" : "DATA_MISSING"; - ReportChipLabel = Sections.Count > 0 ? "READY" : "DATA_MISSING"; + try + { + var report = await Http.GetFromJsonAsync("api/operational-report"); + if (report != null) + { + Sections.Clear(); + Sections.AddRange(report.Sections); + SectionCountLabel = report.SectionCount.ToString(); + GeneratedAtLabel = report.GeneratedAt; + SourceLabel = report.SourceJson; + ReportStateLabel = Sections.Count > 0 ? "READY" : "DATA_MISSING"; + ReportChipLabel = Sections.Count > 0 ? "READY" : "DATA_MISSING"; + } + } + catch + { + ReportStateLabel = "DATA_MISSING"; + ReportChipLabel = "DATA_MISSING"; + } } } diff --git a/src/dotnet/QuantEngine.Web/Client/Pages/Login.razor b/src/dotnet/QuantEngine.Web/Client/Pages/Login.razor new file mode 100644 index 0000000..0779a4b --- /dev/null +++ b/src/dotnet/QuantEngine.Web/Client/Pages/Login.razor @@ -0,0 +1,308 @@ +@page "/login" +@attribute [AllowAnonymous] +@inject AuthenticationStateProvider AuthStateProvider +@inject NavigationManager NavigationManager +@inject HttpClient Http + +로그인 - QuantEngine + +
+
+
+ +

QuantEngine

+

은퇴자산포트폴리오 투자 관리 시스템

+
+ +
+
+ + +
+ +
+ + +
+ + @if (!string.IsNullOrEmpty(ErrorMessage)) + { +
+ + + + @ErrorMessage +
+ } + + +
+
+
+ + + +@code { + private string Username { get; set; } = string.Empty; + private string Password { get; set; } = string.Empty; + private string ErrorMessage { get; set; } = string.Empty; + private bool IsSubmitting { get; set; } = false; + + private async Task HandleLoginAsync() + { + ErrorMessage = string.Empty; + if (string.IsNullOrWhiteSpace(Username) || string.IsNullOrWhiteSpace(Password)) + { + ErrorMessage = "아이디와 비밀번호를 모두 입력해 주세요."; + return; + } + + IsSubmitting = true; + + try + { + var response = await Http.PostAsJsonAsync("api/auth/login", new { Username, Password }); + + if (response.IsSuccessStatusCode) + { + var customProvider = (CustomAuthenticationStateProvider)AuthStateProvider; + await customProvider.MarkUserAsAuthenticatedAsync(Username); + + // Redirect back to home dashboard + NavigationManager.NavigateTo(""); + } + else + { + ErrorMessage = "아이디 또는 비밀번호가 올바르지 않습니다."; + } + } + catch (Exception ex) + { + ErrorMessage = $"로그인 중 오류가 발생했습니다: {ex.Message}"; + } + finally + { + IsSubmitting = false; + } + } +} diff --git a/src/dotnet/QuantEngine.Web/Components/Pages/NotFound.razor b/src/dotnet/QuantEngine.Web/Client/Pages/NotFound.razor similarity index 100% rename from src/dotnet/QuantEngine.Web/Components/Pages/NotFound.razor rename to src/dotnet/QuantEngine.Web/Client/Pages/NotFound.razor diff --git a/src/dotnet/QuantEngine.Web/Components/Pages/Operations.razor b/src/dotnet/QuantEngine.Web/Client/Pages/Operations.razor similarity index 84% rename from src/dotnet/QuantEngine.Web/Components/Pages/Operations.razor rename to src/dotnet/QuantEngine.Web/Client/Pages/Operations.razor index fa21792..9e44e3f 100644 --- a/src/dotnet/QuantEngine.Web/Components/Pages/Operations.razor +++ b/src/dotnet/QuantEngine.Web/Client/Pages/Operations.razor @@ -1,6 +1,7 @@ @page "/operations" +@attribute [Authorize] @using QuantEngine.Core.Infrastructure -@inject IWebHostEnvironment Environment +@inject HttpClient Http Quant Engine - Operations @@ -97,19 +98,29 @@ protected override async Task OnInitializedAsync() { - ReportPath = Path.GetFullPath(Path.Combine(Environment.ContentRootPath, "..", "..", "..", "Temp", "operational_report.json")); + try + { + var report = await Http.GetFromJsonAsync("api/operational-report"); + if (report != null) + { + SchemaVersion = report.SchemaVersion; + SourceJson = report.SourceJson; + GeneratedAt = report.GeneratedAt; + + Sections.Clear(); + Sections.AddRange(report.Sections); - var report = OperationalReportLoader.Load(ReportPath); - SchemaVersion = report.SchemaVersion; - SourceJson = report.SourceJson; - GeneratedAt = report.GeneratedAt; - Sections.AddRange(report.Sections); + HighlightSections.Clear(); + HighlightSections.AddRange(Sections.Take(4)); - HighlightSections.Clear(); - HighlightSections.AddRange(Sections.Take(4)); - - SectionCountLabel = report.SectionCount.ToString(); - RenderedSectionCountLabel = Sections.Count.ToString(); - HealthLabel = Sections.Count > 0 ? "PASS" : "DATA_MISSING"; + SectionCountLabel = report.SectionCount.ToString(); + RenderedSectionCountLabel = Sections.Count.ToString(); + HealthLabel = Sections.Count > 0 ? "PASS" : "DATA_MISSING"; + } + } + catch + { + HealthLabel = "DATA_MISSING"; + } } } diff --git a/src/dotnet/QuantEngine.Web/Client/Program.cs b/src/dotnet/QuantEngine.Web/Client/Program.cs new file mode 100644 index 0000000..89b6b09 --- /dev/null +++ b/src/dotnet/QuantEngine.Web/Client/Program.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using Microsoft.FluentUI.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Authorization; +using QuantEngine.Web.Client.Services; +using QuantEngine.Web.Client.Infrastructure; + +var builder = WebAssemblyHostBuilder.CreateDefault(args); + +// Register Fluent UI +builder.Services.AddFluentUIComponents(); + +// Register LocalStorage for cross-platform session persistence +builder.Services.AddScoped(); + +// Authentication setup in WebAssembly client +builder.Services.AddAuthorizationCore(); +builder.Services.AddCascadingAuthenticationState(); +builder.Services.AddScoped(); + +// HttpClient register (API-First standard) +builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); + +await builder.Build().RunAsync(); diff --git a/src/dotnet/QuantEngine.Web/Client/QuantEngine.Web.Client.csproj b/src/dotnet/QuantEngine.Web/Client/QuantEngine.Web.Client.csproj new file mode 100644 index 0000000..ad69745 --- /dev/null +++ b/src/dotnet/QuantEngine.Web/Client/QuantEngine.Web.Client.csproj @@ -0,0 +1,23 @@ + + + + net10.0 + enable + enable + true + Default + + + + + + + + + + + + + + + diff --git a/src/dotnet/QuantEngine.Web/Client/RedirectToLogin.razor b/src/dotnet/QuantEngine.Web/Client/RedirectToLogin.razor new file mode 100644 index 0000000..ab9645f --- /dev/null +++ b/src/dotnet/QuantEngine.Web/Client/RedirectToLogin.razor @@ -0,0 +1,8 @@ +@inject NavigationManager NavigationManager + +@code { + protected override void OnInitialized() + { + NavigationManager.NavigateTo("login"); + } +} diff --git a/src/dotnet/QuantEngine.Web/Services/ApiClient.cs b/src/dotnet/QuantEngine.Web/Client/Services/ApiClient.cs similarity index 91% rename from src/dotnet/QuantEngine.Web/Services/ApiClient.cs rename to src/dotnet/QuantEngine.Web/Client/Services/ApiClient.cs index b036a6b..cc03d04 100644 --- a/src/dotnet/QuantEngine.Web/Services/ApiClient.cs +++ b/src/dotnet/QuantEngine.Web/Client/Services/ApiClient.cs @@ -2,19 +2,16 @@ using System.Net.Http.Json; using System.Text.Json.Serialization; using QuantEngine.Core.Interfaces; -namespace QuantEngine.Web.Services; +namespace QuantEngine.Web.Client.Services; public class ApiClient { private readonly HttpClient _http; private readonly ILogger _logger; - private string BaseUrl { get; set; } - public ApiClient(HttpClient http, ILogger logger) { _http = http; _logger = logger; - BaseUrl = "http://localhost:5001"; // Default for Blazor Server } // Collection API Methods @@ -23,7 +20,7 @@ public class ApiClient { try { - return await _http.GetFromJsonAsync($"{BaseUrl}/api/collection/state"); + return await _http.GetFromJsonAsync("api/collection/state"); } catch (Exception ex) { @@ -36,7 +33,7 @@ public class ApiClient { try { - return await _http.GetFromJsonAsync($"{BaseUrl}/api/collection/runs?limit={limit}"); + return await _http.GetFromJsonAsync($"api/collection/runs?limit={limit}"); } catch (Exception ex) { @@ -49,7 +46,7 @@ public class ApiClient { try { - return await _http.GetFromJsonAsync($"{BaseUrl}/api/collection/runs/{runId}/snapshots"); + return await _http.GetFromJsonAsync($"api/collection/runs/{runId}/snapshots"); } catch (Exception ex) { @@ -62,7 +59,7 @@ public class ApiClient { try { - return await _http.GetFromJsonAsync($"{BaseUrl}/api/collection/runs/{runId}/errors?limit={limit}"); + return await _http.GetFromJsonAsync($"api/collection/runs/{runId}/errors?limit={limit}"); } catch (Exception ex) { @@ -75,7 +72,7 @@ public class ApiClient { try { - var response = await _http.PostAsJsonAsync($"{BaseUrl}/api/collection/run", new { }); + var response = await _http.PostAsJsonAsync("api/collection/run", new { }); if (response.IsSuccessStatusCode) { return await response.Content.ReadFromJsonAsync(); diff --git a/src/dotnet/QuantEngine.Web/Client/Services/LocalStorageService.cs b/src/dotnet/QuantEngine.Web/Client/Services/LocalStorageService.cs new file mode 100644 index 0000000..bd175f4 --- /dev/null +++ b/src/dotnet/QuantEngine.Web/Client/Services/LocalStorageService.cs @@ -0,0 +1,43 @@ +using Microsoft.JSInterop; +using System.Text.Json; + +namespace QuantEngine.Web.Client.Services +{ + public class LocalStorageService + { + private readonly IJSRuntime _js; + + public LocalStorageService(IJSRuntime js) + { + _js = js; + } + + public async Task SetAsync(string key, T value) + { + var json = JsonSerializer.Serialize(value); + await _js.InvokeVoidAsync("localStorage.setItem", key, json); + } + + public async Task GetAsync(string key) + { + try + { + var json = await _js.InvokeAsync("localStorage.getItem", key); + if (string.IsNullOrEmpty(json)) + { + return default; + } + return JsonSerializer.Deserialize(json); + } + catch + { + return default; + } + } + + public async Task DeleteAsync(string key) + { + await _js.InvokeVoidAsync("localStorage.removeItem", key); + } + } +} diff --git a/src/dotnet/QuantEngine.Web/Client/_Imports.razor b/src/dotnet/QuantEngine.Web/Client/_Imports.razor index c5994ea..b98bd91 100644 --- a/src/dotnet/QuantEngine.Web/Client/_Imports.razor +++ b/src/dotnet/QuantEngine.Web/Client/_Imports.razor @@ -8,7 +8,10 @@ @using Microsoft.JSInterop @using Microsoft.FluentUI.AspNetCore.Components @using Microsoft.FluentUI.AspNetCore.Components.Icons -@using QuantEngine.Web -@using QuantEngine.Web.Components -@using QuantEngine.Web.Components.Layout -@using QuantEngine.Web.Services +@using QuantEngine.Web.Client +@using QuantEngine.Web.Client.Pages +@using QuantEngine.Web.Client.Layout +@using QuantEngine.Web.Client.Infrastructure +@using QuantEngine.Web.Client.Services +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Authorization diff --git a/src/dotnet/QuantEngine.Web/Components/App.razor b/src/dotnet/QuantEngine.Web/Components/App.razor index d3f356f..cf436ec 100644 --- a/src/dotnet/QuantEngine.Web/Components/App.razor +++ b/src/dotnet/QuantEngine.Web/Components/App.razor @@ -13,12 +13,12 @@ - + - + diff --git a/src/dotnet/QuantEngine.Web/Components/Routes.razor b/src/dotnet/QuantEngine.Web/Components/Routes.razor index 105855d..8fe12d7 100644 --- a/src/dotnet/QuantEngine.Web/Components/Routes.razor +++ b/src/dotnet/QuantEngine.Web/Components/Routes.razor @@ -1,6 +1,16 @@ - - - - - - +@using QuantEngine.Web.Client +@using QuantEngine.Web.Client.Pages +@using QuantEngine.Web.Client.Layout + + + + + + + + + + + + + diff --git a/src/dotnet/QuantEngine.Web/Components/_Imports.razor b/src/dotnet/QuantEngine.Web/Components/_Imports.razor index 61840dc..9fae3a5 100644 --- a/src/dotnet/QuantEngine.Web/Components/_Imports.razor +++ b/src/dotnet/QuantEngine.Web/Components/_Imports.razor @@ -1,4 +1,4 @@ -@using System.Net.Http +@using System.Net.Http @using System.Net.Http.Json @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @@ -11,3 +11,6 @@ @using QuantEngine.Web @using QuantEngine.Web.Components @using QuantEngine.Web.Components.Layout +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Authorization +@using QuantEngine.Web.Infrastructure diff --git a/src/dotnet/QuantEngine.Web/Endpoints/CollectionEndpoints.cs b/src/dotnet/QuantEngine.Web/Endpoints/CollectionEndpoints.cs index 83826a2..29d86ad 100644 --- a/src/dotnet/QuantEngine.Web/Endpoints/CollectionEndpoints.cs +++ b/src/dotnet/QuantEngine.Web/Endpoints/CollectionEndpoints.cs @@ -118,7 +118,7 @@ public static class CollectionEndpoints var runId = Guid.NewGuid().ToString("N"); var now = DateTime.UtcNow.ToString("o"); - var body = await request.ReadAsAsync(); + var body = await request.ReadFromJsonAsync(); var account = body?.Account ?? "real"; var tickers = body?.Tickers ?? new List { "005930", "000660" }; diff --git a/src/dotnet/QuantEngine.Web/Program.cs b/src/dotnet/QuantEngine.Web/Program.cs index 72a6ea4..54f8294 100644 --- a/src/dotnet/QuantEngine.Web/Program.cs +++ b/src/dotnet/QuantEngine.Web/Program.cs @@ -1,6 +1,7 @@ using QuantEngine.Web.Components; -using QuantEngine.Web.Services; using QuantEngine.Infrastructure.Data; +using Microsoft.AspNetCore.Components.Authorization; +using QuantEngine.Web.Infrastructure; using QuantEngine.Infrastructure.Repositories; using QuantEngine.Infrastructure.Services; using QuantEngine.Core.Interfaces; @@ -9,7 +10,8 @@ using System.Text.Json; using static QuantEngine.Application.Services.DataCollectionService; using Microsoft.FluentUI.AspNetCore.Components; using Serilog; -using QuantEngine.Web.Infrastructure; +using QuantEngine.Web.Client.Infrastructure; +using QuantEngine.Web.Client.Services; using QuantEngine.Web.Endpoints; // Serilog Configuration with Telegram Sink @@ -24,7 +26,13 @@ builder.Host.UseSerilog(); // Add services to the container. builder.Services.AddRazorComponents() - .AddInteractiveServerComponents(); + .AddInteractiveWebAssemblyComponents(); + +// Authentication and Custom State Provider (Shared client components) +builder.Services.AddCascadingAuthenticationState(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddAuthorizationCore(); // Fluent UI Services builder.Services.AddFluentUIComponents(); @@ -94,6 +102,32 @@ app.MapStaticAssets(); // Collection API Endpoints (must be before MapRazorComponents) app.MapCollectionEndpoints(); +// Login API (API-First for Blazor WASM client authentication) +app.MapPost("/api/auth/login", (LoginRequest request, IConfiguration config) => +{ + var expectedUser = config["AdminSettings:Username"] ?? "admin"; + var expectedPass = config["AdminSettings:Password"] ?? "quant123!"; + + if (request.Username == expectedUser && request.Password == expectedPass) + { + return Results.Ok(new { success = true, username = request.Username }); + } + return Results.Json(new { success = false, error = "invalid_credentials" }, statusCode: 401); +}); + +// Operational Report serving API (WASM safe file loading substitute) +app.MapGet("/api/operational-report", async (IWebHostEnvironment env) => +{ + var path = Path.GetFullPath(Path.Combine(env.ContentRootPath, "..", "..", "..", "Temp", "operational_report.json")); + if (!File.Exists(path)) + { + return Results.NotFound(new { gate = "FAIL", error = "operational_report_missing" }); + } + var json = await File.ReadAllTextAsync(path); + using var doc = JsonDocument.Parse(json); + return Results.Ok(doc.RootElement); +}); + app.MapGet("/api/history/{domain}", async (string domain, int? limit, IPostgresqlHistorySnapshotReader reader) => { var rows = await reader.ReadAsync(domain, limit ?? 500); @@ -138,7 +172,14 @@ app.MapPost("/api/history/{domain}", async (string domain, JsonElement payload, }); app.MapRazorComponents() - .AddInteractiveServerRenderMode(); + .AddInteractiveWebAssemblyRenderMode() + .AddAdditionalAssemblies(typeof(QuantEngine.Web.Client._Imports).Assembly); app.Run(); +public class LoginRequest +{ + public string Username { get; set; } = ""; + public string Password { get; set; } = ""; +} + diff --git a/src/dotnet/QuantEngine.Web/QuantEngine.Web.csproj b/src/dotnet/QuantEngine.Web/QuantEngine.Web.csproj index 6cd066a..38d8a2b 100644 --- a/src/dotnet/QuantEngine.Web/QuantEngine.Web.csproj +++ b/src/dotnet/QuantEngine.Web/QuantEngine.Web.csproj @@ -4,12 +4,22 @@ + + + + + + + + + + diff --git a/src/dotnet/QuantEngine.Web/appsettings.json b/src/dotnet/QuantEngine.Web/appsettings.json index acf124a..b1a203e 100644 --- a/src/dotnet/QuantEngine.Web/appsettings.json +++ b/src/dotnet/QuantEngine.Web/appsettings.json @@ -8,5 +8,9 @@ "AllowedHosts": "*", "ConnectionStrings": { "DefaultConnection": "Host=127.0.0.1;Database=giteadb;Username=gitea;Password=;Search Path=quantengine;" + }, + "AdminSettings": { + "Username": "admin", + "Password": "quant123!" } } diff --git a/src/dotnet/QuantEngine.Web/wwwroot/images/quant_engine_logo.jpg b/src/dotnet/QuantEngine.Web/wwwroot/images/quant_engine_logo.jpg new file mode 100644 index 0000000..37f0547 Binary files /dev/null and b/src/dotnet/QuantEngine.Web/wwwroot/images/quant_engine_logo.jpg differ diff --git a/src/dotnet/QuantEngine.sln b/src/dotnet/QuantEngine.sln index fe56a5d..470e55d 100644 --- a/src/dotnet/QuantEngine.sln +++ b/src/dotnet/QuantEngine.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 @@ -15,6 +15,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuantEngine.Core.Tests", "Q EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuantEngine.Tools", "QuantEngine.Tools\QuantEngine.Tools.csproj", "{E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}" EndProject + +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuantEngine.Web.Client", "QuantEngine.Web\Client\QuantEngine.Web.Client.csproj", "{C5F2F3BD-1258-40FC-803A-EE7EEC928107}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -97,8 +100,21 @@ Global {E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}.Release|x64.Build.0 = Release|Any CPU {E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}.Release|x86.ActiveCfg = Release|Any CPU {E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}.Release|x86.Build.0 = Release|Any CPU + {C5F2F3BD-1258-40FC-803A-EE7EEC928107}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C5F2F3BD-1258-40FC-803A-EE7EEC928107}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C5F2F3BD-1258-40FC-803A-EE7EEC928107}.Debug|x64.ActiveCfg = Debug|Any CPU + {C5F2F3BD-1258-40FC-803A-EE7EEC928107}.Debug|x64.Build.0 = Debug|Any CPU + {C5F2F3BD-1258-40FC-803A-EE7EEC928107}.Debug|x86.ActiveCfg = Debug|Any CPU + {C5F2F3BD-1258-40FC-803A-EE7EEC928107}.Debug|x86.Build.0 = Debug|Any CPU + {C5F2F3BD-1258-40FC-803A-EE7EEC928107}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C5F2F3BD-1258-40FC-803A-EE7EEC928107}.Release|Any CPU.Build.0 = Release|Any CPU + {C5F2F3BD-1258-40FC-803A-EE7EEC928107}.Release|x64.ActiveCfg = Release|Any CPU + {C5F2F3BD-1258-40FC-803A-EE7EEC928107}.Release|x64.Build.0 = Release|Any CPU + {C5F2F3BD-1258-40FC-803A-EE7EEC928107}.Release|x86.ActiveCfg = Release|Any CPU + {C5F2F3BD-1258-40FC-803A-EE7EEC928107}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + EndGlobal diff --git a/tools/validate_gitea_token_home_v1.py b/tools/validate_gitea_token_home_v1.py index 2303ba4..f58ff83 100644 --- a/tools/validate_gitea_token_home_v1.py +++ b/tools/validate_gitea_token_home_v1.py @@ -29,6 +29,7 @@ def _request_json(url: str, token: str, method: str = "GET", body: dict[str, Any headers = { "Authorization": f"token {token}", "Accept": "application/json", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", } if body is not None: headers["Content-Type"] = "application/json"