feat: 이메일 인증 인프라 및 API 구현 (#64) #65
|
|
@ -91,6 +91,19 @@ public class AuthController : ControllerBase
|
||||||
return Ok(ApiResponse.Success());
|
return Ok(ApiResponse.Success());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("email/verify")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
[SwaggerOperation(
|
||||||
|
Summary = "이메일 인증",
|
||||||
|
Description = "회원가입 시 발송된 인증 코드로 이메일을 인증합니다.")]
|
||||||
|
[SwaggerResponse(200, "이메일 인증 성공")]
|
||||||
|
[SwaggerResponse(400, "인증 코드 불일치 또는 만료")]
|
||||||
|
public async Task<IActionResult> VerifyEmailAsync([FromBody] EmailVerifyRequestDto request)
|
||||||
|
{
|
||||||
|
await _authService.VerifyEmailAsync(request);
|
||||||
|
return Ok(ApiResponse.Success());
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost("password/change")]
|
[HttpPost("password/change")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[SwaggerOperation(
|
[SwaggerOperation(
|
||||||
|
|
|
||||||
13
SPMS.Application/DTOs/Auth/EmailVerifyRequestDto.cs
Normal file
13
SPMS.Application/DTOs/Auth/EmailVerifyRequestDto.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace SPMS.Application.DTOs.Auth;
|
||||||
|
|
||||||
|
public class EmailVerifyRequestDto
|
||||||
|
{
|
||||||
|
[Required(ErrorMessage = "이메일은 필수입니다.")]
|
||||||
|
[EmailAddress(ErrorMessage = "올바른 이메일 형식이 아닙니다.")]
|
||||||
|
public string Email { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "인증 코드는 필수입니다.")]
|
||||||
|
public string Code { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
@ -10,4 +10,5 @@ public interface IAuthService
|
||||||
Task LogoutAsync(long adminId);
|
Task LogoutAsync(long adminId);
|
||||||
Task ChangePasswordAsync(long adminId, ChangePasswordRequestDto request);
|
Task ChangePasswordAsync(long adminId, ChangePasswordRequestDto request);
|
||||||
Task<EmailCheckResponseDto> CheckEmailAsync(EmailCheckRequestDto request);
|
Task<EmailCheckResponseDto> CheckEmailAsync(EmailCheckRequestDto request);
|
||||||
|
Task VerifyEmailAsync(EmailVerifyRequestDto request);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
7
SPMS.Application/Interfaces/IEmailService.cs
Normal file
7
SPMS.Application/Interfaces/IEmailService.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
namespace SPMS.Application.Interfaces;
|
||||||
|
|
||||||
|
public interface IEmailService
|
||||||
|
{
|
||||||
|
Task SendVerificationCodeAsync(string email, string code);
|
||||||
|
Task SendPasswordResetTokenAsync(string email, string token);
|
||||||
|
}
|
||||||
8
SPMS.Application/Interfaces/ITokenStore.cs
Normal file
8
SPMS.Application/Interfaces/ITokenStore.cs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
namespace SPMS.Application.Interfaces;
|
||||||
|
|
||||||
|
public interface ITokenStore
|
||||||
|
{
|
||||||
|
Task StoreAsync(string key, string value, TimeSpan expiry);
|
||||||
|
Task<string?> GetAsync(string key);
|
||||||
|
Task RemoveAsync(string key);
|
||||||
|
}
|
||||||
|
|
@ -16,17 +16,23 @@ public class AuthService : IAuthService
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly IJwtService _jwtService;
|
private readonly IJwtService _jwtService;
|
||||||
private readonly JwtSettings _jwtSettings;
|
private readonly JwtSettings _jwtSettings;
|
||||||
|
private readonly ITokenStore _tokenStore;
|
||||||
|
private readonly IEmailService _emailService;
|
||||||
|
|
||||||
public AuthService(
|
public AuthService(
|
||||||
IAdminRepository adminRepository,
|
IAdminRepository adminRepository,
|
||||||
IUnitOfWork unitOfWork,
|
IUnitOfWork unitOfWork,
|
||||||
IJwtService jwtService,
|
IJwtService jwtService,
|
||||||
IOptions<JwtSettings> jwtSettings)
|
IOptions<JwtSettings> jwtSettings,
|
||||||
|
ITokenStore tokenStore,
|
||||||
|
IEmailService emailService)
|
||||||
{
|
{
|
||||||
_adminRepository = adminRepository;
|
_adminRepository = adminRepository;
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
_jwtService = jwtService;
|
_jwtService = jwtService;
|
||||||
_jwtSettings = jwtSettings.Value;
|
_jwtSettings = jwtSettings.Value;
|
||||||
|
_tokenStore = tokenStore;
|
||||||
|
_emailService = emailService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SignupResponseDto> SignupAsync(SignupRequestDto request)
|
public async Task<SignupResponseDto> SignupAsync(SignupRequestDto request)
|
||||||
|
|
@ -61,7 +67,15 @@ public class AuthService : IAuthService
|
||||||
await _adminRepository.AddAsync(admin);
|
await _adminRepository.AddAsync(admin);
|
||||||
await _unitOfWork.SaveChangesAsync();
|
await _unitOfWork.SaveChangesAsync();
|
||||||
|
|
||||||
// 5. 응답 반환
|
// 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
|
return new SignupResponseDto
|
||||||
{
|
{
|
||||||
AdminCode = admin.AdminCode,
|
AdminCode = admin.AdminCode,
|
||||||
|
|
@ -225,4 +239,40 @@ public class AuthService : IAuthService
|
||||||
IsAvailable = !exists
|
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}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ using SPMS.Infrastructure.Auth;
|
||||||
using SPMS.Infrastructure.Persistence;
|
using SPMS.Infrastructure.Persistence;
|
||||||
using SPMS.Infrastructure.Persistence.Repositories;
|
using SPMS.Infrastructure.Persistence.Repositories;
|
||||||
using SPMS.Infrastructure.Security;
|
using SPMS.Infrastructure.Security;
|
||||||
|
using SPMS.Infrastructure.Services;
|
||||||
|
|
||||||
namespace SPMS.Infrastructure;
|
namespace SPMS.Infrastructure;
|
||||||
|
|
||||||
|
|
@ -32,6 +33,11 @@ public static class DependencyInjection
|
||||||
services.AddSingleton<IE2EEService, E2EEService>();
|
services.AddSingleton<IE2EEService, E2EEService>();
|
||||||
services.AddSingleton<ICredentialEncryptionService, CredentialEncryptionService>();
|
services.AddSingleton<ICredentialEncryptionService, CredentialEncryptionService>();
|
||||||
|
|
||||||
|
// Token Store & Email Service
|
||||||
|
services.AddMemoryCache();
|
||||||
|
services.AddSingleton<ITokenStore, InMemoryTokenStore>();
|
||||||
|
services.AddSingleton<IEmailService, ConsoleEmailService>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
30
SPMS.Infrastructure/Services/ConsoleEmailService.cs
Normal file
30
SPMS.Infrastructure/Services/ConsoleEmailService.cs
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using SPMS.Application.Interfaces;
|
||||||
|
|
||||||
|
namespace SPMS.Infrastructure.Services;
|
||||||
|
|
||||||
|
public class ConsoleEmailService : IEmailService
|
||||||
|
{
|
||||||
|
private readonly ILogger<ConsoleEmailService> _logger;
|
||||||
|
|
||||||
|
public ConsoleEmailService(ILogger<ConsoleEmailService> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task SendVerificationCodeAsync(string email, string code)
|
||||||
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
"[EMAIL] 이메일 인증 코드 발송 → To: {Email}, Code: {Code}",
|
||||||
|
email, code);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task SendPasswordResetTokenAsync(string email, string token)
|
||||||
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
"[EMAIL] 비밀번호 재설정 토큰 발송 → To: {Email}, Token: {Token}",
|
||||||
|
email, token);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
SPMS.Infrastructure/Services/InMemoryTokenStore.cs
Normal file
32
SPMS.Infrastructure/Services/InMemoryTokenStore.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using SPMS.Application.Interfaces;
|
||||||
|
|
||||||
|
namespace SPMS.Infrastructure.Services;
|
||||||
|
|
||||||
|
public class InMemoryTokenStore : ITokenStore
|
||||||
|
{
|
||||||
|
private readonly IMemoryCache _cache;
|
||||||
|
|
||||||
|
public InMemoryTokenStore(IMemoryCache cache)
|
||||||
|
{
|
||||||
|
_cache = cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StoreAsync(string key, string value, TimeSpan expiry)
|
||||||
|
{
|
||||||
|
_cache.Set(key, value, expiry);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<string?> GetAsync(string key)
|
||||||
|
{
|
||||||
|
_cache.TryGetValue(key, out string? value);
|
||||||
|
return Task.FromResult(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task RemoveAsync(string key)
|
||||||
|
{
|
||||||
|
_cache.Remove(key);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user