fix: enable WASM-first auth flow for dashboard

Key changes:
- Add @rendermode InteractiveWebAssembly to Dashboard.razor
  (CLAUDE.md mandates Interactive WebAssembly as default)
- Reorder MapRazorComponents: WebAssembly first, then Server
- Simplify login.html: just redirect after 2s (no fetch verification)

Root cause of 302 redirect loop:
- Dashboard was rendering server-side (no @rendermode specified)
- Server-side rendering can't access localStorage
- CustomAuthenticationStateProvider read empty token
- Dashboard redirected to /login
- Result: 302 loop

Solution: Force client-side WASM rendering so:
1. Blazor WASM loads in browser
2. CustomAuthenticationStateProvider accesses localStorage
3. Token is read from localStorage
4. /api/auth/me validates token
5. User is authenticated
6. Dashboard displays

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 01:38:09 +09:00
parent 53ae2fcc51
commit eae0a68f06
9 changed files with 272 additions and 7 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

+127
View File
@@ -0,0 +1,127 @@
import { chromium } from "@playwright/test";
(async () => {
console.log("════════════════════════════════════════════════════════");
console.log(" 🔐 COMPLETE LOGIN FLOW TEST");
console.log("════════════════════════════════════════════════════════\n");
const b = await chromium.launch({ headless: false });
const p = await b.newPage();
// 모든 콘솔 로그 캡처
const consoleLogs = [];
p.on("console", msg => {
const text = msg.text();
consoleLogs.push(text);
if (text.includes("[Login]") || text.includes("[Dashboard]") || text.includes("[Auth]")) {
console.log(` 📝 ${text}`);
}
});
// 요청/응답 모니터링
p.on("response", res => {
if (res.url().includes("auth") || res.url().includes("dashboard")) {
console.log(` 📡 ${res.status()} ${res.url().split('/').pop()}`);
}
});
try {
// 서버 준비 확인
let serverReady = false;
for (let attempt = 0; attempt < 5; attempt++) {
try {
const resp = await fetch("http://localhost:5265/login.html");
if (resp.ok) {
serverReady = true;
break;
}
} catch (e) {}
console.log(` [대기] 서버 시작 확인 중... (${attempt + 1}/5)`);
await new Promise(r => setTimeout(r, 5000));
}
if (!serverReady) {
console.log(" ❌ 서버가 시작되지 않음");
await b.close();
return;
}
console.log("\n✅ 서버 준비 완료!\n");
// STEP 1: 로그인 페이지 로드
console.log("1️⃣ 로그인 페이지 로드");
await p.goto("http://localhost:5265/login.html", { waitUntil: "networkidle" });
console.log(" ✓ 페이지 로드됨\n");
// STEP 2: 폼 입력
console.log("2️⃣ 로그인 폼 입력 (admin/admin)");
await p.fill("input[name='username']", "admin");
await p.fill("input[name='password']", "admin");
console.log(" ✓ 입력 완료\n");
// STEP 3: 로그인 제출
console.log("3️⃣ 로그인 버튼 클릭");
await p.click("button[type='submit']");
console.log(" ✓ 클릭됨\n");
// STEP 4: 상태 모니터링 (10초)
console.log("4️⃣ 로그인 처리 모니터링 (10초):");
let redirected = false;
for (let i = 1; i <= 10; i++) {
await new Promise(r => setTimeout(r, 1000));
const url = p.url();
const title = await p.title();
process.stdout.write(` [${i}s] URL: ${url}`);
if (!url.includes("login")) {
console.log(" ✅ REDIRECTED!");
redirected = true;
break;
} else {
console.log("");
}
}
console.log("\n5️⃣ 최종 상태:");
const finalUrl = p.url();
const finalTitle = await p.title();
console.log(` 📍 URL: ${finalUrl}`);
console.log(` 📄 Page Title: ${finalTitle}`);
if (finalUrl.includes("/dashboard")) {
console.log(" ✅ 대시보드 URL 확인됨!");
const content = await p.content();
if (content.includes("관리자 대시보드")) {
console.log(" ✅ 대시보드 콘텐츠 확인됨!");
console.log("\n🎉 로그인 성공! 대시보드 정상 로드!\n");
} else if (content.includes("Not Found")) {
console.log(" ❌ Not Found 에러");
} else {
console.log(" ⚠️ 대시보드 콘텐츠 미확인");
}
} else if (finalUrl.includes("/login")) {
console.log(" ❌ 다시 로그인 페이지로 리다이렉트됨");
console.log(" → 대시보드 인증 체크에서 실패한 것 같습니다");
} else if (finalUrl.includes("/not-found")) {
console.log(" ❌ /not-found 에러");
} else {
console.log(" ⚠️ 예상치 못한 페이지");
}
// 스크린샷
await p.screenshot({ path: "./direct-test-result.png", fullPage: true });
console.log(" 📷 스크린샷: direct-test-result.png");
console.log("\n════════════════════════════════════════════════════════");
console.log(" 테스트 완료");
console.log("════════════════════════════════════════════════════════");
} catch (e) {
console.error("❌ 테스트 에러:", e.message);
} finally {
await b.close();
}
})();
Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

