87 lines
2.9 KiB
Go
87 lines
2.9 KiB
Go
package usecase
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
libcrypto "gateway/internal/library/crypto"
|
|
redislib "gateway/internal/library/redis"
|
|
memberconfig "gateway/internal/model/member/config"
|
|
domrepo "gateway/internal/model/member/domain/repository"
|
|
domusecase "gateway/internal/model/member/domain/usecase"
|
|
"gateway/internal/model/member/repository"
|
|
)
|
|
|
|
// Module bundles member atomic primitives. Each entry is a single-purpose
|
|
// usecase; composite flows (e.g. "send verification email then mark
|
|
// business_email verified") are assembled at the logic / driver layer and
|
|
// MUST NOT live inside another usecase.
|
|
type Module struct {
|
|
// OTP issues and verifies one-time codes (purpose-agnostic).
|
|
OTP domusecase.OTPUseCase
|
|
// TOTP is nil when Member.TOTP.SecretKEK is empty / invalid; downstream
|
|
// code must gracefully degrade (e.g. fall back to SMS/email OTP at the
|
|
// logic layer).
|
|
TOTP domusecase.TOTPUseCase
|
|
|
|
// Stores exposed for logic-layer orchestration (rate limit, profile
|
|
// flips). They are intentionally surfaced so the logic layer can compose
|
|
// atomic usecases with rate-limit + profile mutation without re-wiring.
|
|
VerifyRate domrepo.VerifyRateStore
|
|
Profile domrepo.ProfileRepository
|
|
}
|
|
|
|
// ModuleParam wires member module dependencies.
|
|
type ModuleParam struct {
|
|
Redis *redislib.Client
|
|
Config memberconfig.Config
|
|
// Profile is optional; defaults to memory repository.
|
|
Profile domrepo.ProfileRepository
|
|
// TOTPProfile is optional; defaults to memory repository.
|
|
TOTPProfile domrepo.TOTPProfileRepository
|
|
}
|
|
|
|
// NewModuleFromParam builds member atomic usecases.
|
|
//
|
|
// TOTP is wired only when Member.TOTP.SecretKEK is provided; this lets local
|
|
// dev / unit tests boot without a KMS-backed key while production deployments
|
|
// fail loud when the operator forgets to configure it.
|
|
func NewModuleFromParam(param ModuleParam) (*Module, error) {
|
|
if param.Redis == nil || param.Redis.Zero() == nil {
|
|
return nil, fmt.Errorf("member: redis is required")
|
|
}
|
|
|
|
otpStore := repository.NewRedisOTPChallengeStore(param.Redis)
|
|
rateStore := repository.NewRedisVerifyRateStore(param.Redis)
|
|
profile := param.Profile
|
|
if profile == nil {
|
|
profile = repository.NewMemoryProfileRepository()
|
|
}
|
|
|
|
cfg := param.Config.Defaults()
|
|
mod := &Module{
|
|
OTP: MustOTPUseCase(OTPUseCaseParam{Store: otpStore, Config: cfg}),
|
|
VerifyRate: rateStore,
|
|
Profile: profile,
|
|
}
|
|
|
|
if cfg.TOTP.SecretKEK != "" {
|
|
cipher, err := libcrypto.NewAESGCMFromString(cfg.TOTP.SecretKEK)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("member: totp kek: %w", err)
|
|
}
|
|
totpProfile := param.TOTPProfile
|
|
if totpProfile == nil {
|
|
totpProfile = repository.NewMemoryTOTPProfileRepository()
|
|
}
|
|
mod.TOTP = MustTOTPUseCase(TOTPUseCaseParam{
|
|
Profile: totpProfile,
|
|
Enroll: repository.NewRedisTOTPEnrollStore(param.Redis),
|
|
Replay: repository.NewRedisTOTPReplayStore(param.Redis),
|
|
Cipher: cipher,
|
|
Config: cfg,
|
|
})
|
|
}
|
|
|
|
return mod, nil
|
|
}
|