template-monorepo/internal/model/member/usecase/verification_usecase.go

151 lines
5.0 KiB
Go

package usecase
import (
"context"
"time"
"gateway/internal/model/member"
memberconfig "gateway/internal/model/member/config"
"gateway/internal/model/member/domain/enum"
domrepo "gateway/internal/model/member/domain/repository"
domusecase "gateway/internal/model/member/domain/usecase"
notifenum "gateway/internal/model/notification/domain/enum"
domnotif "gateway/internal/model/notification/domain/usecase"
)
type verificationUseCase struct {
otp domusecase.OTPUseCase
notifier domnotif.NotifierUseCase
profile domrepo.ProfileRepository
rates domrepo.VerifyRateStore
config memberconfig.Config
}
// VerificationUseCaseParam wires VerificationUseCase.
type VerificationUseCaseParam struct {
OTP domusecase.OTPUseCase
Notifier domnotif.NotifierUseCase
Profile domrepo.ProfileRepository
Rates domrepo.VerifyRateStore
Config memberconfig.Config
}
// MustVerificationUseCase constructs VerificationUseCase.
func MustVerificationUseCase(param VerificationUseCaseParam) domusecase.VerificationUseCase {
return &verificationUseCase{
otp: param.OTP,
notifier: param.Notifier,
profile: param.Profile,
rates: param.Rates,
config: param.Config.Defaults(),
}
}
func (uc *verificationUseCase) StartEmailVerify(ctx context.Context, tenantID, uid, target, locale string) (*domusecase.OTPChallengeDTO, error) {
return uc.startVerify(ctx, tenantID, uid, target, locale, enum.VerifyKindEmail, enum.OTPPurposeBusinessEmail, notifenum.ChannelEmail, notifenum.NotifyVerifyEmail)
}
func (uc *verificationUseCase) ConfirmEmailVerify(ctx context.Context, tenantID, uid, challengeID, code string) error {
target, err := uc.otp.Verify(ctx, &domusecase.VerifyOTPRequest{
TenantID: tenantID,
UID: uid,
ChallengeID: challengeID,
Code: code,
Purpose: enum.OTPPurposeBusinessEmail,
})
if err != nil {
return err
}
return uc.profile.SetBusinessEmailVerified(ctx, tenantID, uid, target)
}
func (uc *verificationUseCase) StartPhoneVerify(ctx context.Context, tenantID, uid, target, locale string) (*domusecase.OTPChallengeDTO, error) {
return uc.startVerify(ctx, tenantID, uid, target, locale, enum.VerifyKindPhone, enum.OTPPurposeBusinessPhone, notifenum.ChannelSMS, notifenum.NotifyVerifyPhone)
}
func (uc *verificationUseCase) ConfirmPhoneVerify(ctx context.Context, tenantID, uid, challengeID, code string) error {
target, err := uc.otp.Verify(ctx, &domusecase.VerifyOTPRequest{
TenantID: tenantID,
UID: uid,
ChallengeID: challengeID,
Code: code,
Purpose: enum.OTPPurposeBusinessPhone,
})
if err != nil {
return err
}
return uc.profile.SetBusinessPhoneVerified(ctx, tenantID, uid, target)
}
func (uc *verificationUseCase) startVerify(
ctx context.Context,
tenantID, uid, target, locale string,
kind enum.VerifyKind,
purpose enum.OTPPurpose,
channel notifenum.Channel,
notifyKind notifenum.NotifyKind,
) (*domusecase.OTPChallengeDTO, error) {
if tenantID == "" || uid == "" || target == "" {
return nil, errb.InputMissingRequired("tenant_id, uid and target are required")
}
if uc.notifier == nil {
return nil, errb.SysInternal("notifier is not configured")
}
if uc.rates == nil {
return nil, errb.SysInternal("verify rate store is not configured")
}
if err := uc.checkRateLimits(ctx, tenantID, uid, kind); err != nil {
return nil, err
}
dto, code, err := uc.otp.Generate(ctx, &domusecase.GenerateOTPRequest{
TenantID: tenantID,
UID: uid,
Purpose: purpose,
Target: target,
})
if err != nil {
return nil, err
}
_, err = uc.notifier.Send(ctx, &domnotif.SendRequest{
TenantID: tenantID,
UID: uid,
Channel: channel,
Kind: notifyKind,
Target: target,
Locale: locale,
Data: map[string]any{"code": code, "expires_in": dto.ExpiresIn},
IdempotencyKey: dto.ChallengeID,
DoNotPersistBody: true,
Severity: notifenum.SeverityInfo,
})
if err != nil {
if invErr := uc.otp.Invalidate(ctx, dto.ChallengeID); invErr != nil {
return nil, errb.SysInternal("invalidate otp after send failure").WithCause(invErr)
}
return nil, err
}
return dto, nil
}
func (uc *verificationUseCase) checkRateLimits(ctx context.Context, tenantID, uid string, kind enum.VerifyKind) error {
cooldown := time.Duration(uc.config.OTP.ResendCooldownSeconds) * time.Second
ok, err := uc.rates.TryResendLock(ctx, member.GetVerifyRateRedisKey(tenantID, uid, kind.String()), cooldown)
if err != nil {
return errb.SysInternal("verify rate check failed").WithCause(err)
}
if !ok {
return errb.ResInvalidState("verification resend cooldown").WithCause(member.ErrResendCooldown)
}
count, err := uc.rates.IncrDaily(ctx, member.GetVerifyDailyRedisKey(tenantID, uid, kind.String()), 25*time.Hour)
if err != nil {
return errb.SysInternal("verify daily limit check failed").WithCause(err)
}
if count > int64(uc.config.OTP.DailyVerifyLimit) {
return errb.ResInsufficientQuota("daily verification limit exceeded").WithCause(member.ErrDailyLimit)
}
return nil
}