131 lines
3.9 KiB
Go
131 lines
3.9 KiB
Go
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, ¬ifuc.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
|
|
}
|