Separate auth failures and development settings
TaxBaik CI/CD / build-and-deploy (push) Successful in 3m19s
TaxBaik CI/CD / build-and-deploy (push) Successful in 3m19s
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user