template-monorepo/internal/model/member/README.md

10 KiB
Raw Permalink Blame History

Member 模組

Gateway 的會員核心:Tenant / Member / Identity 三大實體,加上可讀 UID、業務 email/phone OTP、TOTP step-up MFA、重發 / 每日配額。

架構原則docs/model.md §6.1usecase 不可 呼叫其他 usecase多步流程OTP → 寄信 → 驗碼 → flip flag一律在 internal/logic/member/ 編排。

規格 vs 速查:完整 Mongo collection 欄位、Redis key TTL、API endpoint list → SDD.md。本 README 只保留 sequence diagram、curl、ServiceContext wiring 等日常開發要看的東西。


核心實體

實體 用途 主要欄位 儲存
Tenant 租戶元資料 tenant_idsluguid_prefixstatusorg_id Mongo tenants
Member 會員 profile租戶範圍 (tenant_id, uid)zitadel_user_idstatusorigin、business email/phone、totp cipher Mongo members
Identity 外部 ID → UID 對映 zitadel_user_idexternal_iduid Mongo identities

對外可讀主鍵:(tenant_id, uid)UID 格式 {UIDPrefix}-{Sequence}(例:ACME-10000003)。

Origin platform_native(前台註冊)/ oidcZITADEL/Social/ ldap / scim

狀態機:

stateDiagram-v2
    [*] --> unverified: Lifecycle.CreateUnverified
    [*] --> active: Provisioning.Ensure*
    unverified --> active: Activate (OTP 通過)
    unverified --> deleted: AbortPending (註冊逾時)
    active --> suspended: Suspend
    suspended --> active: Reactivate
    active --> deleted: SoftDelete
    suspended --> deleted: SoftDelete

目錄結構

internal/model/member/
├── README.md
├── config/                # OTP / TOTP / Registration 設定
├── domain/                # 介面、enum、entity、errors、redis key
│   ├── entity/            # Member / Tenant / Identity
│   ├── enum/              # MemberStatus / Origin / OTPPurpose / TenantStatus / VerifyKind
│   ├── repository/        # 7 個 repository 介面
│   ├── usecase/           # 7 個 usecase 介面 + DTO
│   ├── const.go           # BSON 欄位、UID 常數
│   ├── errors.go          # ErrNotFound / ErrDuplicateMember ...
│   └── redis.go           # GetOTPChallengeRedisKey ...
├── repository/            # Mongo + Redis 實作
├── totp/                  # RFC 6238 純函式
└── usecase/               # 實作 + module factory

Atomic UseCase 一覽

UseCase 介面方法 職責
TenantUseCase Create / ResolveBySlug 建租戶、依 slug 反查
LifecycleUseCase CreateUnverified / Activate / Suspend / Reactivate / SoftDelete / AbortPending platform 會員建立 + 狀態轉換
ProfileUseCase GetByUID / GetByZitadelUserID / Update / List / SetBusinessEmailVerified / SetBusinessPhoneVerified profile 讀寫、業務 contact 驗證標記
ProvisioningUseCase EnsureFromOIDC / EnsureFromLDAP / EnsureFromSCIM 外部身份首登 upsert冪等
OTPUseCase Generate / Verify / Invalidate / GetChallenge / MatchChallenge 一次性數字碼bcrypt + Redis
TOTPUseCase StartEnroll / ConfirmEnroll / VerifyCode / Disable / RegenerateBackupCodes / Status RFC 6238 step-up MFAAES-GCM 保護 secret
VerifyRateUseCase AssertResendAllowed / AssertDailyAllowed resend 冷卻 + 每日上限

Module factory 條件啟用:

  • Redis 必填 → OTP / VerifyRate 永遠存在
  • Mongo 啟用 → Profile / Lifecycle / Tenant / Provisioning
  • Member.TOTP.SecretKEK 啟用 → TOTP(否則 mod.TOTP == nil

資料儲存

MongoDB Collections

Collection 主要索引
members unique (tenant_id, uid)、unique (tenant_id, zitadel_user_id) sparse
tenants unique slug、unique uid_prefix
identities unique (tenant_id, external_id)、unique (tenant_id, zitadel_user_id)

索引建立由 repository.EnsureMongoIndexescmd/mongo-index 會跑)。

Redis Keys

