- LogoutResponseDto 신규 (logged_out, redirect_to 힌트) - LogoutAsync 반환 타입 Task → Task<LogoutResponseDto> - AuthController Swagger 문서에 설정 화면 단일 API 사용 명시 Closes #253
151 lines
7.5 KiB
C#
151 lines
7.5 KiB
C#
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.AspNetCore.RateLimiting;
|
|
using Swashbuckle.AspNetCore.Annotations;
|
|
using SPMS.Application.DTOs.Auth;
|
|
using SPMS.Application.Interfaces;
|
|
using SPMS.Domain.Common;
|
|
using SPMS.Domain.Exceptions;
|
|
|
|
namespace SPMS.API.Controllers;
|
|
|
|
[ApiController]
|
|
[Route("v1/in/auth")]
|
|
[ApiExplorerSettings(GroupName = "auth")]
|
|
public class AuthController : ControllerBase
|
|
{
|
|
private readonly IAuthService _authService;
|
|
|
|
public AuthController(IAuthService authService)
|
|
{
|
|
_authService = authService;
|
|
}
|
|
|
|
[HttpPost("signup")]
|
|
[AllowAnonymous]
|
|
[SwaggerOperation(
|
|
Summary = "회원가입",
|
|
Description = "새로운 관리자 계정을 생성합니다. 약관/개인정보 동의(agreeTerms, agreePrivacy)가 필수이며, 성공 시 이메일 인증 세션(verifySessionId)과 메일 발송 결과(emailSent)를 반환합니다.")]
|
|
[SwaggerResponse(200, "회원가입 성공", typeof(ApiResponse<SignupResponseDto>))]
|
|
[SwaggerResponse(400, "잘못된 요청 또는 동의 미체크")]
|
|
[SwaggerResponse(409, "이미 사용 중인 이메일")]
|
|
public async Task<IActionResult> SignupAsync([FromBody] SignupRequestDto request)
|
|
{
|
|
var result = await _authService.SignupAsync(request);
|
|
return Ok(ApiResponse<SignupResponseDto>.Success(result, "회원가입 완료. 이메일 인증이 필요합니다."));
|
|
}
|
|
|
|
[HttpPost("email/check")]
|
|
[AllowAnonymous]
|
|
[SwaggerOperation(
|
|
Summary = "이메일 중복 체크",
|
|
Description = "회원가입 전 이메일 사용 가능 여부를 확인합니다.")]
|
|
[SwaggerResponse(200, "이메일 중복 체크 성공", typeof(ApiResponse<EmailCheckResponseDto>))]
|
|
[SwaggerResponse(400, "잘못된 요청")]
|
|
public async Task<IActionResult> CheckEmailAsync([FromBody] EmailCheckRequestDto request)
|
|
{
|
|
var result = await _authService.CheckEmailAsync(request);
|
|
return Ok(ApiResponse<EmailCheckResponseDto>.Success(result));
|
|
}
|
|
|
|
[HttpPost("login")]
|
|
[AllowAnonymous]
|
|
[EnableRateLimiting("auth_sensitive")]
|
|
[SwaggerOperation(
|
|
Summary = "관리자 로그인",
|
|
Description = "이메일과 비밀번호로 로그인하여 JWT 토큰을 발급받습니다. " +
|
|
"응답의 nextAction으로 화면 분기: GO_DASHBOARD(대시보드), VERIFY_EMAIL(이메일 인증 필요), CHANGE_PASSWORD(비밀번호 강제변경). " +
|
|
"5회 연속 실패 시 15분간 로그인이 차단됩니다.")]
|
|
[SwaggerResponse(200, "로그인 성공", typeof(ApiResponse<LoginResponseDto>))]
|
|
[SwaggerResponse(401, "로그인 실패")]
|
|
[SwaggerResponse(429, "로그인 시도 횟수 초과")]
|
|
public async Task<IActionResult> LoginAsync([FromBody] LoginRequestDto request)
|
|
{
|
|
var result = await _authService.LoginAsync(request);
|
|
return Ok(ApiResponse<LoginResponseDto>.Success(result));
|
|
}
|
|
|
|
[HttpPost("token/refresh")]
|
|
[AllowAnonymous]
|
|
[SwaggerOperation(
|
|
Summary = "토큰 갱신",
|
|
Description = "Refresh Token을 사용하여 새로운 Access Token과 Refresh Token을 발급받습니다.")]
|
|
[SwaggerResponse(200, "토큰 갱신 성공", typeof(ApiResponse<TokenRefreshResponseDto>))]
|
|
[SwaggerResponse(401, "유효하지 않거나 만료된 Refresh Token")]
|
|
public async Task<IActionResult> RefreshTokenAsync([FromBody] TokenRefreshRequestDto request)
|
|
{
|
|
var result = await _authService.RefreshTokenAsync(request);
|
|
return Ok(ApiResponse<TokenRefreshResponseDto>.Success(result));
|
|
}
|
|
|
|
[HttpPost("logout")]
|
|
[Authorize]
|
|
[SwaggerOperation(
|
|
Summary = "로그아웃",
|
|
Description = "현재 로그인된 관리자의 토큰을 무효화합니다. Refresh Token은 DB에서 삭제되고, Access Token은 Redis 블랙리스트에 등록되어 즉시 사용 불가합니다. " +
|
|
"설정/마이페이지/프로필 등 모든 화면에서 이 단일 API를 사용합니다. 응답의 redirect_to로 이동합니다.")]
|
|
[SwaggerResponse(200, "로그아웃 성공", typeof(ApiResponse<LogoutResponseDto>))]
|
|
[SwaggerResponse(401, "인증되지 않은 요청")]
|
|
public async Task<IActionResult> LogoutAsync()
|
|
{
|
|
var adminIdClaim = User.FindFirst("adminId")?.Value;
|
|
if (string.IsNullOrEmpty(adminIdClaim) || !long.TryParse(adminIdClaim, out var adminId))
|
|
throw SpmsException.Unauthorized("인증 정보가 유효하지 않습니다.");
|
|
|
|
var accessToken = HttpContext.Request.Headers["Authorization"]
|
|
.ToString().Replace("Bearer ", "");
|
|
|
|
var result = await _authService.LogoutAsync(adminId, accessToken);
|
|
return Ok(ApiResponse<LogoutResponseDto>.Success(result));
|
|
}
|
|
|
|
[HttpPost("email/verify")]
|
|
[AllowAnonymous]
|
|
[EnableRateLimiting("auth_sensitive")]
|
|
[SwaggerOperation(
|
|
Summary = "이메일 인증",
|
|
Description = "인증 코드로 이메일을 인증합니다. verify_session_id(권장) 또는 email로 대상을 지정합니다. 5회 실패 시 30분간 차단됩니다.")]
|
|
[SwaggerResponse(200, "이메일 인증 성공", typeof(ApiResponse<EmailVerifyResponseDto>))]
|
|
[SwaggerResponse(400, "인증 코드 불일치 또는 만료")]
|
|
[SwaggerResponse(429, "시도 횟수 초과")]
|
|
public async Task<IActionResult> VerifyEmailAsync([FromBody] EmailVerifyRequestDto request)
|
|
{
|
|
var result = await _authService.VerifyEmailAsync(request);
|
|
return Ok(ApiResponse<EmailVerifyResponseDto>.Success(result));
|
|
}
|
|
|
|
[HttpPost("email/verify/resend")]
|
|
[AllowAnonymous]
|
|
[EnableRateLimiting("auth_sensitive")]
|
|
[SwaggerOperation(
|
|
Summary = "이메일 인증코드 재전송",
|
|
Description = "인증 세션 ID로 인증코드를 재전송합니다. 재전송 간 60초 쿨다운이 적용되며, 인증코드 유효시간은 5분입니다.")]
|
|
[SwaggerResponse(200, "재전송 성공", typeof(ApiResponse<EmailResendResponseDto>))]
|
|
[SwaggerResponse(400, "유효하지 않은 세션")]
|
|
[SwaggerResponse(429, "재전송 쿨다운 중")]
|
|
public async Task<IActionResult> ResendVerificationAsync([FromBody] EmailResendRequestDto request)
|
|
{
|
|
var result = await _authService.ResendVerificationAsync(request);
|
|
return Ok(ApiResponse<EmailResendResponseDto>.Success(result));
|
|
}
|
|
|
|
[HttpPost("password/change")]
|
|
[Authorize]
|
|
[SwaggerOperation(
|
|
Summary = "비밀번호 변경",
|
|
Description = "현재 로그인된 관리자의 비밀번호를 변경합니다. 변경 성공 시 모든 세션이 무효화되며 재로그인이 필요합니다. " +
|
|
"비밀번호 정책: 8자 이상, 영문 대/소문자·숫자·특수문자 각 1자 이상, 현재 비밀번호와 동일 불가.")]
|
|
[SwaggerResponse(200, "비밀번호 변경 성공", typeof(ApiResponse<ChangePasswordResponseDto>))]
|
|
[SwaggerResponse(400, "현재 비밀번호 불일치 / 정책 위반 / 동일 비밀번호 재사용")]
|
|
[SwaggerResponse(401, "인증되지 않은 요청")]
|
|
public async Task<IActionResult> ChangePasswordAsync([FromBody] ChangePasswordRequestDto request)
|
|
{
|
|
var adminIdClaim = User.FindFirst("adminId")?.Value;
|
|
if (string.IsNullOrEmpty(adminIdClaim) || !long.TryParse(adminIdClaim, out var adminId))
|
|
throw SpmsException.Unauthorized("인증 정보가 유효하지 않습니다.");
|
|
|
|
var result = await _authService.ChangePasswordAsync(adminId, request);
|
|
return Ok(ApiResponse<ChangePasswordResponseDto>.Success(result));
|
|
}
|
|
}
|