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

7.4 KiB
Raw Blame History

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簽發建議驗證 issaud
  • /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 退訂 tokentenant_id + list_id + subscriber_id
  • /newsletter/one-click-unsubscribe-tokens 提供 Send Engine 批次取得 one-click 退訂 tokentenant_id + list_id + subscriber_ids[]
  • /newsletter/preferencesGET/POST需要 list_id + email,避免跨租戶資料讀取/更新

通用欄位

  • occurred_atRFC33392026-02-10T09:30:00Z
  • event_idrequest_idUUID

通用錯誤格式

{
  "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.idUUID提供
    • 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 必須存在且 activeauth_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 發信流程
    • 內建 scopenewsletter:send.writenewsletter:send.read
  • usage=webhook_outbound
    • 供 Member Center 內部標記「對外 webhook 用」的租戶憑證用途
    • 不可用於租戶 API 呼叫
    • X-Client-Id 仍以 Send Engine auth_clients.id 為準
    • 需設定 redirect_urisAuthorization Code 流程)
  • usage=platform_service
    • 供平台級 S2S例如 SES 聚合事件回寫)
    • 可不綁定 tenant_idscope 使用 newsletter:events.write.global
  • tenant_api / send_api / platform_service 建議(且實作要求)client_type=confidential
  • redirect_uriswebhook_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.writetenant-scoped
    • newsletter:events.write.globalplatform-scopedSES 回寫用)
  • 建議 Send Engine 使用 client credentials 取 token不建議使用長效固定 token
  • Send Engine 建議以 JWKS 驗簽 JWTJWS並驗證 scope/tenant_id/exp
    • issAuth: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_idUUID
  • subscriber_idUUID
  • list_idUUID
  • reasonhard_bounce | soft_bounce_threshold | complaint | suppression
  • disabled_by(建議固定 send_engine
  • occurred_atRFC3339

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 tokenscopenewsletter:events.writenewsletter:events.write.global
  2. 呼叫 POST /newsletter/one-click-unsubscribe-tokens
  3. 將回傳 status=issuedunsubscribe_token 寫入每封信的 List-Unsubscribe URL

Request批次

{
  "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批次

{
  "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