diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 6962f7b..aa32d75 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,26 +1,37 @@ -# member.ose.tw 架構總覽 +# member.ose.tw 架構總覽(Keycloak 版) ## 核心模型 -- 業務層:`companies -> sites -> users` -- 功能層:`systems -> modules` -- 權限層:`permission_groups`(群組中心) +- 業務層:`companies -> sites` +- 身分層:`users <-> sites`(多對多,透過 `user_sites`) +- 能力層:`systems -> roles` +- 授權層:`sites <-> roles`(多對多,透過 `site_roles`) -## 權限規則 -- `scope_type` 固定 `site` -- `action` 僅 `view` / `edit`(可同時存在) -- 權限透過群組下發給會員,不走細粒度 direct permission 主流程 +## 權限模型(已定版) +- `permission` 正式改名為 `role`。 +- `role` 僅能指派給 `site`,不可直接指派給 `user`。 +- `user` 的有效角色由以下關聯推導: + - `user_sites`(使用者屬於哪些 site) + - `site_roles`(site 擁有哪些 role) +- 不再使用舊的 `permission_groups` 主流程。 + +## Key 規則 +- `system_key`: `SYyyyyMMddX####` +- `company_key`: `CPyyyyMMddX####` +- `site_key`: `STyyyyMMddX####` +- `role_key`: `RLyyyyMMddX####` + +## Keycloak 同步策略 +- Keycloak 為唯一 IdP。 +- 群組階層:`Company Group -> Site SubGroup`。 +- 系統角色:以 Keycloak client role 表示,對應 DB `roles`。 +- `site_roles` 代表某 Site 擁有的 Keycloak role 集合。 +- 使用者加入 Site 時,透過同步邏輯使其在 IdP 端取得對應角色能力。 ## 後台安全線 -- 所有 `/admin/*` 需 Bearer token -- 後端僅依 `ADMIN_REQUIRED_GROUPS` 判定可否進後台 -- 不在群組就算有網址、有 IdP 帳號也會 403 +- `/admin/*` 必須 Bearer token。 +- 後端以 admin 群組白名單判定是否可進後台。 +- 有 Keycloak 帳號但不在 admin 白名單者,後台 API 一律拒絕。 -## 會員資料與 IdP 對齊(Keycloak 優先) -- `username`:登入帳號(可編輯,可同步) -- `display_name`:顯示名稱(可編輯,可同步到 IdP profile) -- `user_sub`:由 IdP 主體識別值回寫 -- `idp_user_id`:保存 IdP 端 user id(字串),供更新/密碼重設 - -## 密碼流程 -- 目前:後台可觸發重設密碼(產生臨時密碼) -- SMTP 開通後:可再補「發送密碼設定/重設通知」自動化 +## API 白名單 +- 保留 `api_clients` 做系統對系統呼叫控管。 +- 管理後台登入控管與 API client 白名單是兩條獨立安全線。 diff --git a/docs/BACKEND_TASKPLAN.md b/docs/BACKEND_TASKPLAN.md index 8f7d406..7f5c445 100644 --- a/docs/BACKEND_TASKPLAN.md +++ b/docs/BACKEND_TASKPLAN.md @@ -1,17 +1,19 @@ # Backend TaskPlan ## 待辦 -- [ ] 補 Keycloak SMTP 通知流程(密碼設定/重設寄信) -- [ ] 補 `/admin/members` 關鍵操作審計日誌 -- [ ] 補更多 API 測試(members username/password reset 路徑) +- [ ] 重建 schema 與 migration:退場舊表(`modules` 舊權限流程、`permission_groups*`、`user_scope_permissions`),上線 `roles`、`site_roles`、`user_sites`。 +- [ ] 重做 Admin API:`Company/Site/System/Role/User` CRUD 與關聯管理 API。 +- [ ] 重做 Role API:只允許 Site 指派/解除,不提供 user direct role API。 +- [ ] 重做 `/me/permissions/snapshot`:改為 role 聚合格式(由 user_sites + site_roles 推導)。 +- [ ] Keycloak 同步器改版:Company/Site group 同步、System client role 同步、Site 角色套用同步。 +- [ ] Swagger response model 全面更新為 role-site 新模型。 +- [ ] 新增/補齊 API 測試(CRUD、關聯、同步、刪除、錯誤碼)。 ## 進行中 -- [ ] 文件與程式持續對齊(避免規格漂移) +- [ ] 文件與實際回應格式持續對齊(避免文件漂移)。 ## 已完成 -- [x] `/admin/*` 改為 Bearer + admin 群組管控(`ADMIN_REQUIRED_GROUPS`) -- [x] 管理 API 完成 systems/modules/companies/sites/members/permission-groups CRUD -- [x] 會員 upsert/update 可同步 Keycloak -- [x] 會員資料新增 `username` 欄位,與 `display_name` 分離 -- [x] 新增 `POST /admin/members/{user_sub}/password/reset` -- [x] DB 新增 `users.username`(含 migration 腳本) +- [x] Keycloak OIDC 登入主流程(authorize + callback + token)。 +- [x] `/admin/*` Bearer token + admin 群組白名單安全線。 +- [x] API 白名單基礎(`api_clients`)已存在並可管理。 +- [x] 前後端本地啟動流程已可分開運行(backend + frontend)。 diff --git a/docs/DB_SCHEMA.md b/docs/DB_SCHEMA.md index a5e7db4..5ef655e 100644 --- a/docs/DB_SCHEMA.md +++ b/docs/DB_SCHEMA.md @@ -1,43 +1,105 @@ -# DB Schema(現行) +# DB Schema(新架構目標版) -## 真實來源 -- `backend/scripts/init_schema.sql` -- 線上增量:`backend/scripts/migrate_add_users_username.sql` -- 欄位重命名增量:`backend/scripts/migrate_rename_identity_columns.sql` +> 本文件是新架構的目標資料模型,供後端 schema 重建與 migration 依據。 +> DB 真實來源仍以 `backend/scripts/init_schema.sql` 為準。 -## 主要表 -- `users` - - `user_sub` UNIQUE - - `idp_user_id` VARCHAR(128) - - `username` UNIQUE - - `email` UNIQUE - - `display_name` - - `is_active`, `status`, timestamps -- `companies` -- `sites`(`company_id -> companies.id`) -- `systems` -- `modules`(`system_key -> systems.system_key`) -- `permission_groups` -- `permission_group_members`(group + user_sub) -- `permission_group_permissions`(group + site/system/module/action) -- `user_scope_permissions`(相容保留) -- `api_clients`(保留給機器對機器用途) +## 1) companies +- `id` UUID PK default `gen_random_uuid()` +- `company_key` TEXT NOT NULL UNIQUE +- `display_name` TEXT NOT NULL +- `legal_name` TEXT +- `idp_group_id` TEXT +- `status` VARCHAR(16) NOT NULL default `'active'` +- `created_at` TIMESTAMPTZ NOT NULL default `now()` +- `updated_at` TIMESTAMPTZ NOT NULL default `now()` -## 權限規則 -- `scope_type='site'` -- `action in ('view','edit')` +## 2) sites +- `id` UUID PK default `gen_random_uuid()` +- `site_key` TEXT NOT NULL UNIQUE +- `company_id` UUID NOT NULL FK -> `companies(id)` ON DELETE CASCADE +- `display_name` TEXT NOT NULL +- `domain` TEXT +- `idp_group_id` TEXT +- `status` VARCHAR(16) NOT NULL default `'active'` +- `created_at` TIMESTAMPTZ NOT NULL default `now()` +- `updated_at` TIMESTAMPTZ NOT NULL default `now()` -## 會員與 IdP 對齊(Keycloak 優先) -- `users.user_sub` 對應 IdP 主體識別 -- `users.username` 對應 IdP `username` -- `users.display_name` 對應 IdP 顯示名稱 +## 3) systems +- `id` UUID PK default `gen_random_uuid()` +- `system_key` TEXT NOT NULL UNIQUE +- `name` TEXT NOT NULL +- `idp_client_id` TEXT NOT NULL UNIQUE +- `status` VARCHAR(16) NOT NULL default `'active'` +- `created_at` TIMESTAMPTZ NOT NULL default `now()` +- `updated_at` TIMESTAMPTZ NOT NULL default `now()` -## 快速檢查 SQL -```sql -SELECT column_name, data_type -FROM information_schema.columns -WHERE table_name='users' -ORDER BY ordinal_position; +## 4) roles +- `id` UUID PK default `gen_random_uuid()` +- `role_key` TEXT NOT NULL UNIQUE +- `system_id` UUID NOT NULL FK -> `systems(id)` ON DELETE CASCADE +- `name` TEXT NOT NULL +- `description` TEXT +- `idp_role_name` TEXT NOT NULL +- `status` VARCHAR(16) NOT NULL default `'active'` +- `created_at` TIMESTAMPTZ NOT NULL default `now()` +- `updated_at` TIMESTAMPTZ NOT NULL default `now()` +- UNIQUE(`system_id`, `idp_role_name`) -SELECT COUNT(*) FROM users WHERE username IS NULL; -``` +## 5) site_roles +- `id` UUID PK default `gen_random_uuid()` +- `site_id` UUID NOT NULL FK -> `sites(id)` ON DELETE CASCADE +- `role_id` UUID NOT NULL FK -> `roles(id)` ON DELETE CASCADE +- `created_at` TIMESTAMPTZ NOT NULL default `now()` +- UNIQUE(`site_id`, `role_id`) + +## 6) users +- `id` UUID PK default `gen_random_uuid()` +- `user_sub` TEXT NOT NULL UNIQUE +- `idp_user_id` TEXT UNIQUE +- `username` TEXT UNIQUE +- `email` TEXT UNIQUE +- `display_name` TEXT +- `status` VARCHAR(16) NOT NULL default `'active'` +- `is_active` BOOLEAN NOT NULL default `true` +- `created_at` TIMESTAMPTZ NOT NULL default `now()` +- `updated_at` TIMESTAMPTZ NOT NULL default `now()` + +## 7) user_sites +- `id` UUID PK default `gen_random_uuid()` +- `user_id` UUID NOT NULL FK -> `users(id)` ON DELETE CASCADE +- `site_id` UUID NOT NULL FK -> `sites(id)` ON DELETE CASCADE +- `created_at` TIMESTAMPTZ NOT NULL default `now()` +- `updated_at` TIMESTAMPTZ NOT NULL default `now()` +- UNIQUE(`user_id`, `site_id`) + +## 8) auth_sync_state +- `id` UUID PK default `gen_random_uuid()` +- `entity_type` VARCHAR(32) NOT NULL +- `entity_id` UUID NOT NULL +- `last_synced_at` TIMESTAMPTZ +- `source_version` TEXT +- `last_error` TEXT +- `updated_at` TIMESTAMPTZ NOT NULL default `now()` +- UNIQUE(`entity_type`, `entity_id`) + +## 9) api_clients +- `id` UUID PK default `gen_random_uuid()` +- `client_key` TEXT NOT NULL UNIQUE +- `name` TEXT NOT NULL +- `status` VARCHAR(16) NOT NULL default `'active'` +- `api_key_hash` TEXT NOT NULL +- `allowed_origins` JSONB NOT NULL default `'[]'::jsonb` +- `allowed_ips` JSONB NOT NULL default `'[]'::jsonb` +- `allowed_paths` JSONB NOT NULL default `'[]'::jsonb` +- `rate_limit_per_min` INTEGER +- `expires_at` TIMESTAMPTZ +- `last_used_at` TIMESTAMPTZ +- `created_at` TIMESTAMPTZ NOT NULL default `now()` +- `updated_at` TIMESTAMPTZ NOT NULL default `now()` + +## 關聯總結 +- Company 1:N Site +- System 1:N Role +- Site M:N Role(`site_roles`) +- User M:N Site(`user_sites`) +- User 最終角色由 Site 推導,不做 user direct role 指派。 diff --git a/docs/FRONTEND_HANDOFF.md b/docs/FRONTEND_HANDOFF.md index 44fbd27..899b1a0 100644 --- a/docs/FRONTEND_HANDOFF.md +++ b/docs/FRONTEND_HANDOFF.md @@ -1,17 +1,43 @@ -# Frontend Handoff +# Frontend Handoff(Role-Site 模型) -## 目前後端契約重點 -- 後台登入:只吃 Bearer + admin 群組檢查 -- 會員模型:`user_sub`, `username`, `email`, `display_name`, `is_active` -- 會員密碼:支援重設 API(回傳臨時密碼) +## 目標 +前端只實作新模型,不再使用舊 `permission_groups` / `module-action` 流程。 -## 會員頁必做 -1. 新增會員表單欄位:`username`、`email`、`display_name` -2. 編輯會員表單欄位:`username`、`email`、`display_name`、`is_active` -3. 表格欄位要顯示:`user_sub`、`username`、`email`、`display_name` -4. 操作欄新增「重設密碼」按鈕,串 `POST /admin/members/{user_sub}/password/reset` -5. 重設成功後顯示臨時密碼,並提醒管理員安全轉交 +## 主要頁面 +1. 公司管理(CRUD) +- 欄位:`company_key`, `display_name`, `legal_name`, `status` +- 詳情頁需顯示底下 `sites` 列表 -## 其他頁面 -- 仍維持群組中心模型:site/system/module/member + action(view/edit) -- 系統/模組/公司/會員關聯頁面沿用目前 API +2. 站台管理(CRUD) +- 欄位:`site_key`, `company_id`, `display_name`, `domain`, `status` +- 站台詳情需顯示: + - 此站台綁定的 `roles` + - 此站台包含的 `users` + +3. 系統管理(CRUD) +- 欄位:`system_key`, `name`, `idp_client_id`, `status` +- 系統詳情需顯示底下 `roles` 列表 + +4. 角色管理(CRUD) +- 欄位:`role_key`, `system_id`, `name`, `description`, `idp_role_name`, `status` +- 關聯操作:指派到 Site(新增/刪除 `site_roles`) + +5. 會員管理(CRUD) +- 欄位:`user_sub`, `username`, `email`, `display_name`, `is_active`, `status` +- 關聯操作:加入/移除 Site(新增/刪除 `user_sites`) +- 顯示推導角色(唯讀) + +6. API Clients 管理(CRUD) +- 欄位:`client_key`, `name`, `status`, `allowed_origins`, `allowed_ips`, `allowed_paths`, `rate_limit_per_min`, `expires_at` + +## 前端互動規則 +- 角色不可直接綁會員(UI 不提供此操作)。 +- Site 與 User 的關聯調整後,角色顯示即時刷新。 +- 刪除操作一律二次確認,顯示影響提示。 + +## 驗收重點 +- 看不到舊 permission group 流程。 +- 可以完整做: + - Site 綁 Role + - User 綁 Site + - 顯示 User 推導角色 diff --git a/docs/FRONTEND_TASKPLAN.md b/docs/FRONTEND_TASKPLAN.md index 371b9d0..2c7518d 100644 --- a/docs/FRONTEND_TASKPLAN.md +++ b/docs/FRONTEND_TASKPLAN.md @@ -1,16 +1,23 @@ # Frontend TaskPlan ## 待辦 -- [ ] SMTP 通知開通後,補上「發送重設通知」UX 文案 -- [ ] 會員頁重設密碼流程加上二次確認 Dialog -- [ ] 針對大量資料頁面補分頁/搜尋體驗優化 +- [ ] 後台導覽改版:改為 `公司 / 站台 / 系統 / 角色 / 會員 / API Clients`。 +- [ ] 角色管理頁改成「Site 綁 Role」: + - [ ] 可查看 Site 目前擁有角色 + - [ ] 可新增/移除 Site 角色 +- [ ] 會員管理頁改成「User 綁 Site」: + - [ ] 可查看使用者目前所屬 Site + - [ ] 可新增/移除使用者 Site + - [ ] 顯示推導角色(只讀) +- [ ] 公司詳情頁顯示站台列表。 +- [ ] 系統詳情頁顯示角色列表。 +- [ ] 補齊刪除流程與二次確認(公司/站台/系統/角色/會員)。 +- [ ] API 失敗狀態頁統一(401/403/409/422/500)。 ## 進行中 -- [ ] 與最新後端契約持續對齊(members username/password reset) +- [ ] OIDC callback 與 token 持久化穩定性檢查。 ## 已完成 -- [x] Vue3 + JS + Vite + Element Plus + Tailwind 基礎架構 -- [x] admin 基礎頁面:systems/modules/companies/sites/members/permission-groups -- [x] 會員頁新增 `username` 欄位(新增/編輯/列表) -- [x] 會員頁新增「重設密碼」操作按鈕 -- [x] OIDC 登入 + `/me`、`/me/permissions/snapshot` 流程 +- [x] Vue3 + JS + Vite + Element Plus + Tailwind 基礎框架。 +- [x] OIDC 登入按鈕導轉與 callback 路由骨架。 +- [x] 基礎 admin 頁面與 API service 分層已建立。 diff --git a/docs/INTERNAL_API_HANDOFF.md b/docs/INTERNAL_API_HANDOFF.md index 6ac34d0..d6db526 100644 --- a/docs/INTERNAL_API_HANDOFF.md +++ b/docs/INTERNAL_API_HANDOFF.md @@ -1,190 +1,38 @@ -# Internal API Handoff +# Internal API Handoff(新模型) ## Base URL - Local: `http://127.0.0.1:8000` - Prod: 由部署環境提供 -## Auth Headers(每支 `/internal/*` 都必帶) +## Auth Headers(`/internal/*`) - `X-Client-Key: ` - `X-API-Key: ` ## Common Error Response ```json -{ - "detail": "error_code" -} +{ "detail": "error_code" } ``` -常見 `detail`: -- `invalid_client`(401) -- `invalid_api_key`(401) -- `client_expired`(401) -- `origin_not_allowed`(403) -- `ip_not_allowed`(403) -- `path_not_allowed`(403) +## 資源模型(重點) +- `company`: `id`, `company_key`, `display_name`, `legal_name`, `status` +- `site`: `id`, `site_key`, `company_id`, `display_name`, `domain`, `status` +- `system`: `id`, `system_key`, `name`, `idp_client_id`, `status` +- `role`: `id`, `role_key`, `system_id`, `name`, `description`, `idp_role_name`, `status` +- `user`: `id`, `user_sub`, `username`, `email`, `display_name`, `is_active`, `status` -## Endpoints +## 主要端點(目標) +1. `GET /internal/companies` +2. `GET /internal/sites` +3. `GET /internal/systems` +4. `GET /internal/roles` +5. `GET /internal/users` +6. `GET /internal/users/{user_sub}/roles` +- 回傳該 user 透過 site 推導出的最終 roles。 -### GET `/internal/systems` -Response: -```json -{ - "items": [ - { "id": "uuid", "system_key": "ST20260331X1234", "name": "Marketing", "status": "active" } - ], - "total": 1, - "limit": 200, - "offset": 0 -} -``` +## 關聯端點(目標) +1. `POST /internal/site-roles` / `DELETE /internal/site-roles/{id}` +2. `POST /internal/user-sites` / `DELETE /internal/user-sites/{id}` -### GET `/internal/modules` -Response: -```json -{ - "items": [ - { - "id": "uuid", - "module_key": "MD20260331X5678", - "system_key": "ST20260331X1234", - "name": "Campaign", - "status": "active" - } - ], - "total": 1, - "limit": 500, - "offset": 0 -} -``` - -### GET `/internal/companies` -Query: -- `keyword`(optional) -- `limit`(default 500) -- `offset`(default 0) - -Response: -```json -{ - "items": [ - { "id": "uuid", "company_key": "CP20260331X9999", "name": "OSE", "status": "active" } - ], - "total": 1, - "limit": 500, - "offset": 0 -} -``` - -### GET `/internal/sites` -Query: -- `company_key`(optional) -- `limit`(default 500) -- `offset`(default 0) - -Response: -```json -{ - "items": [ - { - "id": "uuid", - "site_key": "ST20260331X1111", - "company_key": "CP20260331X9999", - "name": "main-site", - "status": "active" - } - ], - "total": 1, - "limit": 500, - "offset": 0 -} -``` - -### GET `/internal/members` -Query: -- `keyword`(optional) -- `limit`(default 500) -- `offset`(default 0) - -Response: -```json -{ - "items": [ - { - "id": "uuid", - "user_sub": "idp-uid", - "username": "chris", - "email": "chris@ose.tw", - "display_name": "Chris", - "is_active": true - } - ], - "total": 1, - "limit": 500, - "offset": 0 -} -``` - -### POST `/internal/users/upsert-by-sub` -Request: -```json -{ - "user_sub": "idp-uid", - "username": "chris", - "email": "chris@ose.tw", - "display_name": "Chris", - "is_active": true -} -``` - -Response: -```json -{ - "id": "uuid", - "user_sub": "idp-uid", - "idp_user_id": "idp-user-id-or-uuid", - "username": "chris", - "email": "chris@ose.tw", - "display_name": "Chris", - "is_active": true -} -``` - -### GET `/internal/permissions/{user_sub}/snapshot` -Response: -```json -{ - "user_sub": "idp-uid", - "permissions": [ - { - "scope_type": "site", - "scope_id": "ST20260331X1111", - "system": "ST20260331X1234", - "module": "MD20260331X5678", - "actions": ["view", "edit"] - } - ] -} -``` - -### POST `/internal/idp/users/ensure` -(相容路徑:`/internal/idp/users/ensure`) -Request: -```json -{ - "user_sub": "idp-uid", - "email": "user@example.com", - "username": "user1", - "display_name": "User One", - "is_active": true -} -``` - -Response: -```json -{ - "idp_user_id": "idp-user-id-or-uuid", - "action": "created" -} -``` - -`action` 可能值:`created` / `updated` +## 注意事項 +- 不提供 user direct role 寫入 API。 +- 若其他系統需要判斷某 user 可否做某事,請吃 `users/{user_sub}/roles` 聚合結果。 diff --git a/docs/LOCAL_DEV_RUNBOOK.md b/docs/LOCAL_DEV_RUNBOOK.md index e12919c..d88a242 100644 --- a/docs/LOCAL_DEV_RUNBOOK.md +++ b/docs/LOCAL_DEV_RUNBOOK.md @@ -1,4 +1,4 @@ -# Local Dev Runbook +# Local Dev Runbook(Keycloak) ## 1) 啟動後端 ```bash @@ -13,18 +13,29 @@ npm install npm run dev ``` -## 3) 重要環境變數 -- `backend/.env.development` - - `ADMIN_REQUIRED_GROUPS=member-admin` - - `KEYCLOAK_*` +## 3) 必要環境變數(backend/.env.development) +- `KEYCLOAK_BASE_URL` +- `KEYCLOAK_REALM` +- `KEYCLOAK_CLIENT_ID` +- `KEYCLOAK_CLIENT_SECRET` +- `KEYCLOAK_ADMIN_CLIENT_ID` +- `KEYCLOAK_ADMIN_CLIENT_SECRET` +- `ADMIN_REQUIRED_GROUPS` ## 4) 基本檢查 -- `GET http://127.0.0.1:8000/healthz` -- 登入後打 `GET /me` 應可回資料 -- 非 admin 群組帳號打 `/admin/*` 應回 `403` +1. `GET http://127.0.0.1:8000/healthz` 應為 200。 +2. 前端按「前往 Keycloak 登入」應可成功導轉與回跳。 +3. `GET /me` 登入後應有資料。 +4. 非 admin 群組帳號打 `/admin/*` 應為 403。 -## 5) 會員流程驗收 -1. 新增會員(開啟 `sync_to_idp`) -2. 確認列表可看到新會員與 `user_sub` -3. 點「重設密碼」,取得臨時密碼 -4. 到 Keycloak 驗證該會員可用新密碼登入 +## 5) 新模型驗收路徑 +1. 新增 Company、Site。 +2. 新增 System、Role。 +3. 對 Site 指派 Role。 +4. 新增 User,加入 Site。 +5. 驗證 User 的角色是由 Site 推導,不是 direct assign。 + +## 6) API 白名單驗收 +1. 建立 `api_client`。 +2. 用 `X-Client-Key` + `X-API-Key` 呼叫 `/internal/*`。 +3. 驗證未授權 key 會被拒絕。 diff --git a/docs/index.md b/docs/index.md index 9ac32c8..f266ba5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,4 +1,4 @@ -# member.ose.tw 文件入口 +# member.ose.tw 文件入口(新架構) ## 閱讀順序 1. `docs/ARCHITECTURE.md` @@ -6,16 +6,26 @@ 3. `docs/BACKEND_TASKPLAN.md` 4. `docs/FRONTEND_TASKPLAN.md` 5. `docs/FRONTEND_HANDOFF.md` -6. `docs/LOCAL_DEV_RUNBOOK.md` +6. `docs/INTERNAL_API_HANDOFF.md` +7. `docs/LOCAL_DEV_RUNBOOK.md` + +## 交辦順序(建議) +1. 先看 `ARCHITECTURE.md` 鎖定資料模型與權限模型。 +2. 再看 `DB_SCHEMA.md` 對齊 table/欄位/關聯。 +3. 後端依 `BACKEND_TASKPLAN.md` 執行 schema/API/Keycloak 同步調整。 +4. 前端依 `FRONTEND_TASKPLAN.md` + `FRONTEND_HANDOFF.md` 開工。 +5. 其他系統串接時看 `INTERNAL_API_HANDOFF.md`。 ## 目前狀態 -- 架構:公司/站台/會員 + 系統/模組 + 群組整合權限(已定版) -- 後台安全:Auth token + admin 群組檢查(`ADMIN_REQUIRED_GROUPS`) -- 會員流程:member 新增/更新可同步 Keycloak,並支援重設密碼 +- 架構定版:`Company -> Site`、`System -> Role`。 +- 權限定版:`Role` 只能指派給 `Site`(透過 `site_roles`)。 +- 成員授權定版:`User` 不直接綁 `Role`,僅透過 `user_sites` 取得 Site 角色。 +- IdP 定版:Keycloak 為唯一 IdP。 +- API 白名單:保留 `api_clients`。 ## 單一真實來源 - DB SQL:`backend/scripts/init_schema.sql` -- DB 線上補丁:`backend/scripts/migrate_add_users_username.sql` -## 備註 -- 本輪先維持可開發/可交辦文件,不產最終規格總表。 +## 文件邊界 +- 本輪只保留可開發、可交辦、可驗收文件。 +- 最終規格白皮書延後到專案完成後再產出。