# Auth 模組

Gateway 認證領域層：邀請碼、註冊稽核、OAuth Session、CloudEP JWT 簽發／刷新／登出。完整 HTTP 流程由 `internal/logic/auth/` 編排，並與 ZITADEL（身份）、Member（會員）、Notification（通知）協作。

> **架構原則**（[`docs/model.md`](../../../docs/model.md) §6.1）：usecase 為 atomic primitive，**不可** 呼叫其他 usecase；跨模組編排放 `internal/logic/auth/`。
>
> **規格 vs 速查**：
> - 規格書（Data Dictionary `invite_codes` / `registration_metadata`、Redis key、API endpoint list）→ [`SDD.md`](./SDD.md)
> - 統一註冊（Email/OTP/Social）完整時序 + ZITADEL 互動細節 → [`docs/auth-unified-registration.md`](../../../docs/auth-unified-registration.md)
> - 本 README = 範圍 + ServiceContext wiring + 設定 + curl + 快速跟其他模組對齊用

## 範圍

| 範圍內 | 範圍外（委派） |
|--------|-----------------|
| 邀請碼 Validate / Consume | 使用者身份建立 → ZITADEL |
| 註冊稽核 metadata | 會員 profile / OTP → Member |
| OAuth 註冊 / 登入暫存 Session（Redis） | 郵件 / 簡訊 → Notification |
| CloudEP JWT access / refresh 生命週期 | RBAC → Permission |
| JWT 黑名單 + jti pair |  |

---

## 目錄結構

```
internal/model/auth/
├── README.md                # 本檔
├── config/
├── domain/
│   ├── entity/              # InviteCode、RegistrationMetadata
│   ├── enum/                # RegistrationChannel（email / google）
│   ├── repository/          # 介面 + Session struct
│   ├── usecase/             # 介面 + DTO
│   ├── const.go             # BSON 欄位、Redis key、code hash helper
│   └── errors.go            # 領域 sentinel
├── repository/              # Mongo + Redis 實作 + EnsureMongoIndexes
└── usecase/                 # 實作 + NewModuleFromParam
```

## Module 與依賴

```mermaid
flowchart TB
    Logic["logic/auth\n(orchestration)"]
    subgraph M["auth.Module"]
      Token[TokenUseCase]
      Invite[InviteUseCase]
      RegMeta[RegistrationMetaUseCase]
      RegSess[RegistrationSessionUseCase]
      LoginSess[LoginSessionUseCase]
    end
    Logic --> Token
    Logic --> Invite
    Logic --> RegMeta
    Logic --> RegSess
    Logic --> LoginSess
    Logic --> Member[(member.Module)]
    Logic --> Zitadel[(library/zitadel)]
    Logic --> Notif[(notification)]
```

**ServiceContext 注入：**

| 欄位 | UseCase | 啟用條件 |
|------|---------|----------|
| `AuthToken` | TokenUseCase | JWT secret + Redis |
| `AuthInvite` | InviteUseCase | Mongo + Redis |
| `AuthRegistrationMeta` | RegistrationMetaUseCase | Mongo |
| `AuthRegistrationSession` | RegistrationSessionUseCase | Redis |
| `AuthLoginSession` | LoginSessionUseCase | Redis |

---

## API（`/api/v1/auth`）

### 公開（無 Bearer）

| Method | Path | 說明 |
|--------|------|------|
| POST | `/register` | Email 註冊 → `{challenge_id, expires_in, uid}` |
| POST | `/register/confirm` | OTP 確認 → 核發 JWT |
| POST | `/register/resend` | 重發註冊 OTP |
| POST | `/register/social/start` | 社交註冊起始 → `{oauth_url, session_id}` |
| GET  | `/register/social/callback` | OAuth callback → JWT |
| POST | `/login` | 密碼登入（ZITADEL ROPG） |
| POST | `/login/social/start` | 社交登入起始 |
| GET  | `/login/social/callback` | 社交登入 callback |
| POST | `/token/refresh` | 刷新 token pair |
| POST | `/token/exchange` | id_token → CloudEP JWT |

### 需 Bearer

| Method | Path | 說明 |
|--------|------|------|
| POST | `/logout` | 黑名單 access + paired refresh jti |

完整 schema：`generate/api/auth.api`；成功 envelope `code=102000`。

---

## Token（CloudEP JWT）

HS256；access / refresh 使用不同 secret。

| Claim | 說明 |
|-------|------|
| `tenant_id` | 租戶 ID |
| `uid` | 會員 UID |
| `typ` | `access` 或 `refresh` |
| `auth_gen` | 簽發世代（強制登出時 +1） |
| `jti` | Token 唯一 ID |
| `iat` / `exp` | 簽發 / 過期 |

| Token | 預設 TTL |
|-------|---------|
| Access | 900s（15 分鐘） |
| Refresh | 604800s（7 天） |
| Registration Session | 600s（10 分鐘） |

### Redis Key

| Key | 用途 | TTL |
|-----|------|-----|
| `auth:jwt:pair:{jti}` | access ↔ refresh jti 映射 | token TTL |
| `auth:jwt:bl:{jti}` | 黑名單 jti | 至自然過期 |
| `auth:register:session:{id}` | 社交註冊 OAuth session | 600s |
| `auth:login:session:{id}` | 社交登入 OAuth session | 600s |
| `auth:invite:consume:{tenant}:{hash}` | 邀請碼消費鎖 | 30s |

### Refresh 流程

```mermaid
sequenceDiagram
    participant C as Client
    participant L as TokenRefreshLogic
    participant T as TokenUseCase
    participant R as Redis
    C->>L: POST /auth/token/refresh {refresh_token}
    L->>T: Refresh(refreshToken)
    T->>T: Parse + verify typ=refresh
    T->>R: Blacklist 舊 refresh jti + paired access jti
    T->>T: IssuePair（new access + refresh）
    T->>R: SavePair(new jti mapping)
    T-->>L: TokenPair
    L-->>C: AuthTokenData
```

