diff --git a/src/TaxBaik.Web/Endpoints/Auth/LoginEndpoint.cs b/src/TaxBaik.Web/Endpoints/Auth/LoginEndpoint.cs index f397d43..fa2f678 100644 --- a/src/TaxBaik.Web/Endpoints/Auth/LoginEndpoint.cs +++ b/src/TaxBaik.Web/Endpoints/Auth/LoginEndpoint.cs @@ -39,18 +39,20 @@ public class LoginEndpoint : Endpoint ThrowError("로그인 정보가 필요합니다."); } - var tokenPair = await _authService.AuthenticateAndGenerateTokenPairAsync(request.Username, request.Password); - if (tokenPair == null) + var authResult = await _authService.AuthenticateAndGenerateTokenPairAsync(request.Username, request.Password); + if (!authResult.IsSuccess || authResult.TokenPair is null) { - ThrowError("아이디 또는 비밀번호가 올바르지 않습니다."); + ThrowError(authResult.FailureReason == AuthFailureReason.DatabaseUnavailable + ? "로그인 서비스를 일시적으로 사용할 수 없습니다." + : "아이디 또는 비밀번호가 올바르지 않습니다."); } await SendAsync(new TokenPairResponse { - Token = tokenPair!.AccessToken, - AccessToken = tokenPair.AccessToken, - RefreshToken = tokenPair.RefreshToken, - ExpiresIn = tokenPair.ExpiresIn + Token = authResult.TokenPair!.AccessToken, + AccessToken = authResult.TokenPair.AccessToken, + RefreshToken = authResult.TokenPair.RefreshToken, + ExpiresIn = authResult.TokenPair.ExpiresIn }, 200, cancellation: ct); } } diff --git a/src/TaxBaik.Web/Pages/Admin/Login.cshtml.cs b/src/TaxBaik.Web/Pages/Admin/Login.cshtml.cs index 57ef7f7..73f60fe 100644 --- a/src/TaxBaik.Web/Pages/Admin/Login.cshtml.cs +++ b/src/TaxBaik.Web/Pages/Admin/Login.cshtml.cs @@ -49,10 +49,14 @@ public class LoginModel(AuthService authService) : PageModel return Page(); } - var tokenPair = await authService.AuthenticateAndGenerateTokenPairAsync(Username, Password); - if (tokenPair is null) + var authResult = await authService.AuthenticateAndGenerateTokenPairAsync(Username, Password); + if (!authResult.IsSuccess || authResult.TokenPair is null) { - ErrorMessage = "아이디 또는 비밀번호가 올바르지 않습니다."; + ErrorMessage = authResult.FailureReason switch + { + AuthFailureReason.DatabaseUnavailable => "로그인 서비스를 일시적으로 사용할 수 없습니다. 잠시 후 다시 시도하세요.", + _ => "아이디 또는 비밀번호가 올바르지 않습니다." + }; return Page(); } @@ -60,10 +64,10 @@ public class LoginModel(AuthService authService) : PageModel RedirectUrl = NormalizeRedirectUrl(ReturnUrl); TokenPair = new TokenPairResponse { - AccessToken = tokenPair.AccessToken, - RefreshToken = tokenPair.RefreshToken, - ExpiresIn = tokenPair.ExpiresIn, - Token = tokenPair.AccessToken + AccessToken = authResult.TokenPair.AccessToken, + RefreshToken = authResult.TokenPair.RefreshToken, + ExpiresIn = authResult.TokenPair.ExpiresIn, + Token = authResult.TokenPair.AccessToken }; return Page(); diff --git a/src/TaxBaik.Web/Services/AuthService.cs b/src/TaxBaik.Web/Services/AuthService.cs index a929913..23badc0 100644 --- a/src/TaxBaik.Web/Services/AuthService.cs +++ b/src/TaxBaik.Web/Services/AuthService.cs @@ -34,10 +34,10 @@ public class AuthService _passwordResetToken = configuration["Admin:PasswordResetToken"]; } - public async Task AuthenticateAndGenerateTokenPairAsync(string username, string password) + public async Task AuthenticateAndGenerateTokenPairAsync(string username, string password) { if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) - return null; + return AuthResult.InvalidCredentials(); AdminUser? user; try @@ -49,17 +49,22 @@ public class AuthService if (IsLocalE2ETestCredentials(username, password)) { _logger.LogWarning(ex, "개발 환경에서 DB 없이 로컬 E2E 관리자 로그인 허용: {Username}", username); - return GenerateTokenPair(new AdminUser + return AuthResult.Success(GenerateTokenPair(new AdminUser { Id = 0, Username = username, PasswordHash = string.Empty, CreatedAt = DateTime.UtcNow - }); + })); } _logger.LogWarning(ex, "개발 환경에서 관리자 로그인 DB 조회 실패: {Username}", username); - return null; + return AuthResult.DatabaseUnavailable(); + } + catch (Exception ex) + { + _logger.LogError(ex, "관리자 로그인 중 예상치 못한 오류: {Username}", username); + return AuthResult.DatabaseUnavailable(); } if (user == null) @@ -68,35 +73,35 @@ public class AuthService if (_environment.IsDevelopment() && IsLocalE2ETestCredentials(username, password)) { _logger.LogWarning("개발 환경에서 시드 계정 없이 로컬 E2E 관리자 로그인 허용: {Username}", username); - return GenerateTokenPair(new AdminUser + return AuthResult.Success(GenerateTokenPair(new AdminUser { Id = 0, Username = username, PasswordHash = string.Empty, CreatedAt = DateTime.UtcNow - }); + })); } - return null; + return AuthResult.InvalidCredentials(); } if (string.IsNullOrWhiteSpace(user.PasswordHash)) { _logger.LogError("로그인 실패: 사용자 '{Username}'의 PasswordHash가 비어 있습니다.", username); - return null; + return AuthResult.DatabaseUnavailable(); } if (!BCrypt.Net.BCrypt.Verify(password, user.PasswordHash)) { _logger.LogWarning("로그인 시도: 잘못된 비밀번호 '{Username}'", username); // 실패한 로그인은 알림하지 않음 (로그만 남김) - return null; + return AuthResult.InvalidCredentials(); } _logger.LogInformation("로그인 성공: {Username}", username); // 로그인 알림은 제거 (로그만 남김) await _adminUserRepository.UpdateLastLoginAtAsync(user.Id); - return GenerateTokenPair(user); + return AuthResult.Success(GenerateTokenPair(user)); } public async Task RefreshAccessTokenAsync(string refreshToken) @@ -271,3 +276,26 @@ public class AuthTokenPair(string accessToken, string refreshToken, int expiresI public string RefreshToken { get; } = refreshToken; public int ExpiresIn { get; } = expiresIn; } + +public enum AuthFailureReason +{ + InvalidCredentials, + DatabaseUnavailable +} + +public sealed class AuthResult +{ + private AuthResult(AuthTokenPair? tokenPair, AuthFailureReason? failureReason) + { + TokenPair = tokenPair; + FailureReason = failureReason; + } + + public AuthTokenPair? TokenPair { get; } + public AuthFailureReason? FailureReason { get; } + public bool IsSuccess => TokenPair is not null; + + public static AuthResult Success(AuthTokenPair tokenPair) => new(tokenPair, null); + public static AuthResult InvalidCredentials() => new(null, AuthFailureReason.InvalidCredentials); + public static AuthResult DatabaseUnavailable() => new(null, AuthFailureReason.DatabaseUnavailable); +}