151 lines
5.0 KiB
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
|
|
}
|