diff --git a/src/dotnet/QuantEngine.Web/Components/Layout/MainLayout.razor b/src/dotnet/QuantEngine.Web/Components/Layout/MainLayout.razor
index 011c427..00b3480 100644
--- a/src/dotnet/QuantEngine.Web/Components/Layout/MainLayout.razor
+++ b/src/dotnet/QuantEngine.Web/Components/Layout/MainLayout.razor
@@ -1,8 +1,11 @@
@inherits LayoutComponentBase
@inject Microsoft.AspNetCore.Hosting.IWebHostEnvironment WebHostEnvironment
+@inject AuthenticationStateProvider AuthStateProvider
+@inject NavigationManager NavigationManager
@using System.IO
@using System.Text.Json
@using Microsoft.FluentUI.AspNetCore.Components
+@using QuantEngine.Web.Infrastructure
@@ -15,6 +18,16 @@
☰
QuantEngine v@appVersion
+
+
+
+ 관리자 (@context.User.Identity?.Name)
+
+ 로그아웃
+
+
+
+
@@ -90,5 +103,12 @@
// Fail-safe default fallback values
}
}
+
+ private async Task HandleLogoutAsync()
+ {
+ var customProvider = (CustomAuthenticationStateProvider)AuthStateProvider;
+ await customProvider.MarkUserAsLoggedOutAsync();
+ NavigationManager.NavigateTo("login");
+ }
}
diff --git a/src/dotnet/QuantEngine.Web/Components/Pages/Login.razor b/src/dotnet/QuantEngine.Web/Components/Pages/Login.razor
new file mode 100644
index 0000000..74988dd
--- /dev/null
+++ b/src/dotnet/QuantEngine.Web/Components/Pages/Login.razor
@@ -0,0 +1,310 @@
+@page "/login"
+@attribute [AllowAnonymous]
+@inject AuthenticationStateProvider AuthStateProvider
+@inject NavigationManager NavigationManager
+@inject IConfiguration Configuration
+
+로그인 - QuantEngine
+
+
+
+
+

+
QuantEngine
+
은퇴자산포트폴리오 투자 관리 시스템
+
+
+
+
+
+
+
+
+@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
+ {
+ // Verify against configurations in appsettings.json
+ var expectedUser = Configuration["AdminSettings:Username"] ?? "admin";
+ var expectedPass = Configuration["AdminSettings:Password"] ?? "quant123!";
+
+ if (Username == expectedUser && Password == expectedPass)
+ {
+ 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/RedirectToLogin.razor b/src/dotnet/QuantEngine.Web/Components/RedirectToLogin.razor
new file mode 100644
index 0000000..ab9645f
--- /dev/null
+++ b/src/dotnet/QuantEngine.Web/Components/RedirectToLogin.razor
@@ -0,0 +1,8 @@
+@inject NavigationManager NavigationManager
+
+@code {
+ protected override void OnInitialized()
+ {
+ NavigationManager.NavigateTo("login");
+ }
+}
diff --git a/src/dotnet/QuantEngine.Web/Components/Routes.razor b/src/dotnet/QuantEngine.Web/Components/Routes.razor
index 105855d..61d858b 100644
--- a/src/dotnet/QuantEngine.Web/Components/Routes.razor
+++ b/src/dotnet/QuantEngine.Web/Components/Routes.razor
@@ -1,6 +1,12 @@
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
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/Infrastructure/CustomAuthenticationStateProvider.cs b/src/dotnet/QuantEngine.Web/Infrastructure/CustomAuthenticationStateProvider.cs
new file mode 100644
index 0000000..6637d86
--- /dev/null
+++ b/src/dotnet/QuantEngine.Web/Infrastructure/CustomAuthenticationStateProvider.cs
@@ -0,0 +1,65 @@
+using System.Security.Claims;
+using Microsoft.AspNetCore.Components.Authorization;
+using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
+
+namespace QuantEngine.Web.Infrastructure
+{
+ public class CustomAuthenticationStateProvider : AuthenticationStateProvider
+ {
+ private readonly ProtectedLocalStorage _localStorage;
+ private readonly ClaimsPrincipal _anonymous = new ClaimsPrincipal(new ClaimsIdentity());
+ private const string StorageKey = "quant_admin_session";
+
+ public CustomAuthenticationStateProvider(ProtectedLocalStorage localStorage)
+ {
+ _localStorage = localStorage;
+ }
+
+ public override async Task GetAuthenticationStateAsync()
+ {
+ try
+ {
+ // ProtectedLocalStorage call will throw an exception during pre-rendering
+ var result = await _localStorage.GetAsync(StorageKey);
+ if (result.Success && !string.IsNullOrEmpty(result.Value))
+ {
+ var username = result.Value;
+ 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 state during pre-rendering or if storage reading fails
+ }
+
+ 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/Program.cs b/src/dotnet/QuantEngine.Web/Program.cs
index 72a6ea4..8e299e1 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.Infrastructure.Repositories;
using QuantEngine.Infrastructure.Services;
using QuantEngine.Core.Interfaces;
@@ -26,6 +27,11 @@ builder.Host.UseSerilog();
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
+// Authentication and Custom State Provider
+builder.Services.AddCascadingAuthenticationState();
+builder.Services.AddScoped();
+builder.Services.AddAuthorizationCore();
+
// Fluent UI Services
builder.Services.AddFluentUIComponents();
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