using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; 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))] [SwaggerResponse(400, "잘못된 요청 또는 동의 미체크")] [SwaggerResponse(409, "이미 사용 중인 이메일")] public async Task SignupAsync([FromBody] SignupRequestDto request) { var result = await _authService.SignupAsync(request); return Ok(ApiResponse.Success(result, "회원가입 완료. 이메일 인증이 필요합니다.")); } [HttpPost("email/check")] [AllowAnonymous] [SwaggerOperation( Summary = "이메일 중복 체크", Description = "회원가입 전 이메일 사용 가능 여부를 확인합니다.")] [SwaggerResponse(200, "이메일 중복 체크 성공", typeof(ApiResponse))] [SwaggerResponse(400, "잘못된 요청")] public async Task CheckEmailAsync([FromBody] EmailCheckRequestDto request) { var result = await _authService.CheckEmailAsync(request); return Ok(ApiResponse.Success(result)); } [HttpPost("login")] [AllowAnonymous] [SwaggerOperation( Summary = "관리자 로그인", Description = "이메일과 비밀번호로 로그인하여 JWT 토큰을 발급받습니다. " + "응답의 nextAction으로 화면 분기: GO_DASHBOARD(대시보드), VERIFY_EMAIL(이메일 인증 필요). " + "미인증 유저는 verifySessionId와 emailSent가 함께 반환됩니다.")] [SwaggerResponse(200, "로그인 성공", typeof(ApiResponse))] [SwaggerResponse(401, "로그인 실패")] public async Task LoginAsync([FromBody] LoginRequestDto request) { var result = await _authService.LoginAsync(request); return Ok(ApiResponse.Success(result)); } [HttpPost("token/refresh")] [AllowAnonymous] [SwaggerOperation( Summary = "토큰 갱신", Description = "Refresh Token을 사용하여 새로운 Access Token과 Refresh Token을 발급받습니다.")] [SwaggerResponse(200, "토큰 갱신 성공", typeof(ApiResponse))] [SwaggerResponse(401, "유효하지 않거나 만료된 Refresh Token")] public async Task RefreshTokenAsync([FromBody] TokenRefreshRequestDto request) { var result = await _authService.RefreshTokenAsync(request); return Ok(ApiResponse.Success(result)); } [HttpPost("logout")] [Authorize] [SwaggerOperation( Summary = "로그아웃", Description = "현재 로그인된 관리자의 토큰을 무효화합니다. Refresh Token은 DB에서 삭제되고, Access Token은 Redis 블랙리스트에 등록되어 즉시 사용 불가합니다.")] [SwaggerResponse(200, "로그아웃 성공")] [SwaggerResponse(401, "인증되지 않은 요청")] public async Task 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 ", ""); await _authService.LogoutAsync(adminId, accessToken); return Ok(ApiResponse.Success()); } [HttpPost("email/verify")] [AllowAnonymous] [SwaggerOperation( Summary = "이메일 인증", Description = "인증 코드로 이메일을 인증합니다. verify_session_id(권장) 또는 email로 대상을 지정합니다. 5회 실패 시 30분간 차단됩니다.")] [SwaggerResponse(200, "이메일 인증 성공", typeof(ApiResponse))] [SwaggerResponse(400, "인증 코드 불일치 또는 만료")] [SwaggerResponse(429, "시도 횟수 초과")] public async Task VerifyEmailAsync([FromBody] EmailVerifyRequestDto request) { var result = await _authService.VerifyEmailAsync(request); return Ok(ApiResponse.Success(result)); } [HttpPost("email/verify/resend")] [AllowAnonymous] [SwaggerOperation( Summary = "이메일 인증코드 재전송", Description = "인증 세션 ID로 인증코드를 재전송합니다. 재전송 간 60초 쿨다운이 적용되며, 인증코드 유효시간은 5분입니다.")] [SwaggerResponse(200, "재전송 성공", typeof(ApiResponse))] [SwaggerResponse(400, "유효하지 않은 세션")] [SwaggerResponse(429, "재전송 쿨다운 중")] public async Task ResendVerificationAsync([FromBody] EmailResendRequestDto request) { var result = await _authService.ResendVerificationAsync(request); return Ok(ApiResponse.Success(result)); } [HttpPost("password/change")] [Authorize] [SwaggerOperation( Summary = "비밀번호 변경", Description = "현재 로그인된 관리자의 비밀번호를 변경합니다.")] [SwaggerResponse(200, "비밀번호 변경 성공")] [SwaggerResponse(400, "현재 비밀번호 불일치")] [SwaggerResponse(401, "인증되지 않은 요청")] public async Task ChangePasswordAsync([FromBody] ChangePasswordRequestDto request) { var adminIdClaim = User.FindFirst("adminId")?.Value; if (string.IsNullOrEmpty(adminIdClaim) || !long.TryParse(adminIdClaim, out var adminId)) throw SpmsException.Unauthorized("인증 정보가 유효하지 않습니다."); await _authService.ChangePasswordAsync(adminId, request); return Ok(ApiResponse.Success()); } }