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

293 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Member 模組
Gateway 的會員核心:**Tenant / Member / Identity** 三大實體,加上可讀 UID、業務 email/phone OTP、TOTP step-up MFA、重發 / 每日配額。
> **架構原則**[`docs/model.md`](../../../docs/model.md) §6.1usecase **不可** 呼叫其他 usecase多步流程OTP → 寄信 → 驗碼 → flip flag一律在 `internal/logic/member/` 編排。
>
> **規格 vs 速查**:完整 Mongo collection 欄位、Redis key TTL、API endpoint list → [`SDD.md`](./SDD.md)。本 README 只保留 sequence diagram、curl、ServiceContext wiring 等日常開發要看的東西。
---
## 核心實體
| 實體 | 用途 | 主要欄位 | 儲存 |
|------|------|---------|------|
| **Tenant** | 租戶元資料 | `tenant_id`、`slug`、`uid_prefix`、`status`、`org_id` | Mongo `tenants` |
| **Member** | 會員 profile租戶範圍 | `(tenant_id, uid)`、`zitadel_user_id`、`status`、`origin`、business email/phone、totp cipher | Mongo `members` |
| **Identity** | 外部 ID → UID 對映 | `zitadel_user_id`、`external_id`、`uid` | Mongo `identities` |
對外可讀主鍵:`(tenant_id, uid)`UID 格式 `{UIDPrefix}-{Sequence}`(例:`ACME-10000003`)。
**Origin** `platform_native`(前台註冊)/ `oidc`ZITADEL/Social/ `ldap` / `scim`
**狀態機:**
```mermaid
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.EnsureMongoIndexes``cmd/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
```mermaid
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
```
Key`Verify` 成功後 challenge 立刻刪除(一次性);`Generate` 必先過 `VerifyRate` 兩道閘。
### 2. TOTP綁定 + step-up + 重放保護)
```mermaid
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 生成
```mermaid
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_000`prefix 限 2~4 個大寫字母。
> 平台註冊 + Provisioning OIDC/LDAP/SCIM 詳細時序,見 [`docs/identity-member-design.md`](../../../docs/identity-member-design.md)。
---
## 設定(`etc/gateway.dev.yaml`
```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 或 KMS`TOTP_SECRET_KEK`)。
---
## ServiceContext 注入
```go
sc.MemberOTP // 一定有Redis 必填)
sc.MemberVerifyRate // 一定有
sc.MemberProfile // Mongo 啟用後
sc.MemberLifecycle // Mongo 啟用後
sc.MemberTenant // Mongo 啟用後
sc.MemberProvisioning // Mongo 啟用後
sc.MemberTOTP // TOTP.SecretKEK 設定後,否則 nil
```
```go
if sc.MemberTOTP == nil {
return errb.SysNotImplemented("member TOTP not configured")
}
```
---
## 測試
### 單元
```bash
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
```bash
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
```bash
make totp-test # STEP=flow整套綁定 + 驗碼 + 重放
make totp-test STEP=status
```
`Member.TOTP.SecretKEK` 已設定。
### E2E
見 [`docs/e2e-testing.md`](../../../docs/e2e-testing.md)`TestMember_*`)。
---
## 相關文件
- [`SDD.md`](./SDD.md) — Member 模組規格書Data Dictionary、完整 API 端點)
- [`docs/model.md`](../../../docs/model.md) — Clean Architecture 分層
- [`docs/identity-member-design.md`](../../../docs/identity-member-design.md) — 跨模組設計
- [`internal/library/errors/README.md`](../../library/errors/README.md) — 錯誤碼