---

## 統一註冊（Email + Social）

> 取代原 ZITADEL Hosted Page；使用者只與 Gateway 互動。Invite 為 B2B 必填、B2C 可選（`Member.Registration.RequireInviteCode`）。

### 三條路徑

```
┌─────────────────────────────────────────────┐
│         前端「註冊」頁（共用 invite + 條款）  │
└────────────┬────────────────┬───────────────┘
             ▼                ▼
    Email + Password     Social (Google)
             │                │
   POST /register       POST /register/social/start
             │                │ → oauth_url
   POST /register/confirm   GET /register/social/callback
             │                │
             └───────┬────────┘
                     ▼
             IssueTokenPair（CloudEP JWT）
```

### Email 路徑

1. Logic 驗 tenant_slug + invite + 條款 + 密碼強度 + email 格式
2. Logic 消耗 invite（Redis SETNX lock + Mongo `$inc used_count`）
3. ZITADEL `CreateHumanUser`
4. `member.Lifecycle.CreateUnverified`（`origin=platform_native`）
5. 寫 registration metadata
6. `member.OTP.Generate(purpose=registration_email)` + `notifier.Send(verify_registration_email)`
7. 回 `{challenge_id, expires_in}`，**不發 JWT**
8. `POST /register/confirm`：OTP verify → `Activate` → `IssueTokenPair`

### Social 路徑（Google）

- Invite 必填；在 OAuth redirect **前** 綁定 registration session（Redis TTL 600s）
- callback 才 **消耗** invite（避免 IdP 中途取消）
- `member.Provisioning.EnsureFromOIDC`（同 sub 已存在則視為登入）
- `Member.Registration.TrustSocialEmailVerified=true` 時，IdP `email_verified=true` 直接 `Activate`，否則走 OTP

### 失敗補償

| 步驟 | 補償 |
|------|------|
| Invite 無效 | 直接 4xx，無 side effect |
| ZITADEL 建 user OK / member 失敗 | Logic 呼叫 `zitadel.DeactivateUser(sub)` |
| Member OK / Send OTP 失敗 | `OTP.Invalidate(challenge_id)` |
| confirm 時 Activate 失敗 | 5xx，保留 challenge 可重試 |

> Invite 在 ZITADEL/member 失敗時 **不自動回滾**（防刷；可 admin 補發）。

### Logic 商務驗證一覽（不下沉 usecase）

- invite 必填 / 過期 / 限新 user
- 條款版本接受
- tenant 是否允許 B2C 註冊
- 密碼政策
- 註冊 rate limit key 組合
- Social：OAuth state 與 registration session 綁定

---

## Invite Code

| 欄位 | 說明 |
|------|------|
| `tenant_id` | 租戶 |
| `code_hash` | SHA-256(normalized code)，**永不明文** |
| `max_uses` / `used_count` | 總次數 / 已用 |
| `expires_at` | 0 = 永不過期 |
| `new_users_only` | 限新用戶（社交註冊用） |

- 索引：`(tenant_id, code_hash)` unique
- Validate / Consume 介面：`InviteUseCase`
- Email 註冊在 `/register` 起始即 Consume；Social 在 Callback 才 Consume

---

## Rate Limit（建議由 middleware 落地）

| Key | 限制 |
|-----|------|
| `auth:register:ip:{ip}` | 10 / hour |
| `auth:register:email:{tenant}:{email}` | 3 / hour |
| `auth:register:invite_fail:{ip}` | 20 / hour |

OTP resend 沿用 `member.VerifyRate`。

---

## 錯誤碼（Auth scope 28）

| 情境 | errb |
|------|------|
| invite 無效 | `InputInvalidFormat` / `ResNotFound("invite")` |
| invite 用盡 | `ResInsufficientQuota` |
| email 已註冊 | `ResAlreadyExist` |
| OTP 錯誤 | `AuthForbidden` + cause |
| tenant 不允許註冊 | `AuthForbidden` |
| 未接受條款 | `InputMissingRequired` |
| ZITADEL 下游失敗 | `SvcThirdParty` |
| DB 失敗 | `DBError` via wrapRepoErr |

Scope / category 完整對照：[`internal/library/errors/README.md`](../../library/errors/README.md)。

---

## 設定（`etc/gateway.dev.yaml`）

```yaml
Auth:
  AccessExpire: 900
  RefreshExpire: 604800
  ActiveKID: v1
  AccessSecret: ...   # 32+ bytes，prod 走 env / secret manager
  RefreshSecret: ...
  RegistrationSessionTTLSeconds: 600

Zitadel:
  Issuer: https://zitadel.internal
  ServiceUserToken: ${ZITADEL_SERVICE_TOKEN}
  GoogleClientID: ...
  GoogleClientSecret: ...
  DefaultOrgID: ...
```

---

## 測試

- **單元**：`go test ./internal/model/auth/...`（invite consume 並發、JWT parse/refresh、errb 映射）
- **E2E**：見 [`docs/e2e-testing.md`](../../../docs/e2e-testing.md)（`TestAuth_*` / `TestZZZ_AuthTokenRefreshAndLogout`）；ZITADEL 整合路徑（A-01~A-09）需 staging 環境

---

## 相關文件

- [`docs/model.md`](../../../docs/model.md) — Clean Architecture 分層
- [`generate/api/README.md`](../../../generate/api/README.md) — `.api` + middleware 約定
- [`internal/library/errors/README.md`](../../library/errors/README.md) — 8 碼錯誤碼
- [`internal/model/member/README.md`](../member/README.md) — Member 模組
