feat(web): add auth and fix deployment checks
Quant Engine CI/CD Pipeline / validate-core (push) Failing after 9s
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (push) Failing after 6s
Quant Engine CI/CD Pipeline / validate-ui-and-storage (push) Has been skipped
Snapshot Admin Deployment / build-and-deploy (push) Failing after 2m30s
Deploy to Production / Build & Deploy to Production (push) Failing after 3m49s

This commit is contained in:
2026-07-01 13:02:10 +09:00
parent 3e4d545e01
commit 90bbb1860d
17 changed files with 445 additions and 53 deletions
@@ -7,25 +7,41 @@ namespace QuantEngine.Web.Client.Infrastructure
public class CustomAuthenticationStateProvider : AuthenticationStateProvider
{
private readonly LocalStorageService _localStorage;
private readonly HttpClient _http;
private readonly ClaimsPrincipal _anonymous = new ClaimsPrincipal(new ClaimsIdentity());
private const string StorageKey = "quant_admin_session";
private const string TokenKey = "quant_admin_access_token";
private const string UsernameKey = "quant_admin_username";
private const string RoleKey = "quant_admin_role";
public CustomAuthenticationStateProvider(LocalStorageService localStorage)
public CustomAuthenticationStateProvider(LocalStorageService localStorage, HttpClient http)
{
_localStorage = localStorage;
_http = http;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
try
{
var username = await _localStorage.GetAsync<string>(StorageKey);
if (!string.IsNullOrEmpty(username))
var token = await _localStorage.GetAsync<string>(TokenKey);
var username = await _localStorage.GetAsync<string>(UsernameKey);
var role = await _localStorage.GetAsync<string>(RoleKey) ?? "Admin";
if (!string.IsNullOrWhiteSpace(token) && !string.IsNullOrWhiteSpace(username))
{
var request = new HttpRequestMessage(HttpMethod.Get, "api/auth/me");
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
var response = await _http.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
await MarkUserAsLoggedOutAsync();
return new AuthenticationState(_anonymous);
}
var identity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, username),
new Claim(ClaimTypes.Role, "Admin")
new Claim(ClaimTypes.Role, role)
}, "QuantAdminAuth");
var user = new ClaimsPrincipal(identity);
@@ -40,14 +56,16 @@ namespace QuantEngine.Web.Client.Infrastructure
return new AuthenticationState(_anonymous);
}
public async Task MarkUserAsAuthenticatedAsync(string username)
public async Task MarkUserAsAuthenticatedAsync(string username, string accessToken, string role)
{
await _localStorage.SetAsync(StorageKey, username);
await _localStorage.SetAsync(TokenKey, accessToken);
await _localStorage.SetAsync(UsernameKey, username);
await _localStorage.SetAsync(RoleKey, role);
var identity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, username),
new Claim(ClaimTypes.Role, "Admin")
new Claim(ClaimTypes.Role, role)
}, "QuantAdminAuth");
var user = new ClaimsPrincipal(identity);
@@ -56,8 +74,30 @@ namespace QuantEngine.Web.Client.Infrastructure
public async Task MarkUserAsLoggedOutAsync()
{
await _localStorage.DeleteAsync(StorageKey);
await _localStorage.DeleteAsync(TokenKey);
await _localStorage.DeleteAsync(UsernameKey);
await _localStorage.DeleteAsync(RoleKey);
NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(_anonymous)));
}
public async Task LogoutFromServerAsync()
{
var token = await _localStorage.GetAsync<string>(TokenKey);
if (!string.IsNullOrWhiteSpace(token))
{
try
{
var request = new HttpRequestMessage(HttpMethod.Post, "api/auth/logout");
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
await _http.SendAsync(request);
}
catch
{
// Best-effort server revocation; always clear local state.
}
}
await MarkUserAsLoggedOutAsync();
}
}
}
@@ -96,7 +96,7 @@
private async Task HandleLogoutAsync()
{
var customProvider = (CustomAuthenticationStateProvider)AuthStateProvider;
await customProvider.MarkUserAsLoggedOutAsync();
await customProvider.LogoutFromServerAsync();
NavigationManager.NavigateTo("login");
}
@@ -14,7 +14,7 @@
<p class="brand-subtitle">은퇴자산포트폴리오 투자 관리 시스템</p>
</div>
<form @onsubmit="HandleLoginAsync" class="auth-form">
<form @onsubmit="HandleLoginAsync" @onsubmit:preventDefault="true" class="auth-form">
<div class="form-group">
<label for="username">관리자 아이디</label>
<input type="text" id="username" class="form-control" @bind="Username" placeholder="아이디를 입력하세요" autocomplete="username" />
@@ -268,6 +268,15 @@
private string ErrorMessage { get; set; } = string.Empty;
private bool IsSubmitting { get; set; } = false;
private sealed class LoginResponse
{
public bool Success { get; set; }
public string? Username { get; set; }
public string? Role { get; set; }
public string? AccessToken { get; set; }
public string? ExpiresAt { get; set; }
}
private async Task HandleLoginAsync()
{
ErrorMessage = string.Empty;
@@ -285,8 +294,15 @@
if (response.IsSuccessStatusCode)
{
var auth = await response.Content.ReadFromJsonAsync<LoginResponse>();
if (auth is null || string.IsNullOrWhiteSpace(auth.AccessToken))
{
ErrorMessage = "로그인 응답이 유효하지 않습니다.";
return;
}
var customProvider = (CustomAuthenticationStateProvider)AuthStateProvider;
await customProvider.MarkUserAsAuthenticatedAsync(Username);
await customProvider.MarkUserAsAuthenticatedAsync(auth.Username ?? Username, auth.AccessToken, auth.Role ?? "Admin");
// Redirect back to home dashboard
NavigationManager.NavigateTo("");