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("로그인 정보가 필요합니다."); ThrowError("로그인 정보가 필요합니다.");
} }
var tokenPair = await _authService.AuthenticateAndGenerateTokenPairAsync(request.Username, request.Password); var authResult = await _authService.AuthenticateAndGenerateTokenPairAsync(request.Username, request.Password);
if (tokenPair == null) if (!authResult.IsSuccess || authResult.TokenPair is null)
{ {
ThrowError("아이디 또는 비밀번호가 올바르지 않습니다."); ThrowError(authResult.FailureReason == AuthFailureReason.DatabaseUnavailable
? "로그인 서비스를 일시적으로 사용할 수 없습니다."
: "아이디 또는 비밀번호가 올바르지 않습니다.");
} }
await SendAsync(new TokenPairResponse await SendAsync(new TokenPairResponse
{ {
Token = tokenPair!.AccessToken, Token = authResult.TokenPair!.AccessToken,
AccessToken = tokenPair.AccessToken, AccessToken = authResult.TokenPair.AccessToken,
RefreshToken = tokenPair.RefreshToken, RefreshToken = authResult.TokenPair.RefreshToken,
ExpiresIn = tokenPair.ExpiresIn ExpiresIn = authResult.TokenPair.ExpiresIn
}, 200, cancellation: ct); }, 200, cancellation: ct);
} }
} }
+11 -7
View File
@@ -49,10 +49,14 @@ public class LoginModel(AuthService authService) : PageModel
return Page(); return Page();
} }
var tokenPair = await authService.AuthenticateAndGenerateTokenPairAsync(Username, Password); var authResult = await authService.AuthenticateAndGenerateTokenPairAsync(Username, Password);
if (tokenPair is null) if (!authResult.IsSuccess || authResult.TokenPair is null)
{ {
ErrorMessage = "아이디 또는 비밀번호가 올바르지 않습니다."; ErrorMessage = authResult.FailureReason switch
{
AuthFailureReason.DatabaseUnavailable => "로그인 서비스를 일시적으로 사용할 수 없습니다. 잠시 후 다시 시도하세요.",
_ => "아이디 또는 비밀번호가 올바르지 않습니다."
};
return Page(); return Page();
} }
@@ -60,10 +64,10 @@ public class LoginModel(AuthService authService) : PageModel
RedirectUrl = NormalizeRedirectUrl(ReturnUrl); RedirectUrl = NormalizeRedirectUrl(ReturnUrl);
TokenPair = new TokenPairResponse TokenPair = new TokenPairResponse
{ {
AccessToken = tokenPair.AccessToken, AccessToken = authResult.TokenPair.AccessToken,
RefreshToken = tokenPair.RefreshToken, RefreshToken = authResult.TokenPair.RefreshToken,
ExpiresIn = tokenPair.ExpiresIn, ExpiresIn = authResult.TokenPair.ExpiresIn,
Token = tokenPair.AccessToken Token = authResult.TokenPair.AccessToken
}; };
return Page(); return Page();
+39 -11
View File
@@ -34,10 +34,10 @@ public class AuthService
_passwordResetToken = configuration["Admin:PasswordResetToken"]; _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)) if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
return null; return AuthResult.InvalidCredentials();
AdminUser? user; AdminUser? user;
try try
@@ -49,17 +49,22 @@ public class AuthService
if (IsLocalE2ETestCredentials(username, password)) if (IsLocalE2ETestCredentials(username, password))
{ {
_logger.LogWarning(ex, "개발 환경에서 DB 없이 로컬 E2E 관리자 로그인 허용: {Username}", username); _logger.LogWarning(ex, "개발 환경에서 DB 없이 로컬 E2E 관리자 로그인 허용: {Username}", username);
return GenerateTokenPair(new AdminUser return AuthResult.Success(GenerateTokenPair(new AdminUser
{ {
Id = 0, Id = 0,
Username = username, Username = username,
PasswordHash = string.Empty, PasswordHash = string.Empty,
CreatedAt = DateTime.UtcNow CreatedAt = DateTime.UtcNow
}); }));
} }
_logger.LogWarning(ex, "개발 환경에서 관리자 로그인 DB 조회 실패: {Username}", username); _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) if (user == null)
@@ -68,35 +73,35 @@ public class AuthService
if (_environment.IsDevelopment() && IsLocalE2ETestCredentials(username, password)) if (_environment.IsDevelopment() && IsLocalE2ETestCredentials(username, password))
{ {
_logger.LogWarning("개발 환경에서 시드 계정 없이 로컬 E2E 관리자 로그인 허용: {Username}", username); _logger.LogWarning("개발 환경에서 시드 계정 없이 로컬 E2E 관리자 로그인 허용: {Username}", username);
return GenerateTokenPair(new AdminUser return AuthResult.Success(GenerateTokenPair(new AdminUser
{ {
Id = 0, Id = 0,
Username = username, Username = username,
PasswordHash = string.Empty, PasswordHash = string.Empty,
CreatedAt = DateTime.UtcNow CreatedAt = DateTime.UtcNow
}); }));
} }
return null; return AuthResult.InvalidCredentials();
} }
if (string.IsNullOrWhiteSpace(user.PasswordHash)) if (string.IsNullOrWhiteSpace(user.PasswordHash))
{ {
_logger.LogError("로그인 실패: 사용자 '{Username}'의 PasswordHash가 비어 있습니다.", username); _logger.LogError("로그인 실패: 사용자 '{Username}'의 PasswordHash가 비어 있습니다.", username);
return null; return AuthResult.DatabaseUnavailable();
} }
if (!BCrypt.Net.BCrypt.Verify(password, user.PasswordHash)) if (!BCrypt.Net.BCrypt.Verify(password, user.PasswordHash))
{ {
_logger.LogWarning("로그인 시도: 잘못된 비밀번호 '{Username}'", username); _logger.LogWarning("로그인 시도: 잘못된 비밀번호 '{Username}'", username);
// 실패한 로그인은 알림하지 않음 (로그만 남김) // 실패한 로그인은 알림하지 않음 (로그만 남김)
return null; return AuthResult.InvalidCredentials();
} }
_logger.LogInformation("로그인 성공: {Username}", username); _logger.LogInformation("로그인 성공: {Username}", username);
// 로그인 알림은 제거 (로그만 남김) // 로그인 알림은 제거 (로그만 남김)
await _adminUserRepository.UpdateLastLoginAtAsync(user.Id); await _adminUserRepository.UpdateLastLoginAtAsync(user.Id);
return GenerateTokenPair(user); return AuthResult.Success(GenerateTokenPair(user));
} }
public async Task<AuthTokenPair?> RefreshAccessTokenAsync(string refreshToken) 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 string RefreshToken { get; } = refreshToken;
public int ExpiresIn { get; } = expiresIn; 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);
}