diff --git a/final-integrated-test.png b/final-integrated-test.png new file mode 100644 index 0000000..9ca753d Binary files /dev/null and b/final-integrated-test.png differ diff --git a/final-integrated.mjs b/final-integrated.mjs new file mode 100644 index 0000000..eddb898 --- /dev/null +++ b/final-integrated.mjs @@ -0,0 +1,70 @@ +import { chromium } from "@playwright/test"; + +(async () => { + console.log("════════════════════════════════════════════════════════"); + console.log(" ✅ FINAL INTEGRATED TEST (JS Interop Enabled)"); + console.log("════════════════════════════════════════════════════════\n"); + + const b = await chromium.launch({ headless: false }); + const p = await b.newPage(); + + p.on("console", msg => { + const text = msg.text(); + if (text.includes("[Auth]") || text.includes("[Dashboard]") || text.includes("[Login]")) { + console.log(" 📝 " + text); + } + }); + + try { + console.log("1️⃣ 로그인 페이지 로드"); + await p.goto("http://localhost:5265/login.html", { waitUntil: "networkidle" }); + + console.log("2️⃣ 로그인 (admin/admin)"); + await p.fill("input[name='username']", "admin"); + await p.fill("input[name='password']", "admin"); + await p.click("button[type='submit']"); + + console.log("3️⃣ 대기 및 모니터링 (12초)\n"); + for (let i = 1; i <= 12; i++) { + await new Promise(r => setTimeout(r, 1000)); + const url = p.url(); + if (!url.includes("login")) { + console.log(`\n ✅ [${i}s] 리다이렉트됨!`); + console.log(` URL: ${url}`); + break; + } + } + + const finalUrl = p.url(); + console.log(`\n4️⃣ 최종 상태:`); + console.log(` URL: ${finalUrl}`); + + if (finalUrl.includes("/dashboard")) { + console.log(" ✅ 대시보드 도착!"); + + // 콘텐츠 확인 + await new Promise(r => setTimeout(r, 2000)); + const content = await p.content(); + + if (content.includes("관리자 대시보드")) { + console.log(" ✅ 대시보드 콘텐츠 확인됨!"); + console.log("\n🎉🎉🎉 로그인 시스템 완전 성공!\n"); + } else { + console.log(" ⚠️ 콘텐츠 미확인"); + } + } else if (finalUrl.includes("/login")) { + console.log(" ❌ 다시 로그인으로 돌아옴"); + console.log(" → 인증 체크에서 실패했거나, JS interop이 작동하지 않음"); + } else { + console.log(" ❓ 예상치 못한 페이지"); + } + + await p.screenshot({ path: "./final-integrated-test.png", fullPage: true }); + console.log("📷 스크린샷: final-integrated-test.png"); + + } catch (e) { + console.error("Error:", e.message); + } + + await b.close(); +})(); diff --git a/precision-test-result.png b/precision-test-result.png new file mode 100644 index 0000000..1dacbb0 Binary files /dev/null and b/precision-test-result.png differ diff --git a/precision-test.mjs b/precision-test.mjs new file mode 100644 index 0000000..c241cac --- /dev/null +++ b/precision-test.mjs @@ -0,0 +1,88 @@ +import { chromium } from "@playwright/test"; + +(async () => { + console.log("════════════════════════════════════════════════════════"); + console.log(" 🔬 PRECISION DEBUG TEST (Auth Check Disabled)"); + console.log("════════════════════════════════════════════════════════\n"); + + const b = await chromium.launch({ headless: false }); + const p = await b.newPage(); + + const allLogs = []; + p.on("console", msg => { + const text = msg.text(); + allLogs.push(text); + if (text.includes("[") || text.includes("dashboard") || text.includes("login")) { + console.log(" 📝 " + text); + } + }); + + // Network events + p.on("response", res => { + const url = res.url(); + if (url.includes("dashboard") || url.includes("login") || url.includes("api")) { + console.log(` 📡 ${res.status()} ${url.split('/').pop() || 'root'}`); + } + }); + + try { + console.log("1️⃣ 로그인 페이지 로드"); + await p.goto("http://localhost:5265/login.html", { waitUntil: "networkidle" }); + + console.log("2️⃣ 로그인 제출"); + await p.fill("input[name='username']", "admin"); + await p.fill("input[name='password']", "admin"); + await p.click("button[type='submit']"); + + console.log("3️⃣ 12초 동안 모니터링\n"); + let urlHistory = []; + for (let i = 0; i < 12; i++) { + await new Promise(r => setTimeout(r, 1000)); + const url = p.url(); + if (!urlHistory.includes(url)) { + urlHistory.push(url); + console.log(` [${i+1}s] → ${url}`); + } + } + + console.log("\n4️⃣ 최종 상태:"); + const finalUrl = p.url(); + const finalContent = await p.content(); + + console.log(` URL: ${finalUrl}`); + + if (finalUrl.includes("/dashboard")) { + console.log(" ✅ /dashboard 도착!"); + + if (finalContent.includes("관리자 대시보드")) { + console.log(" ✅ 대시보드 콘텐츠 로드됨!"); + console.log("\n🎉 SUCCESS!\n"); + } else { + console.log(" ⚠️ URL은 dashboard인데 콘텐츠가 없음"); + } + } else if (finalUrl.includes("/login")) { + console.log(" ❌ 다시 login으로 리다이렉트됨"); + console.log("\n 분석:"); + console.log(" - 이것은 Dashboard.razor에서 redirect되는 뜻"); + console.log(" - localStorage에서 토큰을 읽지 못했을 가능성"); + } else { + console.log(" ❓ 예상치 못한 URL"); + } + + console.log("\n5️⃣ 콘솔 로그 분석:"); + const dashboardLogs = allLogs.filter(l => l.includes("[Dashboard]")); + if (dashboardLogs.length > 0) { + console.log(" Dashboard 로그:"); + dashboardLogs.forEach(l => console.log(" - " + l)); + } else { + console.log(" ⚠️ Dashboard 로그 없음 (페이지가 로드되지 않음?)"); + } + + await p.screenshot({ path: "./precision-test-result.png", fullPage: true }); + + } catch (e) { + console.error("Error:", e.message); + } + + await b.close(); +})(); diff --git a/src/dotnet/QuantEngine.Web/Client/Infrastructure/CustomAuthenticationStateProvider.cs b/src/dotnet/QuantEngine.Web/Client/Infrastructure/CustomAuthenticationStateProvider.cs index 08d2fb3..a583c14 100644 --- a/src/dotnet/QuantEngine.Web/Client/Infrastructure/CustomAuthenticationStateProvider.cs +++ b/src/dotnet/QuantEngine.Web/Client/Infrastructure/CustomAuthenticationStateProvider.cs @@ -1,5 +1,6 @@ using System.Security.Claims; using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.JSInterop; using QuantEngine.Web.Client.Services; namespace QuantEngine.Web.Client.Infrastructure @@ -8,30 +9,59 @@ namespace QuantEngine.Web.Client.Infrastructure { private readonly LocalStorageService _localStorage; private readonly HttpClient _http; + private readonly IJSRuntime _jsRuntime; private readonly ClaimsPrincipal _anonymous = new ClaimsPrincipal(new ClaimsIdentity()); private const string TokenKey = "quant_admin_access_token"; private const string UsernameKey = "quant_admin_username"; private const string RoleKey = "quant_admin_role"; private const string RememberUsernameKey = "quant_admin_remember_username"; - public CustomAuthenticationStateProvider(LocalStorageService localStorage, HttpClient http) + public CustomAuthenticationStateProvider(LocalStorageService localStorage, HttpClient http, IJSRuntime jsRuntime) { _localStorage = localStorage; _http = http; + _jsRuntime = jsRuntime; } public override async Task GetAuthenticationStateAsync() { try { - var token = await _localStorage.GetAsync(TokenKey); - var username = await _localStorage.GetAsync(UsernameKey); - var role = await _localStorage.GetAsync(RoleKey) ?? "Admin"; + string token = null; + string username = null; + string role = null; + + // Try to read from localStorage using JS interop (direct access) + try + { + token = await _jsRuntime.InvokeAsync("localStorage.getItem", TokenKey); + username = await _jsRuntime.InvokeAsync("localStorage.getItem", UsernameKey); + role = await _jsRuntime.InvokeAsync("localStorage.getItem", RoleKey); + + Console.WriteLine($"[Auth] JS interop: token={!string.IsNullOrWhiteSpace(token)}, username={username}"); + } + catch (Exception jsEx) + { + Console.WriteLine($"[Auth] JS interop failed: {jsEx.Message}. Falling back to LocalStorageService..."); + + // Fallback to LocalStorageService + token = await _localStorage.GetAsync(TokenKey); + username = await _localStorage.GetAsync(UsernameKey); + role = await _localStorage.GetAsync(RoleKey); + + Console.WriteLine($"[Auth] LocalStorageService: token={!string.IsNullOrWhiteSpace(token)}, username={username}"); + } + + if (string.IsNullOrWhiteSpace(role)) + { + role = "Admin"; + } if (!string.IsNullOrWhiteSpace(token) && !string.IsNullOrWhiteSpace(username)) { try { + Console.WriteLine($"[Auth] Validating token with /api/auth/me..."); 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); @@ -43,7 +73,7 @@ namespace QuantEngine.Web.Client.Infrastructure return new AuthenticationState(_anonymous); } - Console.WriteLine($"[Auth] User authenticated: {username}"); + Console.WriteLine($"[Auth] ✅ User authenticated: {username}"); var identity = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, username), @@ -61,13 +91,12 @@ namespace QuantEngine.Web.Client.Infrastructure } else { - Console.WriteLine("[Auth] No token or username found in localStorage"); + Console.WriteLine($"[Auth] ❌ No token or username found. token={!string.IsNullOrWhiteSpace(token)}, username={!string.IsNullOrWhiteSpace(username)}"); } } catch (Exception ex) { Console.WriteLine($"[Auth] Error accessing localStorage: {ex.Message}"); - // Return anonymous if localStorage isn't ready } return new AuthenticationState(_anonymous); diff --git a/src/dotnet/QuantEngine.Web/Client/Pages/Dashboard.razor b/src/dotnet/QuantEngine.Web/Client/Pages/Dashboard.razor index c5543cb..8685282 100644 --- a/src/dotnet/QuantEngine.Web/Client/Pages/Dashboard.razor +++ b/src/dotnet/QuantEngine.Web/Client/Pages/Dashboard.razor @@ -245,7 +245,7 @@ { // Check authentication var authState = await AuthStateProvider.GetAuthenticationStateAsync(); - Console.WriteLine($"[Dashboard] Auth state received. IsAuthenticated: {authState.User.Identity?.IsAuthenticated}"); + Console.WriteLine($"[Dashboard] Auth state: IsAuthenticated={authState.User.Identity?.IsAuthenticated}, Name={authState.User.Identity?.Name}"); if (!authState.User.Identity?.IsAuthenticated ?? true) { @@ -255,7 +255,7 @@ return; } - Console.WriteLine($"[Dashboard] Authenticated as: {authState.User.Identity?.Name}"); + Console.WriteLine($"[Dashboard] ✅ Authenticated as: {authState.User.Identity?.Name}"); try { diff --git a/src/dotnet/QuantEngine.Web/Program.cs b/src/dotnet/QuantEngine.Web/Program.cs index 40a9b54..fc26757 100644 --- a/src/dotnet/QuantEngine.Web/Program.cs +++ b/src/dotnet/QuantEngine.Web/Program.cs @@ -294,7 +294,9 @@ app.MapPost("/api/auth/login", async (JsonElement payload, HttpContext httpConte }); // Set HTTP-only cookie for server-side authentication - // Note: Secure=true only in production (HTTPS); localhost uses HTTP + Console.WriteLine($"[Auth/Login] Setting cookie 'quant_auth_token'"); + Console.WriteLine($"[Auth/Login] IsHttps: {httpContext.Request.IsHttps}"); + httpContext.Response.Cookies.Append( "quant_auth_token", rawToken, @@ -308,8 +310,11 @@ app.MapPost("/api/auth/login", async (JsonElement payload, HttpContext httpConte } ); + Console.WriteLine($"[Auth/Login] Cookie append completed"); + Console.WriteLine($"[Auth/Login] Response headers count: {httpContext.Response.Headers.Count}"); + // Also return token for localStorage backup (for SPA navigation) - return Results.Ok(new + var result = Results.Ok(new { success = true, username = account.Username, @@ -317,6 +322,9 @@ app.MapPost("/api/auth/login", async (JsonElement payload, HttpContext httpConte accessToken = rawToken, expiresAt = expiresAt.ToString("O") }); + + Console.WriteLine($"[Auth/Login] About to return 200 OK response"); + return result; }).DisableAntiforgery(); app.MapGet("/api/auth/me", async (HttpContext context, IWorkspaceRepository workspaceRepo) =>