template-monorepo/internal/logic/member/verify_helper.go

131 lines
3.9 KiB
Go
Raw Normal View History

2026-05-20 23:51:22 +00:00
package member
import (
"context"
"time"
errs "gateway/internal/library/errors"
"gateway/internal/library/errors/code"
memberdom "gateway/internal/model/member/domain"
"gateway/internal/model/member/domain/enum"
domusecase "gateway/internal/model/member/domain/usecase"
notifenum "gateway/internal/model/notification/domain/enum"
notifuc "gateway/internal/model/notification/domain/usecase"
"gateway/internal/svc"
"gateway/internal/types"
)
var errb = errs.For(code.Facade)
func startVerification(
ctx context.Context,
sc *svc.ServiceContext,
actor Actor,
purpose enum.OTPPurpose,
channel notifenum.Channel,
kind notifenum.NotifyKind,
target string,
) (*types.VerificationStartData, error) {
if sc.MemberOTP == nil {
return nil, errb.SysInternal("member OTP not configured")
}
if sc.Notifier == nil {
return nil, errb.SysInternal("notifier not configured")
}
if target == "" {
return nil, errb.InputMissingRequired("target is required")
}
cfg := sc.Config.Member.Defaults()
rateKey := memberdom.GetVerifyRateRedisKey(actor.TenantID, actor.UID, string(purpose))
ok, err := sc.MemberVerifyRate.TryResendLock(ctx, rateKey, time.Duration(cfg.OTP.ResendCooldownSeconds)*time.Second)
if err != nil {
return nil, errb.SysInternal("rate limit check failed").WithCause(err)
}
if !ok {
return nil, errb.AuthForbidden("resend cooldown active").WithCause(memberdom.ErrResendCooldown)
}
dailyKey := memberdom.GetVerifyDailyRedisKey(actor.TenantID, actor.UID, string(purpose))
count, err := sc.MemberVerifyRate.IncrDaily(ctx, dailyKey, 24*time.Hour)
if err != nil {
return nil, errb.SysInternal("daily limit check failed").WithCause(err)
}
if count > int64(cfg.OTP.DailyVerifyLimit) {
return nil, errb.AuthForbidden("daily verification limit exceeded").WithCause(memberdom.ErrDailyLimit)
}
dto, plainCode, err := sc.MemberOTP.Generate(ctx, &domusecase.GenerateOTPRequest{
TenantID: actor.TenantID,
UID: actor.UID,
Purpose: purpose,
Target: target,
})
if err != nil {
return nil, err
}
locale := sc.Config.Notification.DefaultLocale
if _, sendErr := sc.Notifier.Send(ctx, &notifuc.SendRequest{
TenantID: actor.TenantID,
UID: actor.UID,
Channel: channel,
Kind: kind,
Target: target,
Locale: locale,
Data: map[string]any{"code": plainCode, "expires_in": dto.ExpiresIn},
IdempotencyKey: dto.ChallengeID,
DoNotPersistBody: true,
Severity: notifenum.SeverityInfo,
}); sendErr != nil {
if invErr := sc.MemberOTP.Invalidate(ctx, dto.ChallengeID); invErr != nil {
return nil, errb.SysInternal("invalidate otp after send failure").WithCause(invErr)
}
return nil, sendErr
}
return &types.VerificationStartData{
ChallengeID: dto.ChallengeID,
ExpiresIn: dto.ExpiresIn,
}, nil
}
func confirmVerification(
ctx context.Context,
sc *svc.ServiceContext,
actor Actor,
req *types.VerificationConfirmReq,
purpose enum.OTPPurpose,
setVerified func(context.Context, string, string, string) error,
) error {
if sc.MemberOTP == nil || sc.MemberProfile == nil {
return errb.SysInternal("member module not configured")
}
if req == nil || req.ChallengeID == "" || req.Code == "" {
return errb.InputMissingRequired("challenge_id and code are required")
}
target, err := sc.MemberOTP.Verify(ctx, &domusecase.VerifyOTPRequest{
TenantID: actor.TenantID,
UID: actor.UID,
ChallengeID: req.ChallengeID,
Code: req.Code,
Purpose: purpose,
})
if err != nil {
return err
}
return setVerified(ctx, actor.TenantID, actor.UID, target)
}
func requireTOTP(sc *svc.ServiceContext) error {
if sc.MemberTOTP == nil {
return errb.SysInternal("member TOTP not configured")
}
return nil
}
func actorOrErr(ctx context.Context) (Actor, error) {
actor, err := ActorFromContext(ctx)
if err != nil {
return Actor{}, errb.AuthForbidden(err.Error())
}
return actor, nil
}