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

87 lines
2.9 KiB
Go
Raw Normal View History

package usecase
import (
"fmt"
2026-05-20 13:03:59 +00:00
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"
)
2026-05-20 13:03:59 +00:00
// 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 {
2026-05-20 13:03:59 +00:00
// 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 {
2026-05-20 13:03:59 +00:00
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
}
2026-05-20 13:03:59 +00:00
// 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()
2026-05-20 13:03:59 +00:00
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
}