member_center/docs/OPENAPI.md
warrenchen 4fbf2e5497 feat: Enhance OAuth client management and add one-click unsubscribe functionality
- Updated OpenAPI documentation to include new OAuth2 usage types: `send_api`.
- Added endpoints for issuing one-click unsubscribe tokens in both single and batch requests.
- Modified OAuth client creation and management to enforce new usage types and redirect URI requirements.
- Implemented logic in the Newsletter service to handle one-click unsubscribe token issuance.
- Updated UI to reflect changes in OAuth client usage options and redirect URI handling.
- Enhanced token generation logic to support new scopes and audience settings for Send Engine.
2026-02-25 14:29:26 +09:00

178 lines
7.4 KiB
Markdown
Raw 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.

# OpenAPI 草案(完整)
已補上完整端點與資料結構,並提供 `docs/openapi.yaml` 作為可直接擴充的版本。
## 版本
- OpenAPI: 3.1.0
- 檔案:`docs/openapi.yaml`
## 核心資源
- OAuth2/OIDC授權、token、discovery、JWKS
- Auth註冊、登入password grant、刷新、登出、忘記/重設密碼、Email 驗證
- User個人資料
- Newsletter訂閱/確認/退訂/偏好
- AdminTenants/Lists/OAuth ClientsMVP CRUD
## Security Schemes
- OAuth2 (Authorization Code + PKCE、Client Credentials)
- Bearer JWTAPI 使用)
## 補充說明
- `/oauth/token``/auth/login``/auth/refresh` 使用 `application/x-www-form-urlencoded`
- Access token 以 JWTJWS簽發建議驗證 `iss``aud`
- `/auth/email/verify` 需要 `token` + `email`
- `/newsletter/subscribe` 會回傳 `confirm_token`
- `/newsletter/unsubscribe-token` 需要 `list_id + email` 才能申請 `unsubscribe_token`
- `/newsletter/one-click-unsubscribe-token` 提供 Send Engine 發信前取得 one-click 退訂 token`tenant_id + list_id + subscriber_id`
- `/newsletter/one-click-unsubscribe-tokens` 提供 Send Engine 批次取得 one-click 退訂 token`tenant_id + list_id + subscriber_ids[]`
- `/newsletter/preferences`GET/POST需要 `list_id + email`,避免跨租戶資料讀取/更新
## 通用欄位
- `occurred_at`RFC3339`2026-02-10T09:30:00Z`
- `event_id``request_id`UUID
## 通用錯誤格式
```json
{
"error": "string_code",
"message": "human readable message",
"request_id": "uuid"
}
```
## 多租戶資料隔離原則
- 與訂閱者資料preferences、unsubscribe token相關的查詢與寫入一律必須帶 `list_id + email` 做租戶邊界約束。
- 不提供僅靠 `email` 或單純 `subscription_id` 的公開查詢/操作端點。
## Webhook AuthMember Center -> Send Engine
- Header對齊 Send Engine 規格):
- `X-Signature`
- `X-Timestamp`
- `X-Nonce`
- `X-Client-Id`
- `X-Client-Id` 來源:
- 由 Send Engine 的 `auth_clients.id`UUID提供
- Member Center 以 DB 設定tenant 設定欄位)保存每個租戶的對應值
- 可由管理 UITenant 編輯)或整合 API `POST /integrations/send-engine/webhook-clients/upsert` 更新
- 簽章建議:
- `HMAC-SHA256(secret, "{raw_body}")`(對齊 Send Engine 驗證器)
- 驗證規則:
- timestamp 在允許時間窗(例如 ±5 分鐘)
- nonce 不可重複(防重放)
- `X-Client-Id` 必須存在且 active`auth_clients.tenant_id` 與 payload `tenant_id` 一致
- 不使用 `X-Client-Id` fallback缺少 tenant 對應 client 時應略過發送
- 預設拒絕 `auth_clients.tenant_id = NULL` 的通用 client除非 Send Engine 明確開啟)
- signature 必須匹配
## OAuth Client 用途分離(強制)
- `usage=tenant_api`
- 供租戶站台拿 token 呼叫 Member Center / Send Engine API
- scope 僅給業務所需(如 `newsletter:events.write`
- `usage=send_api`
- 供租戶站台呼叫 Send Engine 發信流程
- 內建 scope`newsletter:send.write``newsletter:send.read`
- `usage=webhook_outbound`
- 供 Member Center 內部標記「對外 webhook 用」的租戶憑證用途
- 不可用於租戶 API 呼叫
- `X-Client-Id` 仍以 Send Engine `auth_clients.id` 為準
- 需設定 `redirect_uris`Authorization Code 流程)
- `usage=platform_service`
- 供平台級 S2S例如 SES 聚合事件回寫)
- 可不綁定 `tenant_id`scope 使用 `newsletter:events.write.global`
- `tenant_api` / `send_api` / `platform_service` 建議(且實作要求)`client_type=confidential`
- `redirect_uris``webhook_outbound` 需要;其他 usage 可為空
- 管理規則:
- 每個 tenant 至少 2 組憑證(`tenant_api` / `webhook_outbound`
- 平台級流程另建 `platform_service` 憑證
- secret 分開輪替,禁止共用
## 整合 API / Auth狀態
### API
- `GET /newsletter/subscriptions?list_id=...`:已實作(供發送引擎同步)
- `POST /webhooks/subscriptions`已實作Member Center 發送Send Engine 接收)
- `POST /webhooks/lists/full-sync`規格已定義Member Center 發送Send Engine 接收)
- `POST /subscriptions/disable`已實作Send Engine 回寫黑名單)
- `POST /integrations/send-engine/webhook-clients/upsert`已實作Send Engine 回填 tenant webhook client id
- `POST /newsletter/one-click-unsubscribe-token`已實作Send Engine 發信前申請 one-click token
- `POST /newsletter/one-click-unsubscribe-tokens`已實作Send Engine 批次申請 one-click token
### Auth / Scope
- `tenant_api` / `send_api` / `webhook_outbound` 類型需綁定 `tenant_id`
- `platform_service` 可不綁定 `tenant_id`
- 新增 scope
- `newsletter:list.read`
- `newsletter:send.write`
- `newsletter:send.read`
- `newsletter:events.read`
- `newsletter:events.write`
- `newsletter:events.write.global`
- 發送引擎僅能用上述 scope禁止 admin 權限
- `POST /subscriptions/disable` 需 Bearer token 且包含下列其一:
- `newsletter:events.write`tenant-scoped
- `newsletter:events.write.global`platform-scopedSES 回寫用)
- 建議 Send Engine 使用 client credentials 取 token不建議使用長效固定 token
- Send Engine 建議以 JWKS 驗簽 JWTJWS並驗證 `scope/tenant_id/exp`
- `iss``Auth:Issuer` 設定(例:`http://localhost:7850/`
- `aud` 預設:
- Send Engine 流程:`send_engine_api`(可用 `Auth:SendEngineAudience` 覆寫)
- Member Center API 流程:`member_center_api`(可用 `Auth:MemberCenterAudience` 覆寫)
### 回寫原因碼Send Engine -> Member Center
- `hard_bounce`
- `soft_bounce_threshold`
- `complaint`
- `suppression`
### `/subscriptions/disable` 請求欄位Send Engine -> Member Center
- `tenant_id`UUID
- `subscriber_id`UUID
- `list_id`UUID
- `reason``hard_bounce | soft_bounce_threshold | complaint | suppression`
- `disabled_by`(建議固定 `send_engine`
- `occurred_at`RFC3339
Member Center 會用 `subscriber_id + list_id` 查詢訂閱,再驗證 `tenant_id` 邊界;驗證通過後才寫入全域 email 黑名單。
### One-Click 退訂 Token 介接方式Send Engine
用途Send Engine 在寄信前批次向 Member Center 取得每位收件者的 one-click 退訂 token。
步驟:
1. 以 client credentials 取得 access tokenscope`newsletter:events.write``newsletter:events.write.global`
2. 呼叫 `POST /newsletter/one-click-unsubscribe-tokens`
3. 將回傳 `status=issued``unsubscribe_token` 寫入每封信的 `List-Unsubscribe` URL
Request批次
```json
{
"tenant_id": "c9034414-43d6-404e-8d41-e80922420bf1",
"list_id": "a92fdeda-29bb-42ca-9c05-e7df3983288a",
"subscriber_ids": [
"33333333-3333-3333-3333-333333333333",
"44444444-4444-4444-4444-444444444444"
]
}
```
Response批次
```json
{
"items": [
{
"subscriber_id": "33333333-3333-3333-3333-333333333333",
"unsubscribe_token": "token-xxx",
"status": "issued"
},
{
"subscriber_id": "44444444-4444-4444-4444-444444444444",
"unsubscribe_token": null,
"status": "blacklisted"
}
]
}
```
狀態說明:
- `issued`:可直接用於 one-click 退訂連結
- `not_found`找不到對應訂閱者tenant/list/subscriber 邊界不匹配)
- `blacklisted`:已在黑名單,不提供 token