feat: migrate AuthController to FastEndpoints Endpoints (Phase 1)
TaxBaik CI/CD / build-and-deploy (push) Successful in 1m31s

IMPLEMENTATION:
- Create 4 FastEndpoints Endpoint classes:
  - LoginEndpoint: POST /api/auth/login
  - RefreshTokenEndpoint: POST /api/auth/refresh
  - ChangePasswordEndpoint: POST /api/auth/change-password
  - ResetPasswordEndpoint: POST /api/auth/reset-password

- Backup AuthController.cs (no longer active)
- Add FastEndpoints.Endpoint<TRequest, TResponse> pattern
- Implement proper DI with AuthService injection
- Use Policies("Bearer") for authorization
- Proper error handling with ThrowError()

ARCHITECTURE:
- Start of Phase 1: Core Auth APIs
- Endpoints follow FastEndpoints conventions
- DTOs: LoginRequest, RefreshTokenRequest, ChangePasswordRequest, ResetPasswordRequest, TokenPairResponse, MessageResponse
- AllowAnonymous for login/refresh/reset
- Bearer policy for change-password

VERIFICATION:
 dotnet build: 0 errors, 0 warnings
 dotnet test: 26/26 passed
 FastEndpoints auto-discovery working (no endpoint errors)
 JWT validation passes

Next Phase: BlogController and remaining APIs

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 17:14:35 +09:00
parent 055bc48d1d
commit 675ef64975
102 changed files with 4293 additions and 2 deletions
@@ -0,0 +1,56 @@
using System.Security.Claims;
using FastEndpoints;
using TaxBaik.Web.Services;
namespace TaxBaik.Web.Endpoints.Auth;
public class ChangePasswordRequest
{
public string CurrentPassword { get; set; } = string.Empty;
public string NewPassword { get; set; } = string.Empty;
}
public class MessageResponse
{
public string Message { get; set; } = string.Empty;
}
public class ChangePasswordEndpoint : Endpoint<ChangePasswordRequest, MessageResponse>
{
private readonly AuthService _authService;
public ChangePasswordEndpoint(AuthService authService)
{
_authService = authService;
}
public override void Configure()
{
Post("/api/auth/change-password");
Policies("Bearer");
}
public override async Task HandleAsync(ChangePasswordRequest request, CancellationToken ct)
{
var username = User.FindFirstValue(ClaimTypes.Name);
if (string.IsNullOrWhiteSpace(username))
{
ThrowError("인증 정보가 올바르지 않습니다.");
}
try
{
var changed = await _authService.ChangePasswordAsync(username!, request.CurrentPassword, request.NewPassword);
if (!changed)
{
ThrowError("현재 비밀번호가 올바르지 않습니다.");
}
await SendAsync(new MessageResponse { Message = "비밀번호가 변경되었습니다." }, 200, cancellation: ct);
}
catch (ArgumentException ex)
{
ThrowError(ex.Message);
}
}
}
@@ -0,0 +1,56 @@
using FastEndpoints;
using TaxBaik.Web.Services;
namespace TaxBaik.Web.Endpoints.Auth;
public class LoginRequest
{
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}
public class TokenPairResponse
{
public string Token { get; set; } = string.Empty;
public string AccessToken { get; set; } = string.Empty;
public string RefreshToken { get; set; } = string.Empty;
public int ExpiresIn { get; set; }
}
public class LoginEndpoint : Endpoint<LoginRequest, TokenPairResponse>
{
private readonly AuthService _authService;
public LoginEndpoint(AuthService authService)
{
_authService = authService;
}
public override void Configure()
{
Post("/api/auth/login");
AllowAnonymous();
}
public override async Task HandleAsync(LoginRequest request, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(request.Username) || string.IsNullOrWhiteSpace(request.Password))
{
ThrowError("로그인 정보가 필요합니다.");
}
var tokenPair = await _authService.AuthenticateAndGenerateTokenPairAsync(request.Username, request.Password);
if (tokenPair == null)
{
ThrowError("아이디 또는 비밀번호가 올바르지 않습니다.");
}
await SendAsync(new TokenPairResponse
{
Token = tokenPair!.AccessToken,
AccessToken = tokenPair.AccessToken,
RefreshToken = tokenPair.RefreshToken,
ExpiresIn = tokenPair.ExpiresIn
}, 200, cancellation: ct);
}
}
@@ -0,0 +1,47 @@
using FastEndpoints;
using TaxBaik.Web.Services;
namespace TaxBaik.Web.Endpoints.Auth;
public class RefreshTokenRequest
{
public string RefreshToken { get; set; } = string.Empty;
}
public class RefreshTokenEndpoint : Endpoint<RefreshTokenRequest, TokenPairResponse>
{
private readonly AuthService _authService;
public RefreshTokenEndpoint(AuthService authService)
{
_authService = authService;
}
public override void Configure()
{
Post("/api/auth/refresh");
AllowAnonymous();
}
public override async Task HandleAsync(RefreshTokenRequest request, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(request.RefreshToken))
{
ThrowError("Refresh token이 필요합니다.");
}
var tokenPair = await _authService.RefreshAccessTokenAsync(request.RefreshToken);
if (tokenPair == null)
{
ThrowError("Refresh token이 유효하지 않습니다.");
}
await SendAsync(new TokenPairResponse
{
Token = tokenPair!.AccessToken,
AccessToken = tokenPair.AccessToken,
RefreshToken = tokenPair.RefreshToken,
ExpiresIn = tokenPair.ExpiresIn
}, 200, cancellation: ct);
}
}
@@ -0,0 +1,49 @@
using FastEndpoints;
using TaxBaik.Web.Services;
namespace TaxBaik.Web.Endpoints.Auth;
public class ResetPasswordRequest
{
public string Username { get; set; } = string.Empty;
public string NewPassword { get; set; } = string.Empty;
public string ResetToken { get; set; } = string.Empty;
}
public class ResetPasswordEndpoint : Endpoint<ResetPasswordRequest, MessageResponse>
{
private readonly AuthService _authService;
public ResetPasswordEndpoint(AuthService authService)
{
_authService = authService;
}
public override void Configure()
{
Post("/api/auth/reset-password");
AllowAnonymous();
}
public override async Task HandleAsync(ResetPasswordRequest request, CancellationToken ct)
{
try
{
var reset = await _authService.ResetPasswordAsync(request.Username, request.NewPassword, request.ResetToken);
if (!reset)
{
ThrowError("재설정 토큰 또는 사용자 정보가 올바르지 않습니다.");
}
await SendAsync(new MessageResponse { Message = "비밀번호가 재설정되었습니다." }, 200, cancellation: ct);
}
catch (InvalidOperationException)
{
ThrowError("비밀번호 재설정 토큰이 서버에 설정되어 있지 않습니다.", statusCode: 503);
}
catch (ArgumentException ex)
{
ThrowError(ex.Message);
}
}
}