@@ -1,4 +1,5 @@
@page "/dashboard" @page "/dashboard"
@rendermode InteractiveWebAssembly
@using QuantEngine.Core.Infrastructure @using QuantEngine.Core.Infrastructure
@using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Authorization
@inject HttpClient Http @inject HttpClient Http
@@ -244,13 +245,18 @@
{ {
// Check authentication // Check authentication
var authState = await AuthStateProvider.GetAuthenticationStateAsync(); var authState = await AuthStateProvider.GetAuthenticationStateAsync();
Console.WriteLine($"[Dashboard] Auth state received. IsAuthenticated: {authState.User.Identity?.IsAuthenticated}");
if (!authState.User.Identity?.IsAuthenticated ?? true) if (!authState.User.Identity?.IsAuthenticated ?? true)
{ {
// Not authenticated - redirect to login // Not authenticated - redirect to login
Console.WriteLine("[Dashboard] Not authenticated. Redirecting to login...");
NavManager.NavigateTo("/login.html", forceLoad: true); NavManager.NavigateTo("/login.html", forceLoad: true);
return; return;
} }
Console.WriteLine($"[Dashboard] Authenticated as: {authState.User.Identity?.Name}");
try try
{ {
// Load operational report // Load operational report
+1 -1
View File
@@ -496,8 +496,8 @@ app.MapRazorPages();
// Map Blazor Components - catches all remaining routes // Map Blazor Components - catches all remaining routes
app.MapRazorComponents<App>() app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode()
.AddInteractiveWebAssemblyRenderMode() .AddInteractiveWebAssemblyRenderMode()
.AddInteractiveServerRenderMode()
.AddAdditionalAssemblies(typeof(QuantEngine.Web.Client._Imports).Assembly); .AddAdditionalAssemblies(typeof(QuantEngine.Web.Client._Imports).Assembly);
app.Run(); app.Run();
+13 -6
View File
@@ -302,16 +302,18 @@
// 토큰 저장 (Blazor 인증 상태에 필요) // 토큰 저장 (Blazor 인증 상태에 필요)
if (data.accessToken) { if (data.accessToken) {
localStorage.setItem('quant_admin_access_token', data.accessToken); localStorage.setItem('quant_admin_access_token', data.accessToken);
console.log('[Login] Access token saved to localStorage'); console.log('[Login] Access token saved to localStorage: ' + data.accessToken.substring(0, 16) + '...');
} }
// 사용자 정보 저장 // 사용자 정보 저장
if (data.username) { if (data.username) {
localStorage.setItem('quant_admin_username', data.username); localStorage.setItem('quant_admin_username', data.username);
console.log('[Login] Username saved: ' + data.username);
} }
if (data.role) { if (data.role) {
localStorage.setItem('quant_admin_role', data.role); localStorage.setItem('quant_admin_role', data.role);
console.log('[Login] Role saved: ' + data.role);
} }
// 아이디 자동입력 설정 저장 // 아이디 자동입력 설정 저장
@@ -324,15 +326,20 @@
// 로그인 성공 메시지 표시 // 로그인 성공 메시지 표시
alertDiv.className = 'alert alert-success show'; alertDiv.className = 'alert alert-success show';
alertDiv.textContent = '로그인 성공! 대시보드로 이동합니다.'; alertDiv.textContent = '로그인 성공! 대시보드로 이동합니다...';
// Blazor 앱이 로드될 시간 제공 // localStorage 확인
// (대시보드는 이제 [Authorize] 없이 로드되고, 내부에서 인증 체크) const savedToken = localStorage.getItem('quant_admin_access_token');
console.log('[Login] Token saved. Redirecting in 3 seconds...'); console.log('[Login] Verifying localStorage - Token exists: ' + (savedToken ? 'YES' : 'NO'));
// Blazor WASM 앱이 로드될 시간 제공
// Dashboard는 이제 @rendermode InteractiveWebAssembly로 설정되어
// CustomAuthenticationStateProvider가 localStorage에서 토큰을 읽을 수 있습니다
console.log('[Login] Redirecting to dashboard in 2 seconds...');
setTimeout(() => { setTimeout(() => {
console.log('[Login] Navigating to dashboard'); console.log('[Login] Navigating to dashboard');
window.location.href = '/dashboard'; window.location.href = '/dashboard';
}, 3000); }, 2000);
} else { } else {
const data = await response.json(); const data = await response.json();
alertDiv.className = 'alert alert-error show'; alertDiv.className = 'alert alert-error show';
+57
View File
@@ -0,0 +1,57 @@
import { chromium } from "@playwright/test";
(async () => {
console.log("🔐 FINAL TEST - Server-side Token Verification\n");
const b = await chromium.launch({ headless: false });
const p = await b.newPage();
const consoleLogs = [];
p.on("console", msg => {
const text = msg.text();
consoleLogs.push(text);
if (text.includes("[Login]")) {
console.log(" 📝 " + text);
}
});
try {
console.log("1. Loading login page...");
await p.goto("http://localhost:5265/login.html", { waitUntil: "networkidle" });
console.log("2. Submitting login...");
await p.fill("input[name='username']", "admin");
await p.fill("input[name='password']", "admin");
await p.click("button[type='submit']");
console.log("3. Monitoring (15 seconds)...\n");
for (let i = 1; i <= 15; i++) {
await new Promise(r => setTimeout(r, 1000));
const url = p.url();
if (!url.includes("login")) {
console.log(`\n ✅ [${i}s] Redirected to: ${url}`);
break;
}
}
const finalUrl = p.url();
console.log(`\n📍 Final URL: ${finalUrl}`);
if (finalUrl.includes("/dashboard")) {
console.log("✅✅✅ SUCCESS! User is on dashboard!\n");
const content = await p.content();
if (content.includes("관리자 대시보드")) {
console.log("✅ Dashboard content confirmed!");
}
} else {
console.log("❌ Still at: " + finalUrl);
}
await p.screenshot({ path: "./final-success-test.png" });
} catch (e) {
console.error("Error:", e.message);
}
await b.close();
})();
Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

+68
View File
@@ -0,0 +1,68 @@
import { chromium } from "@playwright/test";
(async () => {
console.log("════════════════════════════════════════════════════════");
console.log(" ✅ FINAL TEST - @rendermode InteractiveWebAssembly");
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("[") && text.includes("]")) {
console.log(" 📝 " + text);
}
});
try {
console.log("1️⃣ 로그인 페이지 로드");
await p.goto("http://localhost:5265/login.html");
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️⃣ 모니터링 (10초)\n");
let redirected = false;
for (let i = 1; i <= 10; 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}`);
redirected = true;
break;
}
process.stdout.write(` [${i}s] ${url}\r`);
}
const finalUrl = p.url();
if (finalUrl.includes("/dashboard")) {
console.log("\n\n✅✅✅ 성공! 대시보드에 도착했습니다!\n");
// 페이지 콘텐츠 확인
await new Promise(r => setTimeout(r, 2000));
const content = await p.content();
if (content.includes("관리자 대시보드")) {
console.log("✅ 대시보드 콘텐츠 확인됨!\n");
console.log("🎉 로그인 완성! 인증 흐름 정상 작동!\n");
} else {
console.log("⚠️ 대시보드 콘텐츠 미확인\n");
}
} else {
console.log(`\n❌ 아직도 ${finalUrl}에 있습니다\n`);
}
await p.screenshot({ path: "./ultimate-test-result.png", fullPage: true });
console.log("📷 스크린샷: ultimate-test-result.png");
} catch (e) {
console.error("❌ Error:", e.message);
}
await b.close();
})();