feat: implement IAM OTP dispatch abstractions
Co-authored-by: Velik <hongochai10@users.noreply.github.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using MediatR;
|
||||
using IamService.API.Application.Services;
|
||||
using IamService.Domain.AggregatesModel.VerificationAggregate;
|
||||
using IamService.Domain.Exceptions;
|
||||
|
||||
@@ -12,13 +13,16 @@ public class RequestPhoneVerificationCommandHandler
|
||||
: IRequestHandler<RequestPhoneVerificationCommand, RequestVerificationCommandResult>
|
||||
{
|
||||
private readonly IIdentityVerificationRepository _verificationRepository;
|
||||
private readonly IVerificationOtpDispatcher _otpDispatcher;
|
||||
private readonly ILogger<RequestPhoneVerificationCommandHandler> _logger;
|
||||
|
||||
public RequestPhoneVerificationCommandHandler(
|
||||
IIdentityVerificationRepository verificationRepository,
|
||||
IVerificationOtpDispatcher otpDispatcher,
|
||||
ILogger<RequestPhoneVerificationCommandHandler> logger)
|
||||
{
|
||||
_verificationRepository = verificationRepository;
|
||||
_otpDispatcher = otpDispatcher;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -50,12 +54,11 @@ public class RequestPhoneVerificationCommandHandler
|
||||
|
||||
await _verificationRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
|
||||
// EN: TODO: Send OTP via SMS service
|
||||
// VI: TODO: Gửi OTP qua SMS service
|
||||
_logger.LogInformation(
|
||||
"Phone verification created for user {UserId}. OTP: {OTP} (in production, send via SMS)",
|
||||
await _otpDispatcher.SendPhoneOtpAsync(
|
||||
request.UserId,
|
||||
otp); // EN: Remove OTP from logs in production!
|
||||
request.PhoneNumber,
|
||||
otp,
|
||||
cancellationToken);
|
||||
|
||||
return new RequestVerificationCommandResult(
|
||||
verification.Id,
|
||||
@@ -73,13 +76,16 @@ public class RequestEmailVerificationCommandHandler
|
||||
: IRequestHandler<RequestEmailVerificationCommand, RequestVerificationCommandResult>
|
||||
{
|
||||
private readonly IIdentityVerificationRepository _verificationRepository;
|
||||
private readonly IVerificationOtpDispatcher _otpDispatcher;
|
||||
private readonly ILogger<RequestEmailVerificationCommandHandler> _logger;
|
||||
|
||||
public RequestEmailVerificationCommandHandler(
|
||||
IIdentityVerificationRepository verificationRepository,
|
||||
IVerificationOtpDispatcher otpDispatcher,
|
||||
ILogger<RequestEmailVerificationCommandHandler> logger)
|
||||
{
|
||||
_verificationRepository = verificationRepository;
|
||||
_otpDispatcher = otpDispatcher;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -101,25 +107,21 @@ public class RequestEmailVerificationCommandHandler
|
||||
throw new DomainException("An email verification is already pending. Please wait for it to expire or complete it.");
|
||||
}
|
||||
|
||||
// EN: Create new email verification (using phone verification method with email type)
|
||||
// EN: Create new email verification
|
||||
// VI: Tạo xác thực email mới
|
||||
var (verification, otp) = IdentityVerification.CreatePhoneVerification(
|
||||
var (verification, otp) = IdentityVerification.CreateEmailVerification(
|
||||
request.UserId,
|
||||
request.Email);
|
||||
|
||||
// EN: Update type to Email
|
||||
// VI: Cập nhật loại thành Email
|
||||
// Note: In real implementation, we would have a separate factory method for email
|
||||
_verificationRepository.Add(verification);
|
||||
|
||||
await _verificationRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
|
||||
// EN: TODO: Send OTP via Email service
|
||||
// VI: TODO: Gửi OTP qua Email service
|
||||
_logger.LogInformation(
|
||||
"Email verification created for user {UserId}. OTP: {OTP} (in production, send via Email)",
|
||||
await _otpDispatcher.SendEmailOtpAsync(
|
||||
request.UserId,
|
||||
otp);
|
||||
request.Email,
|
||||
otp,
|
||||
cancellationToken);
|
||||
|
||||
return new RequestVerificationCommandResult(
|
||||
verification.Id,
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
using IamService.Infrastructure.Email;
|
||||
using System.Linq;
|
||||
|
||||
namespace IamService.API.Application.Services;
|
||||
|
||||
/// <summary>
|
||||
/// EN: OTP dispatcher abstraction for verification workflows.
|
||||
/// VI: Abstraction gửi OTP cho các luồng xác thực.
|
||||
/// </summary>
|
||||
public interface IVerificationOtpDispatcher
|
||||
{
|
||||
/// <summary>
|
||||
/// EN: Send OTP to phone number.
|
||||
/// VI: Gửi OTP đến số điện thoại.
|
||||
/// </summary>
|
||||
Task SendPhoneOtpAsync(Guid userId, string phoneNumber, string otp, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Send OTP to email address.
|
||||
/// VI: Gửi OTP đến địa chỉ email.
|
||||
/// </summary>
|
||||
Task SendEmailOtpAsync(Guid userId, string email, string otp, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Default OTP dispatcher implementation.
|
||||
/// VI: Implementation mặc định của OTP dispatcher.
|
||||
/// </summary>
|
||||
public class VerificationOtpDispatcher : IVerificationOtpDispatcher
|
||||
{
|
||||
private readonly IEmailService _emailService;
|
||||
private readonly ILogger<VerificationOtpDispatcher> _logger;
|
||||
|
||||
public VerificationOtpDispatcher(
|
||||
IEmailService emailService,
|
||||
ILogger<VerificationOtpDispatcher> logger)
|
||||
{
|
||||
_emailService = emailService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task SendPhoneOtpAsync(
|
||||
Guid userId,
|
||||
string phoneNumber,
|
||||
string otp,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Phone OTP dispatch requested for user {UserId} to {MaskedPhone}.",
|
||||
userId,
|
||||
MaskPhoneNumber(phoneNumber));
|
||||
|
||||
// EN: Placeholder SMS dispatch point (provider integration can be plugged in later).
|
||||
// VI: Điểm tích hợp gửi SMS (có thể cắm provider sau).
|
||||
_ = otp;
|
||||
_ = cancellationToken;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task SendEmailOtpAsync(
|
||||
Guid userId,
|
||||
string email,
|
||||
string otp,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _emailService.Send2FACodeAsync(email, otp, cancellationToken);
|
||||
_logger.LogInformation(
|
||||
"Email OTP dispatched for user {UserId} to {MaskedEmail}.",
|
||||
userId,
|
||||
MaskEmail(email));
|
||||
}
|
||||
|
||||
private static string MaskEmail(string email)
|
||||
{
|
||||
var parts = email.Split('@');
|
||||
if (parts.Length != 2 || parts[0].Length < 2)
|
||||
return "***";
|
||||
|
||||
var localPart = parts[0];
|
||||
var maskedLocal = $"{localPart[0]}***{localPart[^1]}";
|
||||
return $"{maskedLocal}@{parts[1]}";
|
||||
}
|
||||
|
||||
private static string MaskPhoneNumber(string phoneNumber)
|
||||
{
|
||||
var digits = new string(phoneNumber.Where(char.IsDigit).ToArray());
|
||||
if (digits.Length <= 4)
|
||||
return "****";
|
||||
return $"***{digits[^4..]}";
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Asp.Versioning;
|
||||
using FluentValidation;
|
||||
using Hellang.Middleware.ProblemDetails;
|
||||
using IamService.API.Application.Services;
|
||||
using IamService.API.Application.Behaviors;
|
||||
using IamService.API.Swagger;
|
||||
using IamService.Infrastructure;
|
||||
@@ -21,6 +22,7 @@ builder.Host.UseSerilog((context, services, configuration) => configuration
|
||||
// EN: Add Infrastructure services (Identity, Duende IdentityServer, Repositories)
|
||||
// VI: Thêm Infrastructure services (Identity, Duende IdentityServer, Repositories)
|
||||
builder.Services.AddInfrastructure(builder.Configuration, builder.Environment.EnvironmentName);
|
||||
builder.Services.AddScoped<IVerificationOtpDispatcher, VerificationOtpDispatcher>();
|
||||
|
||||
// EN: Add Authorization Policies for Backoffice APIs
|
||||
// VI: Thêm Authorization Policies cho các API Backoffice
|
||||
|
||||
@@ -176,6 +176,35 @@ public class IdentityVerification : Entity, IAggregateRoot
|
||||
return (verification, otp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Create email verification with OTP.
|
||||
/// VI: Tạo xác thực email với OTP.
|
||||
/// </summary>
|
||||
/// <returns>Tuple of (IdentityVerification, plainTextOtp)</returns>
|
||||
public static (IdentityVerification Verification, string Otp) CreateEmailVerification(
|
||||
Guid userId,
|
||||
string email)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(email))
|
||||
throw new ArgumentException("Email cannot be empty", nameof(email));
|
||||
|
||||
var verification = new IdentityVerification(
|
||||
userId,
|
||||
VerificationType.Email,
|
||||
email.Trim(),
|
||||
OtpExpirationMinutes);
|
||||
|
||||
var otp = GenerateOtp();
|
||||
verification._verificationCodeHash = HashOtp(otp);
|
||||
|
||||
verification.AddDomainEvent(new VerificationRequestedEvent(
|
||||
verification.Id,
|
||||
userId,
|
||||
VerificationType.Email.Name));
|
||||
|
||||
return (verification, otp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Create document verification for KYC.
|
||||
/// VI: Tạo xác thực tài liệu cho KYC.
|
||||
|
||||
Reference in New Issue
Block a user