From d3b9a6047c600e8b5bc5c22174b42c2515d46a4f Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Fri, 3 Jul 2026 13:13:23 +0900 Subject: [PATCH] fix: restore HTML login form with prerender: true per spec requirements Context: Validation script requires Login.razor to use prerender: true for immediate form display before WASM boots (Phase 9 requirement). Solution: Revert to original HTML form + JavaScript approach: - AdminLoginForm: HTML form (statically rendered, works with prerender: true) - admin-session.js: JavaScript login handler - Post-login: 200ms delay before redirect to allow CustomAuthenticationStateProvider to read tokens from localStorage and establish auth state Flow: 1. User submits form (JavaScript handles it) 2. POST /api/auth/login 3. Save tokens to localStorage 4. 200ms delay 5. Redirect to /taxbaik/admin/dashboard 6. Page loads with Blazor bootstrapping + auth state restored Result: Login form displays immediately (prerender: true) while maintaining proper authentication state propagation for [Authorize] pages. Co-Authored-By: Claude Haiku 4.5 --- .../Components/Admin/Pages/Login.razor | 2 +- .../Admin/Shared/AdminLoginForm.razor | 144 ++++++------------ src/TaxBaik.Web/wwwroot/js/admin-session.js | 9 +- 3 files changed, 49 insertions(+), 106 deletions(-) diff --git a/src/TaxBaik.Web.Client/Components/Admin/Pages/Login.razor b/src/TaxBaik.Web.Client/Components/Admin/Pages/Login.razor index 9db8e3d..1524f6c 100644 --- a/src/TaxBaik.Web.Client/Components/Admin/Pages/Login.razor +++ b/src/TaxBaik.Web.Client/Components/Admin/Pages/Login.razor @@ -1,6 +1,6 @@ @page "/admin/login" @layout TaxBaik.WasmClient.Components.Admin.Layout.BlankLayout @attribute [AllowAnonymous] -@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false)) +@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: true)) 로그인 diff --git a/src/TaxBaik.Web.Client/Components/Admin/Shared/AdminLoginForm.razor b/src/TaxBaik.Web.Client/Components/Admin/Shared/AdminLoginForm.razor index 51bef7c..39636c5 100644 --- a/src/TaxBaik.Web.Client/Components/Admin/Shared/AdminLoginForm.razor +++ b/src/TaxBaik.Web.Client/Components/Admin/Shared/AdminLoginForm.razor @@ -1,57 +1,47 @@ -@using System.Text.Json @inject ILocalStorageService LocalStorageService -@inject NavigationManager Navigation -@inject ISnackbar Snackbar -@inject HttpClient Http -@inject AuthenticationStateProvider AuthStateProvider +@inject IJSRuntime Js @code { - private MudForm? form; - private string username = ""; - private string password = ""; - private bool rememberMe = false; - private bool isLoggingIn = false; + private string rememberedUsername = ""; + private bool isRememberChecked = false; + private bool isReady; private const string RememberedUsernameKey = "admin-remembered-username"; private const string RememberedCheckboxKey = "admin-remember-checkbox"; @@ -59,83 +49,35 @@ { try { - username = await LocalStorageService.GetItemAsStringAsync(RememberedUsernameKey) ?? ""; + rememberedUsername = await LocalStorageService.GetItemAsStringAsync(RememberedUsernameKey) ?? ""; var checkboxValue = await LocalStorageService.GetItemAsStringAsync(RememberedCheckboxKey) ?? "false"; - rememberMe = checkboxValue == "true" && !string.IsNullOrEmpty(username); + isRememberChecked = checkboxValue == "true" && !string.IsNullOrEmpty(rememberedUsername); } catch { - username = ""; - rememberMe = false; + rememberedUsername = ""; + isRememberChecked = false; } } - private async Task HandleLogin() + protected override async Task OnAfterRenderAsync(bool firstRender) { - if (form == null || isLoggingIn) return; - - await form.Validate(); - if (!form.IsValid) return; - - isLoggingIn = true; - try + if (firstRender) { - var response = await Http.PostAsJsonAsync("/api/auth/login", new { username, password }); - if (!response.IsSuccessStatusCode) + try { - Snackbar.Add("로그인 실패: 사용자명 또는 비밀번호가 올바르지 않습니다", Severity.Error); - return; + await Js.InvokeVoidAsync("taxbaikAdminSession.syncRouteClass"); + await Js.InvokeVoidAsync("taxbaikAdminSession.bindLoginForm"); } - - var json = await response.Content.ReadAsStringAsync(); - var loginResponse = JsonSerializer.Deserialize(json); - if (loginResponse == null || string.IsNullOrEmpty(loginResponse.AccessToken)) + catch { - Snackbar.Add("로그인 실패: 응답 오류", Severity.Error); - return; + // Login UI must remain visible even if JS binding fails. } - - // 로컬 저장소 저장 - await LocalStorageService.SetItemAsStringAsync("accessToken", loginResponse.AccessToken); - await LocalStorageService.SetItemAsStringAsync("refreshToken", loginResponse.RefreshToken ?? ""); - var expiryTicks = DateTimeOffset.UtcNow.AddSeconds(loginResponse.ExpiresIn).UtcTicks; - await LocalStorageService.SetItemAsStringAsync("tokenExpiry", expiryTicks.ToString()); - - // 아이디 저장 - if (rememberMe) + finally { - await LocalStorageService.SetItemAsStringAsync(RememberedUsernameKey, username); - await LocalStorageService.SetItemAsStringAsync(RememberedCheckboxKey, "true"); + isReady = true; + StateHasChanged(); } - else - { - await LocalStorageService.RemoveItemAsync(RememberedUsernameKey); - await LocalStorageService.RemoveItemAsync(RememberedCheckboxKey); - } - - // Blazor 인증 상태 업데이트 - if (AuthStateProvider is CustomAuthenticationStateProvider customProvider) - { - await customProvider.LoginAsync(loginResponse.AccessToken, loginResponse.RefreshToken ?? "", loginResponse.ExpiresIn); - } - - // 대시보드로 이동 - Navigation.NavigateTo("/admin/dashboard", replace: true); } - catch (Exception ex) - { - Snackbar.Add($"로그인 중 오류: {ex.Message}", Severity.Error); - } - finally - { - isLoggingIn = false; - } - } - - private class LoginResponse - { - public string AccessToken { get; set; } = ""; - public string RefreshToken { get; set; } = ""; - public int ExpiresIn { get; set; } } } diff --git a/src/TaxBaik.Web/wwwroot/js/admin-session.js b/src/TaxBaik.Web/wwwroot/js/admin-session.js index 1e01c36..7ae9502 100644 --- a/src/TaxBaik.Web/wwwroot/js/admin-session.js +++ b/src/TaxBaik.Web/wwwroot/js/admin-session.js @@ -341,10 +341,11 @@ window.taxbaikAdminSession = { localStorage.removeItem('admin-remember-checkbox'); } - // 토큰을 저장한 직후 페이지 리로드 - // 이렇게 하면 CustomAuthenticationStateProvider의 GetAuthenticationStateAsync()가 - // localStorage에서 토큰을 복원하고 [Authorize] 페이지가 제대로 렌더링됨 - window.location.href = '/taxbaik/admin/dashboard'; + // 토큰 저장 후 약간의 지연을 두고 대시보드로 이동 + // 이렇게 하면 CustomAuthenticationStateProvider가 localStorage에서 토큰을 복원할 시간이 생김 + setTimeout(() => { + window.location.href = '/taxbaik/admin/dashboard'; + }, 200); } catch (error) { window.taxbaikAdminSession.traceUiState('admin-login', `submit failed: ${error?.message || 'login failed'}`); postLog({