# Member 模組

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

> **架構原則**（[`docs/model.md`](../../../docs/model.md) §6.1）：usecase **不可** 呼叫其他 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 MFA（AES-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 challenge（bcrypt 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: AssertResendAllowed（cooldown）
    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) — 錯誤碼
