member_center/docs/DESIGN.md

190 lines
8.8 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.

# 系統設計草案Member Center
日期2026-01-30
## 1. 需求摘要(已確認)
1) 多個網站共用的會員登入中心SSO
2) 電子報訂閱管理(發送另建)
3) 未註冊會員可訂閱;註冊後沿用訂閱資料
4) 未註冊會員可取消訂閱(單一清單退訂)
5) 登入需支援 API 與 Redirect 兩種方式OAuth2 + OIDC
## 2. 架構原則
- OAuth2 + OIDCAuthorization Code + PKCE
- 會員中心只管理 Email 與訂閱狀態
- Double Opt-in
- 各站自行設計 UI主要走 API少數狀況使用 redirect
- 多租戶為邏輯隔離,但會員資料跨站共享
- 公開訂閱端點必須使用 `list_id + email` 做資料邊界,禁止僅以 `email` 查詢或操作
- 訂閱狀態同步目前採 webhookevent payloadqueue 為後續可選擴充
- PostgreSQL
- 實作C# .NET Core + MVC + OpenIddict
## 3. 使用者故事(精簡)
- 訪客:在任一站點未登入狀態下輸入 Email 訂閱電子報
- 訪客:在信件中點擊連結完成 double opt-in
- 訪客:點擊取消訂閱連結即可退訂(單一清單)
- 會員在任一站點登入後其他站點可無痛登入SSO
- 站點後台:可管理站點資訊、訂閱清單、會員基本資料
## 4. 核心模組
- Identity Service註冊、登入、密碼重設、Email 驗證
- OAuth2/OIDC Service授權流程、token 發放、ID Token
- Subscription Service訂閱/退訂/偏好管理
- Admin Console租戶與清單管理
- Mailer Integration驗證信/退訂信/確認信的發送介面(外部系統)
- Event Publisher訂閱事件發佈供發信系統或數據系統消費
## 5. 資料模型(概念)
- tenants
- id, name, domains, status, created_at
- users (ASP.NET Core Identity)
- id, user_name, email, password_hash, email_confirmed, lockout, is_blacklisted, blacklisted_at, blacklisted_by, created_at
- roles / user_roles (Identity)
- id, name, created_at
- OpenIddictApplications
- id, client_id, client_secret, display_name, permissions, redirect_uris, properties`tenant_id`, `usage=tenant_api|send_api|webhook_outbound|platform_service`
- OpenIddictAuthorizations
- id, application_id, status, subject, type, scopes
- OpenIddictTokens
- id, application_id, authorization_id, subject, type, status, expiration_date
- OpenIddictScopes
- id, name, display_name, resources
- newsletter_lists
- id, tenant_id, name, status, created_at
- newsletter_subscriptions
- id, list_id, email, user_id (nullable), status, preferences, created_at
- email_blacklist
- id, email, reason, blacklisted_at, blacklisted_by
- email_verifications
- id, email, tenant_id, token_hash, purpose, expires_at, consumed_at
- unsubscribe_tokens
- id, subscription_id, token_hash, expires_at, consumed_at
- audit_logs
- id, actor_type, actor_id, action, payload, created_at
- system_flags
- id, key, value, updated_at
關聯說明:
- newsletter_subscriptions.email 與 users.email 維持唯一性關聯
- 使用者註冊時,如 email 存在訂閱紀錄,補上 user_id
- 單一清單退訂unsubscribe token 綁定 subscription_id
- blacklist 記錄於 email_blacklist全租戶共用
## 6. 核心流程
### 6.1 OAuth2/OIDC Redirect 登入Authorization Code + PKCE
1) 站點導向 `/oauth/authorize`,帶 `client_id`, `redirect_uri`, `code_challenge`, `scope=openid email`
2) 使用者於會員中心登入
3) 成功後導回 `redirect_uri` 並附 `code`
4) 站點以 `code` + `code_verifier``/oauth/token` 換取 token + `id_token`
### 6.2 OAuth2 API 使用(站點自行 UI
1) 站點以 API 驗證使用者登入(會員中心提供 login API
2) 成功後取得 token含 ID Token 可選)
3) 站點以 access_token 呼叫其他會員中心 API
### 6.3 未登入狀態的訂閱流程(在獨立平台)
1) 使用者在各站點輸入 Email 並選擇訂閱清單
2) 站點呼叫 `POST /newsletter/subscribe`
3) 會員中心建立 `pending` 訂閱並發送驗證信(透過外部發信系統)
4) 使用者點擊信件連結 `/newsletter/confirm?token=...`
5) 訂閱狀態改為 `active`
6) 會員中心發出事件 `subscription.activated` 到 event/queue
### 6.4 未登入退訂(單一清單)
1) 信件提供「一鍵退訂」連結 `/newsletter/unsubscribe?token=...`
2) 驗證 token 後將該訂閱標記為 `unsubscribed`
3) 會員中心發出事件 `subscription.unsubscribed` 到 event/queue
### 6.4b Send Engine 發信前申請 One-Click Token
1) Send Engine 依收件者呼叫 `POST /newsletter/one-click-unsubscribe-token`,或批次呼叫 `POST /newsletter/one-click-unsubscribe-tokens`
2) body 帶 `tenant_id + list_id + subscriber_id`(批次版為 `subscriber_ids[]`
3) 會員中心簽發 token與手動退訂 token 分離 purpose
4) Send Engine 將 token 寫入 `List-Unsubscribe` 連結
### 6.6 Send Engine 事件同步Member Center → Send Engine
1) Member Center 發出事件(`subscription.activated` / `subscription.unsubscribed` / `preferences.updated`
2) 以 webhook 推送至 Send Engine簽章與重放防護
3) `X-Client-Id` 使用 Send Engine `auth_clients.id`(可按 tenant 做設定覆蓋)
4) Send Engine 驗證 tenant scope更新本地名單快照
### 6.7 Send Engine 退信/黑名單回寫(選用)
1) Send Engine 依事件類型決定回寫時機與原因碼:
2) `hard_bounce` / `soft_bounce_threshold` / `suppression`Member Center 取消該 email 所有訂閱(跨租戶)並加入黑名單
3) `complaint`Member Center 僅取消該筆訂閱,不加入黑名單
4) 回寫請求需帶 `tenant_id + subscriber_id + list_id`Member Center 端做租戶邊界驗證
5) 回寫授權可用:
- tenant client scope`newsletter:events.write`
- platform client scope`newsletter:events.write.global`SES 聚合事件)
### 6.5 註冊後銜接
1) 使用者完成註冊
2) 系統搜尋 `newsletter_subscriptions.email`
3) 將 `user_id` 補上並保留偏好
4) 可選:發出事件 `subscription.linked_to_user`
## 7. API 介面(草案)
- GET `/oauth/authorize`
- POST `/oauth/token`
- GET `/.well-known/openid-configuration`
- POST `/auth/login` (API-only login)
- POST `/auth/refresh`
- POST `/newsletter/subscribe`
- GET `/newsletter/confirm`
- POST `/newsletter/unsubscribe`
- POST `/newsletter/unsubscribe-token`
- POST `/newsletter/one-click-unsubscribe-token`
- POST `/newsletter/one-click-unsubscribe-tokens`
- GET `/newsletter/preferences`
- POST `/newsletter/preferences`
- POST `/webhooks/subscriptions`Send Engine 端點Member Center 呼叫)
- POST `/webhooks/lists/full-sync`Send Engine 端點Member Center 呼叫)
- POST `/subscriptions/disable`Member Center 端點Send Engine 呼叫)
- POST `/integrations/send-engine/webhook-clients/upsert`Member Center 端點Send Engine 呼叫)
## 7.1 進度狀態2026-02
### API
- `GET /newsletter/subscriptions?list_id=...`:已實作
- `POST /webhooks/subscriptions`已實作Member Center 發送)
- `POST /subscriptions/disable`:已實作
- `POST /integrations/send-engine/webhook-clients/upsert`:已實作
- `POST /webhooks/lists/full-sync`:尚未實作(保留規格)
### Auth / Scope
- `tenant_api` / `send_api` / `webhook_outbound` OAuth Client 綁定 `tenant_id`,所有清單/事件 API 需驗證租戶邊界
- OAuth Client 需區分用途:`tenant_api` / `send_api` / `webhook_outbound` / `platform_service`(禁止混用)
- 新增 scope`newsletter:list.read``newsletter:send.write``newsletter:send.read``newsletter:events.read`
- 新增 scope`newsletter:events.write`
- 新增 scope`newsletter:events.write.global`
- JWT Access Token 已改為 JWS`DisableAccessTokenEncryption`),供 Send Engine 以 JWKS 驗簽
### 租戶端取 TokenClient Credentials
- 租戶使用 OAuth Client Credentials 向 Member Center 取得 access token
- token 內含 `tenant_id` 與 scope
- Send Engine 收到租戶請求後以 JWKS 驗簽 JWTJWS
- 驗簽通過後將 `tenant_id` 固定在 request context不接受 body 覆寫
## 7.2 尚未完成(待辦)
- `POST /webhooks/lists/full-sync`Member Center 端尚未發送此事件(僅保留契約)
- 註冊後訂閱綁定(`newsletter_subscriptions.user_id` 補值)尚未在註冊流程落地
- `subscription.linked_to_user` 事件尚未發送
- 安全設定頁access/refresh 時效)目前僅存值,尚未實際套用到 OpenIddict token lifetime
- Audit Logs 目前以查詢為主,關鍵操作的寫入覆蓋率仍不足
## 8. 安全與合規
- 密碼強度與防暴力破解rate limit + lockout
- Token rotation + refresh token revoke
- Redirect URI 白名單 + PKCE
- Double opt-in可配置
- Audit log
- GDPR/CCPA資料匯出與刪除規劃中
## 9. 其他文件
- `docs/UI.md`
- `docs/USE_CASES.md`
- `docs/FLOWS.md`
- `docs/OPENAPI.md`
- `docs/SCHEMA.sql`
- `docs/TECH_STACK.md`