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

293 lines
10 KiB
Markdown
Raw Normal View History

# Member 模組
2026-05-20 13:03:59 +00:00
Gateway 的會員核心:**Tenant / Member / Identity** 三大實體,加上可讀 UID、業務 email/phone OTP、TOTP step-up MFA、重發 / 每日配額。
2026-05-20 13:03:59 +00:00
> **架構原則**[`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 等日常開發要看的東西。
2026-05-20 13:03:59 +00:00
---
## 核心實體
| 實體 | 用途 | 主要欄位 | 儲存 |
|------|------|---------|------|
| **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`
2026-05-20 13:03:59 +00:00
**狀態機:**
```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
2026-05-20 13:03:59 +00:00
```
---
## 目錄結構
2026-05-20 13:03:59 +00:00
```
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
2026-05-20 13:03:59 +00:00
```
---
2026-05-20 13:03:59 +00:00
## Atomic UseCase 一覽
2026-05-20 13:03:59 +00:00
| 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`
2026-05-20 13:03:59 +00:00
---
2026-05-20 13:03:59 +00:00
## 資料儲存
2026-05-20 13:03:59 +00:00
### MongoDB Collections
2026-05-20 13:03:59 +00:00
| 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 編排示範)
2026-05-20 13:03:59 +00:00
`internal/logic/member/verify_helper.go` 串多個 atomic
2026-05-20 13:03:59 +00:00
```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
```
2026-05-20 13:03:59 +00:00
### 3. UID 生成
2026-05-20 13:03:59 +00:00
```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"
2026-05-20 13:03:59 +00:00
```
`UIDSequenceStart = 10_000_000`prefix 限 2~4 個大寫字母。
2026-05-20 13:03:59 +00:00
> 平台註冊 + Provisioning OIDC/LDAP/SCIM 詳細時序,見 [`docs/identity-member-design.md`](../../../docs/identity-member-design.md)。
2026-05-20 13:03:59 +00:00
---
## 設定(`etc/gateway.dev.yaml`
2026-05-20 13:03:59 +00:00
```yaml
Member:
Registration:
RequireInviteCode: true
TrustSocialEmailVerified: true # OIDC email_verified=true 直接 active
2026-05-20 13:03:59 +00:00
OTP:
Length: 6
TTLSeconds: 300
MaxAttempts: 5
ResendCooldownSeconds: 60
DailyVerifyLimit: 10
2026-05-20 13:03:59 +00:00
TOTP:
Issuer: CloudEP
Algorithm: SHA1
Digits: 6
PeriodSeconds: 30
Window: 1
2026-05-20 13:03:59 +00:00
BackupCodeCount: 10
BackupCodeLength: 12
EnrollTTLSeconds: 600
ReplayTTLSeconds: 90
SecretKEK: "" # 32-byte hex (64) 或 base64留空關閉 TOTP
2026-05-20 13:03:59 +00:00
```
**`SecretKEK`** prod 走 env 或 KMS`TOTP_SECRET_KEK`)。
2026-05-20 13:03:59 +00:00
---
## ServiceContext 注入
2026-05-20 13:03:59 +00:00
```go
sc.MemberOTP // 一定有Redis 必填)
sc.MemberVerifyRate // 一定有
sc.MemberProfile // Mongo 啟用後
sc.MemberLifecycle // Mongo 啟用後
sc.MemberTenant // Mongo 啟用後
sc.MemberProvisioning // Mongo 啟用後
sc.MemberTOTP // TOTP.SecretKEK 設定後,否則 nil
2026-05-20 13:03:59 +00:00
```
```go
if sc.MemberTOTP == nil {
return errb.SysNotImplemented("member TOTP not configured")
}
2026-05-20 13:03:59 +00:00
```
---
## 測試
2026-05-20 13:03:59 +00:00
### 單元
2026-05-20 13:03:59 +00:00
```bash
go test ./internal/model/member/... -v
make check
2026-05-20 13:03:59 +00:00
```
| 檔案 | 覆蓋 |
|------|------|
| `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 |
2026-05-20 23:51:22 +00:00
### 本機 API
2026-05-20 23:51:22 +00:00
```bash
make deps-up && make mongo-index
make member-seed # 建 dev tenant + member
make run-dev
2026-05-20 23:51:22 +00:00
curl -s -H "Authorization: Bearer $TOKEN" \
2026-05-20 23:51:22 +00:00
http://127.0.0.1:8888/api/v1/members/me | jq
```
### 互動式 TOTP
2026-05-20 13:03:59 +00:00
```bash
make totp-test # STEP=flow整套綁定 + 驗碼 + 重放
2026-05-20 13:03:59 +00:00
make totp-test STEP=status
```
`Member.TOTP.SecretKEK` 已設定。
### E2E
見 [`docs/e2e-testing.md`](../../../docs/e2e-testing.md)`TestMember_*`)。
2026-05-20 13:03:59 +00:00
---
## 相關文件
2026-05-20 13:03:59 +00:00
- [`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) — 錯誤碼