本卡屬 FR-132 A.6「頁內按鈕照權限隱藏」(子母卡見本卡建立順序前一張),第 1 棒,底層。建議 model:opus。
這一棒在做什麼(白話)
讓前端有一份「這個人全部有效權限」可查,全站按鈕都讀它。今天前端判斷讀的是側欄選單帶回的權限,只含綁在頁面上的那些;沒綁頁的 92 項(SSP、證據分類、AI 小幫手…)查起來永遠是「沒有」。改成登入後一次向後端拿全部有效權限,存進 store,統一一個 has() 給全站用。順手把 AI 小幫手右下角浮鈕接上判斷。
在哪裡(首腦核過)
FE hasCap 現況:src/utils/userUtil.js:16-43(從 /user/web-menu 的 permissions 樹扁平化成 Set)
FE 補查:src/composables/useMyCapabilities.js(每頁自己 load effective-access,has = hasCap || bypass || names)
FE 呼叫點:hasCap( 33 處/17 檔;can( 6 處(useRoleCaps)
FE 浮鈕:src/layout/AppLayout.vue:129-140(v-if="showChatBtn" 只看開關,不看權限)
BE 端點:GET /users/<uid>/effective-access(jedi-iam api/routes/role_assignment_route.py:79 → app/service/effective_access_service.py:26-63,回 bypass.super_admin 與 capabilities[{name, sources}]);能力點集合取自判定同一條(explain_active_capabilities)
BE 守門:jedi-iam authz/capability.py;AI 小幫手聊天端點守 ai.assistant.read(jedi-ai-bot plugin/contract.py:37)
怎麼做
- BE:effective-access 回應太重(每項帶 sources,給檢查存取頁用)。加一支輕量端點 GET /users/me/capabilities(或 effective-access 加 ?names_only=1),只回 {bypass: bool, names: [..]},同一條判定、同一個 service,不另展開。守門:登入即可(查自己的)。
- FE:新 store(Pinia)myCapabilities:login 後與 web-menu 同時載入;提供 has(name)、hasAny([...])、ready。登出清空。切租戶要重載(找現有切租戶流程掛上)。
- FE:userUtil.hasCap 改成讀新 store(簽名不變,33 處呼叫不用動);useMyCapabilities 的 load 改成 no-op 並標 deprecated(下一版刪);useRoleCaps.can 同樣讀 store。
- FE:AppLayout.vue 浮鈕 v-if 加 has('ai.assistant.read')。
- 先查再寫:grep 既有 store(src/stores/)有沒有已存權限的地方,有就擴充不新開。
要寫測試
- BE:新端點回的 names 必須等於 explain_active_capabilities 的集合(突變:改成直接查 user_roles,測試要紅)。
手測
- blspan 登入:右下角沒有小幫手按鈕;blsadmin 有。
- 用 SQL 給稽核人員角色加 ai.assistant.read pattern,blspan 重登,按鈕出現;清掉,消失。
- 既有 17 支有 hasCap 的頁面行為不變(抽三頁:設備、流程範本、雲端整合)。
連帶
- docs/claude/frontend-overview.md 權限判斷那段改成「讀 myCapabilities store」;domain-capabilities.md 補一列(canonical 是新 store,useMyCapabilities 退役)。
紀律