diff --git a/README.md b/README.md index 2a053bd..5db8ee5 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ - `docs/MESSMAIL.md`:租戶站台介接指引(訂閱/退訂/發信) - `docs/TECH_STACK.md`:技術棧與選型 - `docs/INSTALL.md`:安裝、初始化與維運指令 +- `docs/MEMBER_UPGRADE_PLAN.md`:會員中心下一階段升級規劃(設定畫面、SMTP、Email 驗證、忘記密碼、角色分級) ## 專案結構 ```text diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 4cbc364..a1afa08 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -13,11 +13,12 @@ - OAuth2 + OIDC(Authorization Code + PKCE) - 會員中心只管理 Email 與訂閱狀態 - 檔案存取授權沿用 OAuth2/JWT/JWKS,並以 scope + claim 做資源邊界控制 +- 對外資料 API 以 service API 為主,授權完全由 scope 控制 - Double Opt-in - 各站自行設計 UI,主要走 API;少數狀況使用 redirect - 多租戶為邏輯隔離,但會員資料跨站共享 - 公開訂閱端點必須使用 `list_id + email` 做資料邊界,禁止僅以 `email` 查詢或操作 -- 訂閱狀態同步目前採 webhook(event payload);queue 為後續可選擴充 +- 訂閱狀態同步採 webhook(event payload);queue 可作為未來擴充選項 - PostgreSQL - 實作:C# .NET Core + MVC + OpenIddict @@ -31,6 +32,8 @@ ## 4. 核心模組 - Identity Service:註冊、登入、修改密碼、密碼重設、Email 驗證 - OAuth2/OIDC Service:授權流程、token 發放、ID Token +- Auth Resource Registry:管理外部服務 resource / audience / scopes / client usage 對應 +- Profile Service:會員基本資料、聯絡方式、公司資料、地址簿 - Subscription Service:訂閱/退訂/偏好管理 - Admin Console:租戶與清單管理 - Mailer Integration:驗證信/退訂信/確認信的發送介面(外部系統) @@ -40,7 +43,22 @@ - 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 + - id, user_name, email, password_hash, email_confirmed, lockout, is_blacklisted, blacklisted_at, blacklisted_by, created_at, last_login_at, last_seen_at, disabled_at, disabled_by +- user_profiles + - user_id, last_name, first_name, nick_name, mobile_phone, landline_phone, date_of_birth, gender, company_name, department, job_title, company_phone, tax_id, invoice_title, remark, updated_at +- user_addresses + - id, user_id, label, recipient_name, recipient_phone, country_code, postal_code, state_region, city, district, address_line1, address_line2, company_name, usage, is_default, address_meta_json, created_at, updated_at + +資料約束建議: +- `user_profiles.first_name`、`user_profiles.last_name` 必填 +- `user_profiles.gender` 使用列舉:`male` / `female` / `other` / `unspecified` +- `user_profiles.date_of_birth` 使用 `date` +- `user_addresses.country_code` 使用 ISO 3166-1 alpha-2 +- `user_addresses.address_line1` 必填 +- `user_addresses.usage` 使用列舉:`shipping` / `billing` / `both` +- 同一個 `user_id + usage` 只允許一筆預設地址 +- 地址至少保留一筆,禁止刪除最後一筆地址 +- `address_meta_json` 作為國家特有欄位補充,不取代主結構欄位 - roles / user_roles (Identity) - id, name, created_at - OpenIddictApplications @@ -71,6 +89,7 @@ - 使用者註冊時,如 email 存在訂閱紀錄,補上 user_id - 單一清單退訂:unsubscribe token 綁定 subscription_id - blacklist 記錄於 email_blacklist(全租戶共用) +- email 為會員與訂閱領域的對外主 key,不提供改 email 流程 ## 6. 核心流程 @@ -132,6 +151,32 @@ 4) 系統驗證目前密碼正確後更新 password hash 5) 更新成功後刷新目前 session +### 6.8b 會員基本資料維護 +1) 使用者登入會員中心 +2) 進入 profile 頁 +3) 讀取基本資料(姓、名、nick name、電話、生日、性別、公司、部門、職稱、統編、remark 等) +4) 更新後寫入 `user_profiles` +5) 記錄 audit log + +### 6.8c 地址簿管理 +1) 使用者登入會員中心 +2) 進入地址簿頁面 +3) 新增、編輯或刪除地址 +4) 系統驗證地址資料歸屬目前 user +5) 若設定預設地址,需確保同用途只有一筆預設值 +6) 若只剩最後一筆地址,不允許刪除 +7) 記錄 audit log + +### 6.8d 已登入會員管理自己的訂閱 +1) 使用者登入會員中心 +2) 系統依 `user_id` 讀取已綁定的電子報訂閱 +3) 顯示可管理的訂閱清單 +4) 使用者可直接取消訂閱 +5) 系統驗證訂閱歸屬目前 user 後更新狀態 +6) 發送 `subscription.unsubscribed` 事件 +7) 不需再次經過 email token 驗證 +8) 已綁定 `user_id` 後仍保留 `list_id + email` 的 public 訂閱 / 退訂入口 + ### 6.9 檔案存取授權(File Access) 1) Upload 採 S2S: - `A service` 以 `client_credentials` 向 Member Center 取 token @@ -168,6 +213,41 @@ - `tenant_id` - 檔案識別與 method 是否與 token 一致 +### 6.10 外部資源授權抽象(Audience / Resource Registry) +目的: +- 統一 Send Engine、File Access 與未來其他外部服務的 token 發放規則 +- 避免每新增一個服務就新增一組 `Auth__XAudience` 與對應程式分支 + +共通原則: +- Member Center 為 token issuer 與 JWKS 提供者 +- 外部服務皆以 `iss/aud/exp/scope/tenant_id` 驗證 token +- `aud` 不直接以程式硬編碼,而是由 resource registry 決定 + +resource registry 至少需定義: +- `resource_name` +- `audience` +- `allowed_scopes` +- `allowed_client_usages` +- `requires_tenant` +- `allows_delegated_token` + +建議初始資源: +- `member_center_api` + - scopes:`openid`、`email`、`profile`、`newsletter:list.read`、`newsletter:events.write`、`newsletter:events.write.global`、`profile:basic.read`、`profile:basic.write`、`profile:addresses.read`、`profile:addresses.write`、`profile:subscriptions.read`、`profile:subscriptions.write` + - usages:`tenant_api`、`platform_service`、互動式登入 client +- `send_engine_api` + - scopes:`newsletter:send.write`、`newsletter:send.read` + - usages:`send_api` +- `file_access_api` + - scopes:`files:upload.write`、`files:download.read`、`files:download.delegate`、`files:delete`、`files:metadata.read` + - usages:`file_api` + +設計原則: +- resource registry 由 DB 與管理 UI 管理非敏感欄位 +- `TokenController` 依 scope 與 usage 對照 resource registry 計算 resources / audiences +- delegated token 與一般 S2S token 共用同一套 resource registry,不重複維護 audience 規則 +- 對外資料讀寫授權完全由 scope 決定,只要 client 被授權該 scope 即可存取對應能力 + ## 7. API 介面(草案) - GET `/oauth/authorize` - POST `/oauth/token` @@ -197,10 +277,17 @@ ### Auth / Scope - `tenant_api` / `send_api` / `webhook_outbound` OAuth Client 綁定 `tenant_id`,所有清單/事件 API 需驗證租戶邊界 -- OAuth Client 需區分用途:`tenant_api` / `send_api` / `webhook_outbound` / `platform_service`(禁止混用) +- OAuth Client 需區分用途:`tenant_api` / `send_api` / `webhook_outbound` / `platform_service` / `file_api`(禁止混用) - 新增 scope:`newsletter:list.read`、`newsletter:send.write`、`newsletter:send.read`、`newsletter:events.read` - 新增 scope:`newsletter:events.write` - 新增 scope:`newsletter:events.write.global` +- 規劃新增 profile scopes: + - `profile:basic.read` + - `profile:basic.write` + - `profile:addresses.read` + - `profile:addresses.write` + - `profile:subscriptions.read` + - `profile:subscriptions.write` - 規劃新增 file access scopes: - `files:upload.write` - `files:download.read` @@ -209,12 +296,16 @@ - `files:metadata.read` - 規劃新增 audience:`file_access_api` - JWT Access Token 已改為 JWS(`DisableAccessTokenEncryption`),供 Send Engine 以 JWKS 驗簽 +- `aud` 計算由 resource registry 驅動,不於 token 發放流程硬寫各服務 audience ### 租戶端取 Token(Client Credentials) - 租戶使用 OAuth Client Credentials 向 Member Center 取得 access token - token 內含 `tenant_id` 與 scope - Send Engine 收到租戶請求後以 JWKS 驗簽 JWT(JWS) - 驗簽通過後將 `tenant_id` 固定在 request context,不接受 body 覆寫 +- File Access 與未來外部服務亦沿用此模型,差異由 scopes / audience / delegated token 規則表達 +- 若其他服務要讀取會員個資,應只授予必要的 profile read scopes,不應沿用過寬的 `profile` OIDC scope +- service API 為主要整合模式,存取控制以 scopes 為唯一授權來源 ## 7.2 尚未完成(待辦) - `POST /webhooks/lists/full-sync`:Member Center 端尚未發送此事件(僅保留契約) @@ -222,6 +313,7 @@ - `subscription.linked_to_user` 事件已發送 - 安全設定頁(access/refresh 時效)目前僅存值,尚未實際套用到 OpenIddict token lifetime - Audit Logs 目前以查詢為主,關鍵操作的寫入覆蓋率仍不足 +- resource registry 與 file access token issuing 尚未實作,現況仍是兩組 audience 硬編碼 ## 8. 安全與合規 - 密碼強度與防暴力破解(rate limit + lockout) @@ -229,6 +321,8 @@ - Redirect URI 白名單 + PKCE - Double opt-in(可配置) - Audit log +- delegated download token 需短效、不可重放,必要時可引入 `jti` 與 nonce/jti blacklist +- Email 可作為未來 MFA 的挑戰通道 - GDPR/CCPA:資料匯出與刪除(規劃中) ## 9. 其他文件 diff --git a/docs/FLOWS.md b/docs/FLOWS.md index 13163c9..d4c1910 100644 --- a/docs/FLOWS.md +++ b/docs/FLOWS.md @@ -74,6 +74,15 @@ - [API] 站點以 `list_id + email` 更新 `/newsletter/preferences` - [UI] 會員中心提供偏好頁(可選) +## F-06b 我的電子報訂閱管理(登入後) +- [UI] 使用者登入後進入 `/profile/subscriptions` +- [API/UI] 會員中心依目前登入 user 查出其已綁定的訂閱清單 +- [UI] 顯示各租戶 / 清單 / 狀態 / 訂閱時間 +- [UI] 使用者可直接按下取消訂閱 +- [API/UI] 系統驗證該訂閱屬於目前登入 user 後直接退訂 +- [API/UI] 不需再次 email token 驗證 +- [API] 發出 event `subscription.unsubscribed` + ## F-10 Send Engine 事件同步(Member Center → Send Engine) - [API] Member Center 以 webhook 推送 `subscription.activated/unsubscribed/preferences.updated`(scope: `newsletter:events.write`) - [API] Header 使用 `X-Signature` / `X-Timestamp` / `X-Nonce` / `X-Client-Id` @@ -101,6 +110,24 @@ - [API] 站點讀取 `/user/profile` - [UI] 會員中心提供個人資料頁 +## F-07b 會員資料維護 +- [UI] 使用者登入後進入 `/profile` +- [UI] 維護電話、公司、統編等基本資料 +- [API/UI] 儲存會員基本資料 + +## F-07c 地址簿管理 +- [UI] 使用者登入後進入 `/profile/addresses` +- [UI] 新增、編輯、刪除收貨地址 +- [UI] 可設定預設地址 +- [API/UI] 地址簿資料綁定目前登入 user +- [API/UI] 若只剩最後一筆地址,不允許刪除 + +## F-07d 其他服務讀取會員資料 +- [API] 其他服務以 access token 呼叫會員中心 profile API +- [API] token 需帶對應 profile scopes +- [API] 會員中心依 scope 決定可讀取基本資料、地址簿或訂閱資料 +- [API] service API 為主要模式,只要 client 具對應 scope 即可存取對應資料 + ## F-08 管理者管理租戶/清單/Client - [UI] 會員中心管理後台進行 CRUD diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 63e6bc6..89e9691 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -41,12 +41,20 @@ ASPNETCORE_ENVIRONMENT=Development ConnectionStrings__Default=Host=localhost;Database=member_center;Username=postgres;Password=postgres Auth__Issuer=http://localhost:7850/ -Auth__MemberCenterAudience=member_center_api -Auth__SendEngineAudience=send_engine_api +Auth__Resources__MemberCenter__Audience=member_center_api +Auth__Resources__SendEngine__Audience=send_engine_api +Auth__Resources__FileAccess__Audience=file_access_api SendEngine__BaseUrl=http://localhost:6060 SendEngine__WebhookSecret=change-me ``` +相容性說明: +- 現行程式仍使用: + - `Auth__MemberCenterAudience` + - `Auth__SendEngineAudience` +- 規劃上將收斂為 `Auth__Resources__*__Audience` 形式,避免每新增一個外部服務就再增加一組平行 env key。 +- `File Access` 落地時,應直接採用 resource registry 形式,不再新增第三組硬編碼 audience 判斷。 + `SendEngine` 設定說明: - `SendEngine__BaseUrl`: Send Engine API base URL - `SendEngine__WebhookSecret`: 與 Send Engine `Webhook:Secrets:member_center` 一致 diff --git a/docs/MEMBER_UPGRADE_PLAN.md b/docs/MEMBER_UPGRADE_PLAN.md new file mode 100644 index 0000000..1d8bee5 --- /dev/null +++ b/docs/MEMBER_UPGRADE_PLAN.md @@ -0,0 +1,603 @@ +# 會員中心升級規劃 + +此文件整理會員中心的目標升級架構,包含設定畫面、Email 驗證與忘記密碼、帳號分級與角色管理、會員主資料、訂閱管理,以及作為外部服務 Token / Auth 中心的整體模型。此文件以一次到位的最終設計為準,不再以過渡方案或分階段落地作為主軸。 + +## 目標 +- 增加管理者可操作的系統設定畫面,涵蓋 SMTP、Send Engine 與 Auth 資源設定。 +- 補齊 Email 驗證與忘記密碼的完整寄信流程。 +- 建立帳號分級規則,明確區分 `superuser`、`admin` 與一般會員。 +- 確立會員認證狀態模型,以「已認證 / 未認證」為會員狀態基礎。 +- 第三方登入以 Google 為唯一支援 provider。 +- 將「會員中心作為 Token / Auth 中心」的外部服務授權模型文件化,納入 Send Engine 與 File Access。 +- 擴充會員個人資料、地址簿與會員端訂閱管理能力,並同步定義可供其他服務使用的 profile scopes。 +- 補齊帳號生命週期、審計紀錄、rate limit 與 MFA 的基礎治理規則。 + +## 現況盤點 + +### 已存在 +- `MemberCenter.Web` 與 `MemberCenter.Api` 已有本地註冊、登入、忘記密碼、重設密碼、Email 驗證入口。 +- `MemberCenter.Web/Controllers/AccountController.cs` 已有: + - `ForgotPassword` + - `ResetPassword` + - `VerifyEmail` + - `ExternalLogin` / `ExternalLoginCallback` +- `MemberCenter.Api/Controllers/AuthController.cs` 已有: + - `POST /auth/register` + - `POST /auth/password/forgot` + - `POST /auth/password/reset` + - `GET /auth/email/verify` +- Google external login 已在 `src/MemberCenter.Web/Program.cs` 接入,並已支援同 email auto-link。 +- Installer 已可建立初始管理帳號,但角色模型仍需調整為文件定義的 `superuser`。 +- 管理後台已有 `/admin/security` 畫面,目前僅提供 token 時效設定: + - `AccessTokenMinutes` + - `RefreshTokenDays` +- `MemberCenter.Web` 已有 `/profile` 頁面,但目前僅顯示: + - Email + - Email 驗證狀態 + - 建立時間 +- `MemberCenter.Api` 已有 `GET /user/profile`,但目前只提供基礎欄位,不足以支撐其他服務查詢完整會員資料。 +- `docs/DESIGN.md` / `docs/FLOWS.md` 已定義 File Access 流程: + - upload: `client_credentials` 取得 upload token 後由外部服務直連 file space + - download: 由業務服務驗權後再簽發短效 delegated download token +- `TokenController.ResolveResources()` 已依 scope 決定 token audience,目前內建: + - `member_center_api` + - `send_engine_api` + +### 部分實作 +- 忘記密碼與 Email 驗證目前已有 token 產生與驗證邏輯,但尚未接入實際郵件寄送。 +- `SendEngine__BaseUrl`、`SendEngine__WebhookSecret` 已有設定來源與 options binding,但尚未進入可編輯的管理畫面。 +- 系統目前已有 `admin` role policy,但尚未明確拆出 `superuser` 與「由 superuser 指派 admin」的完整流程。 + +### 待補項 +- SMTP 設定的持久化、管理 UI 與寄信服務抽象。 +- Email 驗證信與忘記密碼信的正式寄送流程。 +- 會員狀態與角色管理規則文件化,並同步到 UI/API/Installer 行為。 +- 帳號管理畫面與權限控制。 +- File Access 的 OAuth client usage、scope、audience 與 delegated token 發放流程尚未落地到程式。 +- Token resource / audience 的設定方式目前仍偏硬編碼,尚未抽象成可擴充模型。 +- 會員個人資料欄位、地址簿、個人資料 API 與 scope 尚未定義。 +- 會員端缺少「我的訂閱」頁面,無法在登入後集中管理已訂閱的電子報。 + +## 功能規劃 + +### 1. 系統設定畫面 + +#### 1.1 主要新增項 +- 新增獨立的「系統設定」或「整合設定」畫面。 +- 第一批可編輯設定: + - SMTP Host + - SMTP Port + - SMTP Username + - SMTP Password + - SMTP From Name + - SMTP From Address + - SMTP Enable SSL / TLS + - `SendEngine__BaseUrl` + - `SendEngine__WebhookSecret` + +#### 1.2 可一併納入的既有設定 +- 目前 `/admin/security` 已有的 token 時效設定可整併進同一個設定體系: + - Access token 分鐘數 + - Refresh token 天數 +- `SendEngineWebhookOptions.SubscriptionEventsPath` 目前已有預設值 `/webhooks/subscriptions`,若未來有多環境或反向代理差異,可評估是否也納入設定畫面。 + +#### 1.3 暫不建議放入設定畫面的項目 +- `ConnectionStrings__Default` +- `Auth__Issuer` +- `PathBase` +- Google OAuth Client Secret + +以上屬部署層級或高風險設定,建議仍以環境變數或部署設定管理,不在一般管理 UI 直接編輯。 + +#### 1.4 設定保存策略 +- 所有可運行期調整的設定統一收斂到 DB,例如沿用 `system_flags` 或新增專用設定表。 +- 敏感值至少需要: + - UI 遮罩顯示 + - 審計紀錄 + - 更新權限限制 +- Secret 類設定需明確區分: + - 可明文顯示的設定 + - 僅能覆寫、不可回顯的 secret 類設定 + +### 2. Email 驗證與忘記密碼 + +#### 2.1 目標狀態 +- 註冊後自動寄送 Email 驗證信。 +- 使用者可重新發送驗證信。 +- 忘記密碼送出後寄送 reset password email。 +- Web UI 與 API 共享同一套 token 與寄信服務邏輯。 + +#### 2.2 目前已有的基礎 +- Identity token provider 已可產生: + - email confirmation token + - password reset token +- Web 與 API 已有 verify/reset endpoint。 +- View 已存在: + - `ForgotPassword` + - `ResetPassword` + - `VerifyEmailResult` + +#### 2.3 待補實作 +- 郵件發送 abstraction,例如 `IEmailSender`。 +- SMTP provider 實作與設定綁定。 +- 註冊後觸發發送驗證信。 +- 忘記密碼改為寄送信件,而不是在畫面或 API 直接回傳 token。 +- Email 樣板: + - 驗證信 + - 忘記密碼信 +- 重送驗證信入口。 + +#### 2.4 安全與產品規則 +- 忘記密碼 API 與 UI 都應避免暴露帳號是否存在。 +- Reset token 與 verify token 的 URL 應統一由設定的 public base URL 組出。 +- 驗證信與重設密碼信都應記錄 audit log。 +- 會員未驗證時是否允許登入與可操作範圍,需在實作時明確固定為單一規則,不保留模糊狀態。 + +### 3. 帳號分級與角色管理 + +#### 3.1 角色模型 +- `superuser` + - 只能透過 installer 建立或提升。 + - 可管理所有帳號角色。 + - 可授予或移除 `admin`。 +- `admin` + - 必須先以一般會員身分註冊。 + - 再由 `superuser` 手動授權。 + - 不可自行提升其他帳號為 `admin`,除非後續明確擴權。 +- `member` + - 預設註冊身分。 + - 依 Email 驗證狀態再分為: + - `unverified` + - `verified` + +#### 3.2 會員分級原則 +- 會員狀態以「已認證 / 未認證」為基礎。 +- 若未來擴充,可在不破壞既有角色模型下新增: + - VIP / 付費會員 + - 停權 / 凍結 + - 企業帳號等級 + +#### 3.3 對現行實作的調整方向 +- 現行 `admin` role 應重新定義: + - Installer 建立的高權限帳號應直接改為 `superuser`。 + - 管理後台中的角色治理操作應為 `superuser` 專屬。 +- 需要新增帳號管理頁面,至少包含: + - 查詢帳號 + - 檢視 Email 驗證狀態 + - 指派或移除 `admin` + - 顯示是否為 installer 建立之 `superuser` + +#### 3.4 權限規則 +- 只有 `superuser` 可變更角色。 +- `admin` 可進入既有管理後台,但不可升降他人權限。 +- 一般會員不可看見 `/admin/*`。 +- 未來若導入更多後台功能,建議區分: + - 後台使用權限 + - 帳號治理權限 + +### 3.5 帳號生命週期 +- 帳號狀態至少包含: + - `active` + - `disabled` + - `locked` +- Email 驗證狀態獨立於帳號狀態,維持: + - `unverified` + - `verified` +- Email 為對外主識別 key,不允許修改。 +- 若使用者要更換 email,採重新註冊新帳號,不提供原帳號改 email 流程。 +- 後台需保留帳號停用能力;停用後不得再登入與取得新 token。 +- 建議記錄帳號治理欄位: + - `last_login_at` + - `last_seen_at` + - `disabled_at` + - `disabled_by` +- superuser 重設他人密碼、停用帳號、解除停用等治理規則,先記入規劃,細節待後續檢討。 + +### 4. 第三方登入 +- 只支援 Google。 +- 目前 Google external login 已存在,後續只補齊與整體權限模型的一致性。 +- 若 Google 回傳 email 已驗證,可直接把會員標記為 `verified`;目前 `AccountProvisioningService` 已有依 external login provider 回填 `EmailConfirmed` 的基礎邏輯。 + +### 5. 會員基本資料與地址簿 + +#### 5.1 會員基本資料 +- 會員可自行維護的基本資料欄位包含: + - `last_name` + - `first_name` + - `nick_name` + - `mobile_phone` + - `landline_phone` + - `date_of_birth` + - `gender` + - 公司名稱 + - 部門 + - 職稱 + - 公司電話 + - 統一編號 + - 發票抬頭 + - `remark` +- Email 為固定欄位,不提供變更流程;如需更換 email,採重新註冊。 +- Email 同時作為對外 API 的主 key。 + +建議必填欄位: +- `last_name` +- `first_name` + +建議選填欄位: +- `nick_name` +- `mobile_phone` +- `landline_phone` +- `date_of_birth` +- `gender` +- `company_name` +- `department` +- `job_title` +- `company_phone` +- `tax_id` +- `invoice_title` +- `remark` + +建議 enum: +- `gender` + - `male` + - `female` + - `other` + - `unspecified` + +欄位驗證建議: +- `first_name`、`last_name` + - 必填 + - 長度 `1-100` +- `nick_name` + - 長度 `0-100` +- `mobile_phone`、`landline_phone`、`company_phone` + - 儲存標準化字串 + - 盡量接近 E.164,市話可接受本地格式輸入後正規化 +- `company_name`、`department`、`job_title` + - 長度 `0-200` +- `tax_id` + - 長度 `0-32` + - 驗證以台灣統編規則為主,但 schema 保留國際延展空間 +- `invoice_title` + - 長度 `0-200` +- `remark` + - 長度 `0-1000` +- `date_of_birth` + - 使用 `date` 型別,不使用 datetime + +資料使用原則: +- `first_name + last_name` 為正式姓名來源 +- `nick_name` 為顯示用途,不作為唯一鍵 +- `mobile_phone` 優先於 `landline_phone` 作為主要聯絡電話 + +#### 5.2 地址簿 / 收貨地址清單 +- 新增會員地址簿概念,支援一位會員維護多筆地址。 +- 地址欄位包含: + - `label` + - 收件人姓名 + - 收件人電話 + - `country_code` + - `postal_code` + - `state_region` + - `city` + - `district` + - `address_line1` + - `address_line2` + - 公司名稱(可選) + - 是否預設地址 + - 地址用途 + - `address_meta_json` +- 地址用途建議先預留列舉: + - `shipping` + - `billing` + - `both` +- 資料格式規則: + - 以台灣使用情境為主 + - 國碼、電話、郵遞區號、國家碼等欄位盡量符合國際格式 + - 統編以台灣統編規則為主 + - 地址採「結構化欄位 + `address_meta_json` 補充資料」混合設計,不採純 JSON +- 刪除規則: + - 若會員只剩最後一筆地址,不允許刪除 + - 若刪除的是預設地址,系統需自動補選新的預設地址 + +建議必填欄位: +- `user_id` +- `label` +- `recipient_name` +- `recipient_phone` +- `country_code` +- `address_line1` +- `usage` +- `is_default` + +建議選填欄位: +- `postal_code` +- `state_region` +- `city` +- `district` +- `address_line2` +- `company_name` +- `address_meta_json` + +建議 enum: +- `usage` + - `shipping` + - `billing` + - `both` + +欄位驗證建議: +- `label` + - 長度 `1-100` +- `recipient_name` + - 必填 + - 長度 `1-100` +- `recipient_phone` + - 必填 + - 儲存標準化字串 +- `country_code` + - 必填 + - 使用 ISO 3166-1 alpha-2 + - 固定 2 碼大寫 +- `postal_code` + - 長度 `0-20` +- `state_region`、`city`、`district` + - 長度各 `0-100` +- `address_line1` + - 必填 + - 長度 `1-255` +- `address_line2` + - 長度 `0-255` +- `company_name` + - 長度 `0-200` +- `address_meta_json` + - 可為 `null` + +資料規則: +- 同一個 `user_id + usage` 最好限制只能有一筆 `is_default = true` +- `label` 可為自由文字,不先做 enum +- 地址至少保留一筆,因此最後一筆不可刪除 + +#### 5.3 資料治理原則 +- 會員基本資料與地址簿屬於會員中心主資料,可供其他服務查詢,但寫入權限需嚴格控管。 +- 會員本人可編輯自己的基本資料與地址簿。 +- 其他服務預設只有讀取權限。 +- 若其他服務需要代會員寫入,必須有額外 scope 與審計規則。 + +### 6. 會員資料 API 與 Auth Scope 規範 + +#### 6.1 規劃目的 +- 讓其他服務可透過 API 取得會員中心的基本資料與地址資料。 +- 在一開始就把 scope 邊界定清楚,避免未來 profile API 無限制外放。 + +#### 6.2 建議 scopes +- `profile:basic.read` + - 讀取會員基本資料 +- `profile:basic.write` + - 更新會員基本資料 +- `profile:addresses.read` + - 讀取會員地址簿 +- `profile:addresses.write` + - 新增、修改、刪除會員地址簿 +- `profile:subscriptions.read` + - 讀取會員已訂閱電子報清單 +- `profile:subscriptions.write` + - 透過已登入會員介面取消訂閱或調整會員自己的訂閱狀態 + +#### 6.3 API 邊界建議 +- 其他服務 API: + - 目前規劃以 service API 為主 + - 只要 Auth 設定有授與對應 scope,該服務即可存取對應資料 + - 讀寫能力完全由 scope 控制 + - 預設以最小權限授權,不因 client 類型自動放寬資料邊界 +- 會員本人 UI / API: + - 可讀寫自己的 profile 與地址 + - 可查看自己的訂閱清單 + - 可對自己的訂閱直接退訂,不需再次 email token 確認 +- 管理後台: + - 可查詢會員資料,但是否可編輯應另行定義,不與一般會員本人編輯權混用 + +#### 6.4 與 OIDC 標準 scope 的關係 +- 目前 `openid email profile` 中的 `profile` scope 太寬泛,不足以承載業務上需要的個資與地址簿授權控制。 +- 建議保留 OIDC `profile` 作為基本 claims 用途,但業務資料改由自訂 scope 控制。 + +### 7. 會員端訂閱管理(我的訂閱) + +#### 7.1 目標 +- 會員登入後,可集中查看自己目前已訂閱的電子報清單。 +- 會員可直接從此介面取消訂閱,不需要再次透過 email token 驗證。 +- 此流程與 email 內的一鍵退訂屬不同入口: + - email 退訂:適用未登入或直接從信件操作 + - 會員中心退訂:適用已登入且已確認是本人 + +#### 7.2 UI 能力 +- 顯示已訂閱清單: + - 所屬 tenant / 站台 + - 電子報清單名稱 + - 訂閱狀態 + - 訂閱建立時間 + - 最後更新時間 +- 可執行: + - 直接取消訂閱 + - 未來可擴充為偏好調整或重新訂閱 + +#### 7.3 流程規則 +- 已登入會員對自己的訂閱執行退訂時,不需 email token 二次確認。 +- 系統仍需: + - 驗證 subscription 確實屬於目前登入會員 + - 寫入 audit log + - 發送 `subscription.unsubscribed` 事件 +- 若該 email 已在黑名單中,行為需與既有退訂規則一致。 +- 即使已綁定 `user_id`,仍保留既有以 `list_id + email` 進行訂閱 / 退訂的 public 流程。 +- 會員可重新訂閱已退訂的電子報,規則與既有 public subscribe 流程保持一致。 + +#### 7.4 與現行訂閱 API 的差異 +- 現行 `/newsletter/preferences` 偏向以 `list_id + email` 操作,主要照顧跨站 API 邊界。 +- 新的「我的訂閱」介面應改以登入者身份為主體,不再要求使用者自行輸入 email。 +- 建議新增一組以 current user 為主體的 API,而不是把既有未登入流程硬改成同一支。 + +### 7.5 訂閱識別原則 +- Email 是訂閱領域的主 key。 +- `user_id` 為已註冊會員與訂閱資料的關聯鍵,不取代 email 在訂閱流程中的角色。 +- 因此系統同時保留: + - public flow:`list_id + email` + - member flow:`current user` +- 兩種入口最終都回到同一組 subscription 資料。 + +### 8. 會員中心作為外部服務的 Token / Auth 中心 + +#### 8.1 共通模型 +- Send Engine 與 File Access 本質上是同一套模型: + - 由 Member Center 簽發 access token + - 外部服務以 Member Center JWKS 驗簽 + - 依 `iss/aud/exp/scope/tenant_id` 做授權與租戶邊界控制 +- 差異只在資源類型不同: + - Send Engine 偏向 service-to-service API 呼叫 + - File Access 除了 upload 的 S2S token 外,download 還需要 delegated short-lived token + +#### 8.2 File Access 納入規劃 +- 新增 OAuth client usage,建議至少區分: + - `file_api` + - 或更明確拆成 `file_upload_api` / `file_download_delegate` +- 新增 scopes: + - `files:upload.write` + - `files:download.read` + - `files:download.delegate` + - `files:delete` + - `files:metadata.read` +- 新增 audience: + - `file_access_api` +- 新增 delegated token 規則: + - 下載 token 必須短效 + - 必須綁定 `tenant_id` + - 必須綁定 `file_id` 或 `object_key` + - 必須綁定 `method` + - 不可直接重用一般 S2S access token 給 client + +#### 8.3 目標設計 +- Auth 資源設定採 resource registry,不再以一組一組 `Auth__XAudience` 擴充。 +- 每個外部資源服務都以統一結構註冊: + - resource name + - audience + - scopes + - client usages + - 是否需要 `tenant_id` + - 是否允許 delegated token +- 新增資源服務時,只擴充 registry 與授權規則,不再修改硬編碼 audience 分支。 +- 所有外部資料存取均以 scope 作為唯一授權依據。 + +#### 8.4 授權模型 +- OAuth client usage 與 resource 綁定: + - `tenant_api` -> `member_center_api` + - `send_api` -> `send_engine_api` + - `file_api` -> `file_access_api` +- scope 用於細粒度權限控制。 +- `TokenController` 依 scope 與 usage 對照 resource registry 計算 `resources/audiences`。 +- delegated token 與一般 S2S token 共用同一套 token issuing abstraction。 + +### 9. 審計紀錄 +- 下列事件必須寫入 audit log: + - 註冊 + - Email 驗證成功 + - 忘記密碼申請 + - 密碼重設成功 + - 已登入修改密碼 + - Google external login 綁定 + - 角色變更 + - 帳號停用 / 啟用 + - profile 修改 + - 地址簿新增 / 編輯 / 刪除 / 預設地址切換 + - 會員端直接退訂 + - 系統設定修改 + - OAuth client 建立 + - OAuth client secret 旋轉 +- OAuth client secret 顯示一次、旋轉與治理能力視為既有基線;細節之後再檢討。 + +### 10. Rate Limit 與防濫用 +- 下列入口必須有 rate limit: + - login + - forgot password + - resend verification + - register + - public subscribe + - unsubscribe token 申請 + - one-click unsubscribe token 申請 +- lockout 與 rate limit 需能區分: + - 人類使用者登入 + - service API token 申請 + +### 11. MFA 與非本期項目 +- 在 Email 寄送能力完成後,可規劃 Email-based MFA / challenge 驗證。 +- 其餘先明確列為未來可補項: + - consent / terms acceptance + - 資料匯出 / 刪除 + - login history / device management + - 通知偏好中心 + +## 一次到位的實作範圍 +1. 設定中心: + - SMTP + - Send Engine + - Auth 資源設定 + - token lifetime +2. Identity 與通知: + - Email 驗證寄信 + - 忘記密碼寄信 + - 重送驗證信 +3. 會員主資料: + - 基本資料 + - 地址簿 + - Profile UI / API +4. 訂閱管理: + - 我的訂閱頁 + - current-user 型訂閱 API + - 直接退訂流程 +5. 權限與角色: + - `superuser` + - `admin` + - `member` + verified / unverified + - 帳號管理與角色指派 +6. Auth 中心: + - resource registry + - profile scopes + - file access scopes + - delegated token +7. 安全治理: + - audit log + - rate limit + - MFA 預留 +8. 文件、測試與 installer 同步調整 + +## 影響範圍 + +### Web +- 新增設定畫面與表單。 +- 新增帳號管理畫面。 +- 調整註冊成功後導引與驗證提醒文案。 +- 新增會員 profile 編輯頁、地址簿頁與我的訂閱頁。 + +### API +- 若設定畫面走 API,需新增設定讀寫端點。 +- 若帳號管理後台走 API,需新增角色管理與帳號查詢端點。 +- 若要支援 File Access,需新增或擴充: + - resource / audience mapping + - file scopes 註冊 + - delegated download token issuing +- 需新增 current-user 型 profile / addresses / subscriptions API 與對應 scopes。 + +### Infrastructure +- 新增 SMTP sender 與設定存取服務。 +- 調整帳號 provisioning 與角色管理服務。 +- 抽出 token resource resolver / delegated token issuer,避免 audience 與 scope 判斷散落在 controller。 +- 新增會員基本資料與地址簿的資料模型與服務層。 + +### Installer +- 建立或提升 `superuser`。 +- 視命名策略決定是否保留 `add-admin`,或改名為更貼近語意的指令。 + +## 文件與實作同步原則 +- 文件中的「已存在」代表已有 controller / route / view / 基礎邏輯,不代表整體功能已完成產品化。 +- 本規劃完成後,應同步回寫: + - `docs/UI.md` + - `docs/FLOWS.md` + - `docs/INSTALL.md` + - `README.md` diff --git a/docs/OPENAPI.md b/docs/OPENAPI.md index c7c564a..36f1429 100644 --- a/docs/OPENAPI.md +++ b/docs/OPENAPI.md @@ -107,6 +107,14 @@ - `newsletter:events.read` - `newsletter:events.write` - `newsletter:events.write.global` +- 規劃新增 profile scopes: + - `profile:basic.read` + - `profile:basic.write` + - `profile:addresses.read` + - `profile:addresses.write` + - `profile:subscriptions.read` + - `profile:subscriptions.write` +- profile 相關 API 以 service API 為主要整合模式,授權完全由 scope 控制 - 規劃新增 file access scopes: - `files:upload.write` - `files:download.read` @@ -122,10 +130,12 @@ - 建議 Send Engine 使用 client credentials 取 token,不建議使用長效固定 token - Send Engine 建議以 JWKS 驗簽 JWT(JWS),並驗證 `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` 覆寫) - - File access 流程:`file_access_api`(建議新增設定) + - `aud` 由 Auth resource registry 決定,而非每個服務各自新增一組獨立設定 + - 標準資源包含: + - Send Engine 流程:`send_engine_api` + - Member Center API 流程:`member_center_api` + - File access 流程:`file_access_api` + - `TokenController` 依 scope / usage 對照 resource registry 計算 `aud` ### File Access Auth(A service / client / access agent) @@ -160,6 +170,10 @@ - token 內檔案識別與實際 request 是否一致 - token 內 method 與實際 request 是否一致 +補充: +- File Access 與 Send Engine 同屬「外部資源服務」,驗證模型一致 +- 差異在於 File Access download token 為 delegated short-lived token,而非一般 client credentials token + ### 回寫原因碼(Send Engine -> Member Center) - `hard_bounce` - `soft_bounce_threshold` diff --git a/docs/UI.md b/docs/UI.md index 7d1111b..2348134 100644 --- a/docs/UI.md +++ b/docs/UI.md @@ -4,7 +4,8 @@ ### 會員端 - 註冊 / 登入 / 忘記密碼 / 修改密碼 - Email 驗證 -- 個人資料(Email 為主) +- 個人資料(基本資料、聯絡方式、公司資訊) +- 收貨地址簿 - 訂閱管理(清單與偏好) - 退訂(單一清單) - 連結外站(可選:回到來源站點) @@ -16,6 +17,7 @@ - 訂閱查詢 / 匯出 - 審計紀錄查詢 - 系統設定(安全策略、token 時效) +- Auth 資源設定(resource / audience / scope / usage mapping) ## 各站自建 UI(API) ### 會員端 @@ -48,15 +50,21 @@ - UC-07 訂閱確認(double opt-in): `/newsletter/confirm?token=...` - UC-08 取消訂閱(單一清單): `/newsletter/unsubscribe?token=...` - UC-09 訂閱偏好管理(登入後): `/newsletter/preferences?list_id=...&email=...` -- UC-10 會員資料查看: `/profile` +- UC-10 會員資料查看 / 編輯: `/profile` +- UC-10.1 收貨地址簿管理: `/profile/addresses` +- UC-10.2 我的電子報訂閱: `/profile/subscriptions` ### 管理者端(統一 UI) - UC-11 租戶管理: `/admin/tenants` - UC-11.1 Tenant 可設定 `Send Engine Webhook Client Id`(UUID) -- UC-12 OAuth Client 管理: `/admin/oauth-clients`(建立時顯示一次 client_secret,可旋轉;可選 `usage=tenant_api` / `send_api` / `webhook_outbound` / `platform_service`;`platform_service` 可不指定 tenant) +- UC-12 OAuth Client 管理: `/admin/oauth-clients`(建立時顯示一次 client_secret,可旋轉;可選 `usage=tenant_api` / `send_api` / `webhook_outbound` / `platform_service` / `file_api`;`platform_service` 可不指定 tenant) - `redirect_uris` 僅 `webhook_outbound` 需要;其餘 usage 不需要 - - `tenant_api` / `send_api` / `platform_service` 強制 `client_type=confidential` + - `tenant_api` / `send_api` / `platform_service` / `file_api` 強制 `client_type=confidential` - UC-13 電子報清單管理: `/admin/newsletter-lists` - UC-14 訂閱查詢 / 匯出: `/admin/subscriptions`, `/admin/subscriptions/export` - UC-15 審計紀錄查詢: `/admin/audit-logs` - UC-16 安全策略設定: `/admin/security` +- UC-16.1 Auth 資源設定:建議整併於 `/admin/security` 或其子頁籤,而非獨立新頁 + - 管理 `Issuer` 顯示資訊與非敏感 Auth 設定 + - 管理 resource registry,例如 `member_center_api`、`send_engine_api`、`file_access_api` + - audience 設定建議放在現有 Auth / Security 設定畫面下,而不是放到 SMTP / Send Engine 整合設定頁