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("로그인 정보가 필요합니다.");
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user