Key 用途 TTL
member:otp:challenge:{id} OTP challengebcrypt hash OTP.TTLSeconds(預設 300
member:otp:challenge:{id}:attempts OTP 錯誤次數 同 challenge
member:verify:rate:{tenant}:{uid}:{kind} resend 冷卻 OTP.ResendCooldownSeconds(預設 60
member:verify:daily:{tenant}:{uid}:{kind} 每日上限 24h
member:totp:enroll:{tenant}:{uid} 綁定中 staged secret cipher TOTP.EnrollTTLSeconds(預設 600
member:totp:used:{tenant}:{uid}:{timestep} TOTP replay 保護 TOTP.ReplayTTLSeconds(預設 90
member:seq:{tenant} UID 序號 永久

Key helper 在 domain/redis.go禁止 在他處字串拼接。


重要流程

1. 業務 Email / Phone OTP 驗證logic 編排示範)

internal/logic/member/verify_helper.go 串多個 atomic

sequenceDiagram
    autonumber
    participant Client
    participant Logic as logic/member.startVerification
    participant Rate as VerifyRate
    participant OTP
    participant Notif as Notifier
    participant Profile

    Client->>Logic: POST /me/verifications/email/start {target}
    Logic->>Rate: AssertResendAllowedcooldown
    Logic->>Rate: AssertDailyAllowed每日上限
    Logic->>OTP: Generate(purpose=BusinessEmail, target=email)
    OTP-->>Logic: challenge_id, plainCode
    Logic->>Notif: Send(VerifyEmail, code)
    alt Notifier 失敗
        Logic->>OTP: Invalidate(challenge_id)
    end
    Logic-->>Client: {challenge_id, expires_in}

    Note over Client,Profile: 使用者收到信
    Client->>Logic: POST /me/verifications/email/confirm {challenge_id, code}
    Logic->>OTP: Verify (bcrypt compare、attempts ↑↑↑)
    OTP-->>Logic: target(email)
    Logic->>Profile: SetBusinessEmailVerified(tenant, uid, target)
    Logic-->>Client: 204

KeyVerify 成功後 challenge 立刻刪除(一次性);Generate 必先過 VerifyRate 兩道閘。

2. TOTP綁定 + step-up + 重放保護)

sequenceDiagram
    autonumber
    participant Client
    participant TOTP as TOTPUseCase
    participant Profile as TOTPProfileRepository
    participant Enroll as TOTPEnrollStore
    participant Replay as TOTPReplayStore
    participant Cipher as crypto.Cipher

    Note over Client,Cipher: 綁定階段
    Client->>TOTP: StartEnroll(tenant, uid, account)
    TOTP->>Profile: 必須未 enrolled
    TOTP->>Cipher: Encrypt(secret)
    TOTP->>Enroll: Save(cipherBlob, TTL=600s)
    TOTP-->>Client: {otpauth_url, digits, period}

    Client->>TOTP: ConfirmEnroll(code)
    TOTP->>Enroll: Get cipherBlob
    TOTP->>Cipher: Decrypt → secret
    TOTP->>TOTP: totp.Verify(±window)
    TOTP->>Profile: Save (Enrolled, SecretCipher, BackupCodesHash)
    TOTP-->>Client: plainBackupCodes僅此一次回傳

    Note over Client,Replay: 日常 step-up
    Client->>TOTP: VerifyCode(code)
    TOTP->>Replay: MarkUsed(timestep) → fresh?
    alt 已用過
        TOTP-->>Client: ErrTOTPCodeReplay
    end

3. UID 生成

sequenceDiagram
    Caller->>Gen: Next(tenant, uidPrefix)
    Gen->>Redis: INCR member:seq:{tenant}
    alt seq == 1首次
        Gen->>Redis: INCRBY 9_999_999 → 10_000_000
    end
    Gen-->>Caller: "ACME-10000003"

UIDSequenceStart = 10_000_000prefix 限 2~4 個大寫字母。

平台註冊 + Provisioning OIDC/LDAP/SCIM 詳細時序,見 docs/identity-member-design.md


設定(etc/gateway.dev.yaml

Member:
  Registration:
    RequireInviteCode: true
    TrustSocialEmailVerified: true   # OIDC email_verified=true 直接 active
  OTP:
    Length: 6
    TTLSeconds: 300
    MaxAttempts: 5
    ResendCooldownSeconds: 60
    DailyVerifyLimit: 10
  TOTP:
    Issuer: CloudEP
    Algorithm: SHA1
    Digits: 6
    PeriodSeconds: 30
    Window: 1
    BackupCodeCount: 10
    BackupCodeLength: 12
    EnrollTTLSeconds: 600
    ReplayTTLSeconds: 90
    SecretKEK: ""    # 32-byte hex (64) 或 base64留空關閉 TOTP

SecretKEK prod 走 env 或 KMSTOTP_SECRET_KEK)。


ServiceContext 注入

sc.MemberOTP          // 一定有Redis 必填)
sc.MemberVerifyRate   // 一定有
sc.MemberProfile      // Mongo 啟用後
sc.MemberLifecycle    // Mongo 啟用後
sc.MemberTenant       // Mongo 啟用後
sc.MemberProvisioning // Mongo 啟用後
sc.MemberTOTP         // TOTP.SecretKEK 設定後,否則 nil
if sc.MemberTOTP == nil {
    return errb.SysNotImplemented("member TOTP not configured")
}

測試

單元

go test ./internal/model/member/... -v
make check
檔案 覆蓋
usecase/otp_usecase_test.go Generate / Verify、purpose mismatch、attempts lock
usecase/totp_usecase_test.go 綁定、VerifyCode、備援碼、重放、Disable、Regenerate
totp/totp_test.go RFC 6238 測試向量、window、otpauth URL

本機 API

make deps-up && make mongo-index
make member-seed       # 建 dev tenant + member
make run-dev

curl -s -H "Authorization: Bearer $TOKEN" \
  http://127.0.0.1:8888/api/v1/members/me | jq

互動式 TOTP

make totp-test                  # STEP=flow整套綁定 + 驗碼 + 重放
make totp-test STEP=status

Member.TOTP.SecretKEK 已設定。

E2E

docs/e2e-testing.mdTestMember_*)。


相關文件