docs: rebuild architecture and taskplans for role-site model
This commit is contained in:
@@ -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 白名單是兩條獨立安全線。
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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 指派。
|
||||
|
||||
@@ -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 推導角色
|
||||
|
||||
@@ -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 分層已建立。
|
||||
|
||||
@@ -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: <client_key>`
|
||||
- `X-API-Key: <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` 聚合結果。
|
||||
|
||||
@@ -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 會被拒絕。
|
||||
|
||||
@@ -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`
|
||||
|
||||
## 備註
|
||||
- 本輪先維持可開發/可交辦文件,不產最終規格總表。
|
||||
## 文件邊界
|
||||
- 本輪只保留可開發、可交辦、可驗收文件。
|
||||
- 最終規格白皮書延後到專案完成後再產出。
|
||||
|
||||
Reference in New Issue
Block a user