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

+

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

+
+ +
+
+ + +
+ +
+ + +
+ + @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 + { + // 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