diff --git a/src/dotnet/QuantEngine.Web/Pages/Account/Login.cshtml.cs b/src/dotnet/QuantEngine.Web/Pages/Account/Login.cshtml.cs index 03295a9..86f9267 100644 --- a/src/dotnet/QuantEngine.Web/Pages/Account/Login.cshtml.cs +++ b/src/dotnet/QuantEngine.Web/Pages/Account/Login.cshtml.cs @@ -1,22 +1,26 @@ +using System.Security.Cryptography; +using System.Text; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; +using QuantEngine.Core.Interfaces; +using QuantEngine.Core.Models; namespace QuantEngine.Web.Pages.Account { [AllowAnonymous] public class LoginModel : PageModel { - private readonly HttpClient _httpClient; + private readonly IWorkspaceRepository _workspaceRepo; private readonly ILogger _logger; public string? Username { get; set; } public bool RememberUsername { get; set; } public string? ErrorMessage { get; set; } - public LoginModel(HttpClient httpClient, ILogger logger) + public LoginModel(IWorkspaceRepository workspaceRepo, ILogger logger) { - _httpClient = httpClient; + _workspaceRepo = workspaceRepo; _logger = logger; } @@ -41,51 +45,89 @@ namespace QuantEngine.Web.Pages.Account try { - var loginRequest = new { Username = username, Password = password }; - // Always call the internal API directly to avoid Cloudflare proxy interference. - // Request.Host / Request.Scheme must NOT be used here — they resolve to the external - // domain which causes the self-call to go out through Cloudflare and return 400. - var requestUri = "http://localhost:5265/api/auth/login"; - var response = await _httpClient.PostAsJsonAsync(requestUri, loginRequest); - - if (response.IsSuccessStatusCode) + // Direct repository call — no internal HTTP round-trip. + // Using IWorkspaceRepository injected via DI avoids any port/proxy dependency. + WorkspaceAccount? account = null; + try { - // Extract set-cookie header from API response and set it on proxy client response - if (response.Headers.TryGetValues("Set-Cookie", out var cookieHeaders)) - { - foreach (var cookieHeader in cookieHeaders) - { - Response.Headers.Append("Set-Cookie", cookieHeader); - } - } - - if (rememberUsername) - { - Response.Cookies.Append( - "quant_admin_username", - username, - new Microsoft.AspNetCore.Http.CookieOptions - { - Expires = DateTimeOffset.UtcNow.AddDays(30), - HttpOnly = false, - SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict - } - ); - } - else - { - Response.Cookies.Delete("quant_admin_username"); - } - - return Redirect("/"); + account = await _workspaceRepo.GetAccountByUsernameAsync(username.Trim()); } - else + catch (Exception dbEx) + { + _logger.LogError(dbEx, "[Login] Database lookup failed for user '{Username}'", username); + ErrorMessage = "데이터베이스 연결 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."; + Username = username; + RememberUsername = rememberUsername; + return Page(); + } + + if (account is null || !string.Equals(account.IsActive, "true", StringComparison.OrdinalIgnoreCase)) { ErrorMessage = "로그인 실패: 아이디 또는 비밀번호가 올바르지 않습니다."; Username = username; RememberUsername = rememberUsername; return Page(); } + + var passwordHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(password))); + if (!string.Equals(account.PasswordHash, passwordHash, StringComparison.OrdinalIgnoreCase)) + { + ErrorMessage = "로그인 실패: 아이디 또는 비밀번호가 올바르지 않습니다."; + Username = username; + RememberUsername = rememberUsername; + return Page(); + } + + // Issue session token + var rawToken = Guid.NewGuid().ToString("N"); + var tokenHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(rawToken))); + var now = DateTimeOffset.UtcNow; + var expiresAt = now.AddDays(7); + + await _workspaceRepo.UpsertSessionAsync(new WorkspaceSession + { + SessionTokenHash = tokenHash, + Username = account.Username, + Role = account.Role, + CreatedAt = now.ToString("O"), + ExpiresAt = expiresAt.ToString("O"), + RevokedAt = null + }); + + // Set HTTP-only auth cookie (Secure=true since production is always HTTPS via Cloudflare) + Response.Cookies.Append( + "quant_auth_token", + rawToken, + new Microsoft.AspNetCore.Http.CookieOptions + { + HttpOnly = true, + Secure = true, + SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax, + Expires = expiresAt, + Path = "/" + } + ); + + if (rememberUsername) + { + Response.Cookies.Append( + "quant_admin_username", + username, + new Microsoft.AspNetCore.Http.CookieOptions + { + Expires = DateTimeOffset.UtcNow.AddDays(30), + HttpOnly = false, + SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict + } + ); + } + else + { + Response.Cookies.Delete("quant_admin_username"); + } + + _logger.LogInformation("[Login] User '{Username}' authenticated successfully", account.Username); + return Redirect("/"); } catch (Exception ex) { @@ -99,7 +141,6 @@ namespace QuantEngine.Web.Pages.Account public IActionResult OnGetLogout() { - // Revoke cookies securely Response.Cookies.Delete("quant_auth_token"); return Redirect("/Account/Login"); }