diff --git a/debug-login.mjs b/debug-login.mjs new file mode 100644 index 0000000..6f613ee --- /dev/null +++ b/debug-login.mjs @@ -0,0 +1,40 @@ +import { chromium } from '@playwright/test'; + +(async () => { + const browser = await chromium.launch(); + const page = await browser.newPage(); + + try { + await page.goto('http://localhost:5265/login'); + + // Fill and submit + await page.fill('input[type="text"]', 'admin'); + await page.fill('input[type="password"]', 'admin'); + await page.click('button[type="submit"]'); + + // Wait a bit for response + await page.waitForTimeout(3000); + + // Check current page content + const content = await page.content(); + + if (content.includes('로그인 실패')) { + console.log('✗ Error shown: Login failed'); + } else if (content.includes('오류 발생')) { + console.log('✗ Error shown: Exception'); + } else if (content.includes('로그인 성공')) { + console.log('✓ Login success message shown'); + } else { + console.log('✓ Form still displayed'); + } + + // Take screenshot + await page.screenshot({ path: './login-attempt.png', fullPage: true }); + console.log('Screenshot saved'); + + } catch (e) { + console.error('Error:', e.message); + } + + await browser.close(); +})(); diff --git a/final-test.mjs b/final-test.mjs new file mode 100644 index 0000000..887bcc7 --- /dev/null +++ b/final-test.mjs @@ -0,0 +1,41 @@ +import { chromium } from '@playwright/test'; + +(async () => { + const browser = await chromium.launch(); + const page = await browser.newPage(); + + try { + console.log('Starting login test...'); + await page.goto('http://localhost:5265/login'); + + // Check page structure + const html = await page.content(); + const hasMenu = html.includes('메뉴') && html.includes('대시보드'); + console.log(hasMenu ? '⚠ Menu visible' : '✓ Clean login page'); + + // Fill and submit + await page.fill('input[type="text"]', 'admin'); + await page.fill('input[type="password"]', 'admin'); + await page.click('button[type="submit"]'); + + console.log('Login submitted, waiting for response...'); + await page.waitForTimeout(2000); + + // Check for errors or success + const content = await page.content(); + if (content.includes('로그인 실패')) { + console.log('✗ Login failed'); + } else if (content.includes('오류')) { + console.log('✗ Error occurred'); + } else { + console.log('✓ Attempting navigation to dashboard...'); + await page.waitForNavigation({ waitUntil: 'load', timeout: 6000 }); + console.log('✓ Navigation complete to: ' + page.url()); + } + + } catch (e) { + console.log('Error/Navigation timeout (expected): ' + e.message.substring(0, 50)); + } + + await browser.close(); +})(); diff --git a/login-attempt.png b/login-attempt.png new file mode 100644 index 0000000..3edbc20 Binary files /dev/null and b/login-attempt.png differ diff --git a/src/dotnet/QuantEngine.Web/Client/Layout/EmptyLayout.razor b/src/dotnet/QuantEngine.Web/Client/Layout/EmptyLayout.razor new file mode 100644 index 0000000..479db95 --- /dev/null +++ b/src/dotnet/QuantEngine.Web/Client/Layout/EmptyLayout.razor @@ -0,0 +1,16 @@ +@inherits LayoutComponentBase + +@Body + + diff --git a/src/dotnet/QuantEngine.Web/Client/Pages/Login.razor b/src/dotnet/QuantEngine.Web/Client/Pages/Login.razor new file mode 100644 index 0000000..ef381ed --- /dev/null +++ b/src/dotnet/QuantEngine.Web/Client/Pages/Login.razor @@ -0,0 +1,343 @@ +@page "/login" +@layout EmptyLayout +@using Microsoft.AspNetCore.Components.Authorization +@using QuantEngine.Web.Client.Infrastructure +@inject HttpClient Http +@inject NavigationManager Navigation +@inject AuthenticationStateProvider AuthStateProvider + +로그인 - QuantEngine + +
+
+ + + @if (!string.IsNullOrEmpty(ErrorMessage)) + { +
+ @ErrorMessage +
+ } + + @if (IsLoading) + { +
+ 로그인 중입니다... +
+ } + + + + +
+
+ + + +@code { + private string Username = ""; + private string Password = ""; + private bool RememberUsername = false; + private string ErrorMessage = ""; + private bool IsLoading = false; + + protected override void OnInitialized() + { + var savedUsername = Console.Out; + } + + private async Task HandleLoginAsync() + { + if (string.IsNullOrWhiteSpace(Username) || string.IsNullOrWhiteSpace(Password)) + { + ErrorMessage = "아이디와 비밀번호를 입력하세요."; + return; + } + + IsLoading = true; + ErrorMessage = ""; + + try + { + var loginRequest = new { username = Username, password = Password }; + var response = await Http.PostAsJsonAsync("/api/auth/login", loginRequest); + + if (response.IsSuccessStatusCode) + { + using var stream = await response.Content.ReadAsStreamAsync(); + using var reader = new System.IO.StreamReader(stream); + var json = await reader.ReadToEndAsync(); + var result = System.Text.Json.JsonSerializer.Deserialize(json); + + if (result != null && !string.IsNullOrEmpty(result.AccessToken)) + { + // Get auth provider and mark as authenticated + if (AuthStateProvider is CustomAuthenticationStateProvider authProvider) + { + await authProvider.MarkUserAsAuthenticatedAsync( + result.Username ?? Username, + result.AccessToken, + result.Role ?? "Admin", + RememberUsername + ); + } + + // Navigate to dashboard + Navigation.NavigateTo("/dashboard", forceLoad: false); + } + else + { + ErrorMessage = "로그인 응답이 올바르지 않습니다."; + IsLoading = false; + } + } + else + { + ErrorMessage = "로그인 실패: 아이디 또는 비밀번호가 올바르지 않습니다."; + IsLoading = false; + } + } + catch (Exception ex) + { + ErrorMessage = $"오류 발생: {ex.Message}"; + IsLoading = false; + } + } + + private 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; } + } +} diff --git a/src/dotnet/QuantEngine.Web/Program.cs b/src/dotnet/QuantEngine.Web/Program.cs index fc09bae..9ddcd78 100644 --- a/src/dotnet/QuantEngine.Web/Program.cs +++ b/src/dotnet/QuantEngine.Web/Program.cs @@ -178,13 +178,13 @@ catch (Exception ex) Log.Warning("Hangfire setup failed: {Message}", ex.Message); } -// Root path - redirect unauthenticated to login.html +// Root path - redirect unauthenticated to /login (Blazor component) app.MapGet("/", async (HttpContext ctx) => { var isAuthenticated = ctx.User?.Identity?.IsAuthenticated ?? false; if (!isAuthenticated) { - ctx.Response.Redirect("/login.html"); + ctx.Response.Redirect("/login"); } else { @@ -194,12 +194,6 @@ app.MapGet("/", async (HttpContext ctx) => await Task.CompletedTask; }); -// Redirect /login to /login.html (static file) -app.MapGet("/login", (HttpContext ctx) => -{ - ctx.Response.Redirect("/login.html", permanent: false); -}); - // Collection API Endpoints (must be before MapRazorComponents) app.MapCollectionEndpoints(); diff --git a/src/dotnet/QuantEngine.Web/wwwroot/login.html b/src/dotnet/QuantEngine.Web/wwwroot/login.html index 88c87d5..b02f4ed 100644 --- a/src/dotnet/QuantEngine.Web/wwwroot/login.html +++ b/src/dotnet/QuantEngine.Web/wwwroot/login.html @@ -294,11 +294,28 @@ }); if (response.ok) { - // 아이디 저장 + const data = await response.json(); + + // 토큰 저장 (Blazor 인증 상태에 필요) + if (data.accessToken) { + localStorage.setItem('quant_admin_access_token', data.accessToken); + } + + // 사용자 정보 저장 + if (data.username) { + localStorage.setItem('quant_admin_username', data.username); + } + + if (data.role) { + localStorage.setItem('quant_admin_role', data.role); + } + + // 아이디 자동입력 설정 저장 if (rememberUsername) { - localStorage.setItem('quant_admin_username', username); + localStorage.setItem('quant_admin_remember_username', 'true'); } else { localStorage.removeItem('quant_admin_username'); + localStorage.setItem('quant_admin_remember_username', 'false'); } // 대시보드로 이동 diff --git a/test-dashboard.png b/test-dashboard.png new file mode 100644 index 0000000..da8a76f Binary files /dev/null and b/test-dashboard.png differ diff --git a/test-login-final.mjs b/test-login-final.mjs new file mode 100644 index 0000000..26feb46 --- /dev/null +++ b/test-login-final.mjs @@ -0,0 +1,42 @@ +import { chromium } from '@playwright/test'; + +(async () => { + const browser = await chromium.launch(); + const page = await browser.newPage({ viewport: { width: 1280, height: 720 } }); + + try { + await page.goto('http://localhost:5265/login'); + console.log('✓ Login page loaded'); + + // Check if Blazor component rendered + const content1 = await page.content(); + if (content1.includes('관리자 아이디')) { + console.log('✓ Blazor login form rendered'); + } + + // Fill credentials + await page.fill('input', 'admin'); + const inputs = await page.$$('input[type="password"]'); + if (inputs.length > 0) { + await inputs[0].fill('admin'); + } + + await page.click('button[type="submit"]'); + console.log('✓ Login submitted'); + + // Wait for navigation + await page.waitForNavigation({ waitUntil: 'networkidle', timeout: 5000 }); + + console.log('✓ Navigation complete'); + console.log(' URL: ' + page.url()); + + // Take screenshot + await page.screenshot({ path: './login-success.png', fullPage: true }); + console.log('✓ Screenshot saved'); + + } catch (e) { + console.error('Error:', e.message); + } + + await browser.close(); +})(); diff --git a/test-login-flow.mjs b/test-login-flow.mjs new file mode 100644 index 0000000..76d7095 --- /dev/null +++ b/test-login-flow.mjs @@ -0,0 +1,44 @@ +import { chromium } from '@playwright/test'; + +(async () => { + try { + const browser = await chromium.launch(); + const page = await browser.newPage({ viewport: { width: 1280, height: 720 } }); + + // Go to login page + await page.goto('http://localhost:5265/login'); + console.log('Loaded login page'); + + // Fill in credentials + await page.fill('input[name="username"]', 'admin'); + await page.fill('input[name="password"]', 'admin'); + + // Submit + await page.click('button[type="submit"]'); + console.log('Submitted login form'); + + // Wait for navigation + await page.waitForNavigation({ waitUntil: 'networkidle' }); + console.log('Page navigated'); + + // Check URL + console.log('Current URL: ' + page.url()); + + // Check content + const content = await page.content(); + if (content.includes('Not Found')) { + console.log('ERROR: Page shows Not Found'); + } else if (content.includes('관리자 대시보드') || content.includes('Dashboard')) { + console.log('SUCCESS: Dashboard loaded'); + } else { + console.log('Other content loaded'); + } + + // Take screenshot + await page.screenshot({ path: './test-dashboard.png', fullPage: true }); + + await browser.close(); + } catch (e) { + console.error('Test failed:', e.message); + } +})(); diff --git a/test-login.mjs b/test-login.mjs new file mode 100644 index 0000000..9e15c7c --- /dev/null +++ b/test-login.mjs @@ -0,0 +1,42 @@ +import { chromium } from '@playwright/test'; + +(async () => { + const browser = await chromium.launch(); + const page = await browser.newPage(); + + try { + await page.goto('http://localhost:5265/login'); + console.log('✓ Loaded login page'); + + // Fill and submit + await page.fill('input[type="text"]', 'admin'); + await page.fill('input[type="password"]', 'admin'); + await page.click('button[type="submit"]'); + console.log('✓ Form submitted'); + + // Wait for page load + await page.waitForNavigation({ waitUntil: 'load', timeout: 10000 }); + + const url = page.url(); + const content = await page.content(); + + console.log('\nResult:'); + console.log(' URL: ' + url); + + if (url.includes('/dashboard') && content.includes('관리자 대시보드')) { + console.log('✓ Dashboard successfully loaded!'); + } else if (content.includes('Not Found')) { + console.log('✗ Error: Not Found'); + } else { + console.log(' Status: Other'); + } + + await page.screenshot({ path: './login-result.png', fullPage: true }); + console.log(' Screenshot: login-result.png'); + + } catch (e) { + console.error('Test failed:', e.message); + } + + await browser.close(); +})();