feat: 이메일 인증 인프라 및 API 구현 (#64)
- ITokenStore, IEmailService 인터페이스 정의 - InMemoryTokenStore (IMemoryCache 기반), ConsoleEmailService (로그 출력) 구현 - SignupAsync에 6자리 인증 코드 생성/저장/발송 로직 추가 - VerifyEmailAsync 구현 (코드 검증 → EmailVerified 업데이트) - POST /v1/in/auth/email/verify 엔드포인트 추가 - DI 등록 (ITokenStore, IEmailService, MemoryCache)
This commit is contained in:
parent
e96bbff727
commit
ccdfcbd62e
|
|
@ -91,6 +91,19 @@ public class AuthController : ControllerBase
|
|||
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")]
|
||||
[Authorize]
|
||||
[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 ChangePasswordAsync(long adminId, ChangePasswordRequestDto 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 IJwtService _jwtService;
|
||||
private readonly JwtSettings _jwtSettings;
|
||||
private readonly ITokenStore _tokenStore;
|
||||
private readonly IEmailService _emailService;
|
||||
|
||||
public AuthService(
|
||||
IAdminRepository adminRepository,
|
||||
IUnitOfWork unitOfWork,
|
||||
IJwtService jwtService,
|
||||
IOptions<JwtSettings> jwtSettings)
|
||||
IOptions<JwtSettings> jwtSettings,
|
||||
ITokenStore tokenStore,
|
||||
IEmailService emailService)
|
||||
{
|
||||
_adminRepository = adminRepository;
|
||||
_unitOfWork = unitOfWork;
|
||||
_jwtService = jwtService;
|
||||
_jwtSettings = jwtSettings.Value;
|
||||
_tokenStore = tokenStore;
|
||||
_emailService = emailService;
|
||||
}
|
||||
|
||||
public async Task<SignupResponseDto> SignupAsync(SignupRequestDto request)
|
||||
|
|
@ -61,7 +67,15 @@ public class AuthService : IAuthService
|
|||
await _adminRepository.AddAsync(admin);
|
||||
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
|
||||
{
|
||||
AdminCode = admin.AdminCode,
|
||||
|
|
@ -225,4 +239,40 @@ public class AuthService : IAuthService
|
|||
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.Repositories;
|
||||
using SPMS.Infrastructure.Security;
|
||||
using SPMS.Infrastructure.Services;
|
||||
|
||||
namespace SPMS.Infrastructure;
|
||||
|
||||
|
|
@ -32,6 +33,11 @@ public static class DependencyInjection
|
|||
services.AddSingleton<IE2EEService, E2EEService>();
|
||||
services.AddSingleton<ICredentialEncryptionService, CredentialEncryptionService>();
|
||||
|
||||
// Token Store & Email Service
|
||||
services.AddMemoryCache();
|
||||
services.AddSingleton<ITokenStore, InMemoryTokenStore>();
|
||||
services.AddSingleton<IEmailService, ConsoleEmailService>();
|
||||
|
||||
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