using Microsoft.Extensions.Options; using SPMS.Application.DTOs.Auth; using SPMS.Application.Interfaces; using SPMS.Application.Settings; using SPMS.Domain.Common; using SPMS.Domain.Entities; using SPMS.Domain.Enums; using SPMS.Domain.Exceptions; using SPMS.Domain.Interfaces; namespace SPMS.Application.Services; public class AuthService : IAuthService { private readonly IAdminRepository _adminRepository; private readonly IUnitOfWork _unitOfWork; private readonly IJwtService _jwtService; private readonly JwtSettings _jwtSettings; public AuthService( IAdminRepository adminRepository, IUnitOfWork unitOfWork, IJwtService jwtService, IOptions jwtSettings) { _adminRepository = adminRepository; _unitOfWork = unitOfWork; _jwtService = jwtService; _jwtSettings = jwtSettings.Value; } public async Task SignupAsync(SignupRequestDto request) { // 1. 이메일 중복 검사 if (await _adminRepository.EmailExistsAsync(request.Email)) { throw new SpmsException( ErrorCodes.Conflict, "이미 사용 중인 이메일입니다.", 409); } // 2. AdminCode 생성 (UUID 12자) var adminCode = Guid.NewGuid().ToString("N")[..12].ToUpper(); // 3. Admin 엔티티 생성 var admin = new Admin { AdminCode = adminCode, Email = request.Email, Password = BCrypt.Net.BCrypt.HashPassword(request.Password), Name = request.Name, Phone = request.Phone, Role = AdminRole.User, EmailVerified = false, CreatedAt = DateTime.UtcNow, IsDeleted = false }; // 4. 저장 await _adminRepository.AddAsync(admin); await _unitOfWork.SaveChangesAsync(); // 5. 응답 반환 return new SignupResponseDto { AdminCode = admin.AdminCode, Email = admin.Email }; } public async Task LoginAsync(LoginRequestDto request) { // 1. 이메일로 관리자 조회 var admin = await _adminRepository.GetByEmailAsync(request.Email); if (admin is null) { throw new SpmsException( ErrorCodes.LoginFailed, "이메일 또는 비밀번호가 일치하지 않습니다.", 401); } // 2. 삭제된 계정 확인 if (admin.IsDeleted) { throw new SpmsException( ErrorCodes.LoginFailed, "이메일 또는 비밀번호가 일치하지 않습니다.", 401); } // 3. 비밀번호 검증 (BCrypt) if (!BCrypt.Net.BCrypt.Verify(request.Password, admin.Password)) { throw new SpmsException( ErrorCodes.LoginFailed, "이메일 또는 비밀번호가 일치하지 않습니다.", 401); } // 4. 토큰 생성 var accessToken = _jwtService.GenerateAccessToken( admin.Id, admin.Role.ToString()); var refreshToken = _jwtService.GenerateRefreshToken(); // 5. Refresh Token 및 최종 로그인 시간 저장 admin.RefreshToken = refreshToken; admin.RefreshTokenExpiresAt = DateTime.UtcNow.AddDays(_jwtSettings.RefreshTokenExpiryDays); admin.LastLoginAt = DateTime.UtcNow; _adminRepository.Update(admin); await _unitOfWork.SaveChangesAsync(); // 6. 응답 반환 return new LoginResponseDto { AccessToken = accessToken, RefreshToken = refreshToken, ExpiresIn = _jwtSettings.ExpiryMinutes * 60, Admin = new AdminInfoDto { AdminCode = admin.AdminCode, Email = admin.Email, Name = admin.Name, Role = admin.Role.ToString() } }; } public async Task RefreshTokenAsync(TokenRefreshRequestDto request) { // 1. Refresh Token으로 관리자 조회 var admin = await _adminRepository.GetByRefreshTokenAsync(request.RefreshToken); if (admin is null) { throw new SpmsException( ErrorCodes.Unauthorized, "유효하지 않은 Refresh Token입니다.", 401); } // 2. Refresh Token 만료 확인 if (admin.RefreshTokenExpiresAt < DateTime.UtcNow) { throw new SpmsException( ErrorCodes.Unauthorized, "Refresh Token이 만료되었습니다. 다시 로그인해주세요.", 401); } // 3. 새 토큰 생성 var newAccessToken = _jwtService.GenerateAccessToken( admin.Id, admin.Role.ToString()); var newRefreshToken = _jwtService.GenerateRefreshToken(); // 4. 새 Refresh Token 저장 (Rotation) admin.RefreshToken = newRefreshToken; admin.RefreshTokenExpiresAt = DateTime.UtcNow.AddDays(_jwtSettings.RefreshTokenExpiryDays); _adminRepository.Update(admin); await _unitOfWork.SaveChangesAsync(); // 5. 응답 반환 return new TokenRefreshResponseDto { AccessToken = newAccessToken, RefreshToken = newRefreshToken, ExpiresIn = _jwtSettings.ExpiryMinutes * 60 }; } public async Task LogoutAsync(long adminId) { // 1. 관리자 조회 var admin = await _adminRepository.GetByIdAsync(adminId); if (admin is null) { throw new SpmsException( ErrorCodes.NotFound, "관리자를 찾을 수 없습니다.", 404); } // 2. Refresh Token 무효화 admin.RefreshToken = null; admin.RefreshTokenExpiresAt = null; _adminRepository.Update(admin); await _unitOfWork.SaveChangesAsync(); } public async Task ChangePasswordAsync(long adminId, ChangePasswordRequestDto request) { // 1. 관리자 조회 var admin = await _adminRepository.GetByIdAsync(adminId); if (admin is null) { throw new SpmsException( ErrorCodes.NotFound, "관리자를 찾을 수 없습니다.", 404); } // 2. 현재 비밀번호 검증 if (!BCrypt.Net.BCrypt.Verify(request.CurrentPassword, admin.Password)) { throw new SpmsException( ErrorCodes.PasswordValidationFailed, "현재 비밀번호가 일치하지 않습니다.", 400); } // 3. 새 비밀번호 해싱 및 저장 admin.Password = BCrypt.Net.BCrypt.HashPassword(request.NewPassword); _adminRepository.Update(admin); await _unitOfWork.SaveChangesAsync(); } }