Separate auth failures and development settings
TaxBaik CI/CD / build-and-deploy (push) Successful in 3m19s

This commit is contained in:
2026-07-07 23:36:45 +09:00
parent 1109dde72c
commit e79b06ebcd
3 changed files with 59 additions and 25 deletions
@@ -39,18 +39,20 @@ public class LoginEndpoint : Endpoint<LoginRequest, TokenPairResponse>
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);
}
}
+11 -7
View File
@@ -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();
+39 -11
View File
@@ -34,10 +34,10 @@ public class AuthService
_passwordResetToken = configuration["Admin:PasswordResetToken"];
}
public async Task<AuthTokenPair?> AuthenticateAndGenerateTokenPairAsync(string username, string password)
public async Task<AuthResult> 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<AuthTokenPair?> 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);
}