using Microsoft.Extensions.Options; using SPMS.Application.DTOs.Account; 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; private readonly ITokenStore _tokenStore; private readonly IEmailService _emailService; public AuthService( IAdminRepository adminRepository, IUnitOfWork unitOfWork, IJwtService jwtService, IOptions jwtSettings, ITokenStore tokenStore, IEmailService emailService) { _adminRepository = adminRepository; _unitOfWork = unitOfWork; _jwtService = jwtService; _jwtSettings = jwtSettings.Value; _tokenStore = tokenStore; _emailService = emailService; } 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. 이메일 인증 코드 생성/저장/발송 var verificationCode = Random.Shared.Next(100000, 999999).ToString(); await _tokenStore.StoreAsync( $"email_verify:{request.Email}", verificationCode, TimeSpan.FromHours(1)); await _emailService.SendVerificationCodeAsync(request.Email, verificationCode); // 6. 응답 반환 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(); } public async Task CheckEmailAsync(EmailCheckRequestDto request) { var exists = await _adminRepository.EmailExistsAsync(request.Email); return new EmailCheckResponseDto { Email = request.Email, IsAvailable = !exists }; } public async Task VerifyEmailAsync(EmailVerifyRequestDto request) { var admin = await _adminRepository.GetByEmailAsync(request.Email); if (admin is null) { throw new SpmsException( ErrorCodes.VerificationCodeError, "인증 코드가 유효하지 않습니다.", 400); } if (admin.EmailVerified) { throw new SpmsException( ErrorCodes.VerificationCodeError, "이미 인증된 이메일입니다.", 400); } var storedCode = await _tokenStore.GetAsync($"email_verify:{request.Email}"); if (storedCode is null || storedCode != request.Code) { throw new SpmsException( ErrorCodes.VerificationCodeError, "인증 코드가 유효하지 않거나 만료되었습니다.", 400); } admin.EmailVerified = true; admin.EmailVerifiedAt = DateTime.UtcNow; _adminRepository.Update(admin); await _unitOfWork.SaveChangesAsync(); await _tokenStore.RemoveAsync($"email_verify:{request.Email}"); } public async Task ForgotPasswordAsync(PasswordForgotRequestDto request) { var admin = await _adminRepository.GetByEmailAsync(request.Email); // 보안: 이메일 존재 여부와 관계없이 동일한 성공 응답 반환 if (admin is null) return; var resetToken = Guid.NewGuid().ToString("N"); await _tokenStore.StoreAsync( $"password_reset:{request.Email}", resetToken, TimeSpan.FromMinutes(30)); await _emailService.SendPasswordResetTokenAsync(request.Email, resetToken); } public async Task ResetPasswordAsync(PasswordResetRequestDto request) { var storedToken = await _tokenStore.GetAsync($"password_reset:{request.Email}"); if (storedToken is null || storedToken != request.ResetToken) { throw new SpmsException( ErrorCodes.ResetTokenError, "재설정 토큰이 유효하지 않거나 만료되었습니다.", 400); } var admin = await _adminRepository.GetByEmailAsync(request.Email); if (admin is null) { throw new SpmsException( ErrorCodes.ResetTokenError, "재설정 토큰이 유효하지 않거나 만료되었습니다.", 400); } admin.Password = BCrypt.Net.BCrypt.HashPassword(request.NewPassword); _adminRepository.Update(admin); await _unitOfWork.SaveChangesAsync(); await _tokenStore.RemoveAsync($"password_reset:{request.Email}"); } }