ADR-007: Applet 層級權限模型
狀態
已採用 (Accepted) - 2025-12-20
變更歷史:
- 2025-12-19: 初版 - Role + Authority 雙層模型
- 2025-12-20: 簡化版 - 前端只用 Role,API 層用 R/W/X Authority
- 2025-12-20: 最終版 - 移除 Authority,前端只做 Applet 層級控制
- 2025-12-20: 角色格式統一 - 全系統使用 ROLE_XXX 格式
- 2025-12-20: 角色繼承與扁平化 - 模擬 Spring Security Role Hierarchy
背景 (Context)
問題陳述
原有的 Role + Authority 設計過於複雜:
- 前端維護 Authority 定義和 Role-to-Authority 映射
- Mock 層需要對每個 API 做權限檢查
- 組件層需要判斷用戶是否有權限執行特定操作
經過評估,決定大幅簡化權限模型,遵循以下原則:
- 前端權限以 Applet 為單位:只用 RoleGuard 保護 Applet 入口
- 進入即全功能:通過 RoleGuard 後,該 Applet 所有功能都可用
- 後端兜底:實際環境由 Spring Security 提供 API 層保護
- Mock 不做權限管制:只做認證(token 有效性),不做權限檢查
簡化動機
- 前端是參考實作,不應承擔權限控制的責任
- 真正的權限控制由後端 Spring Security 處理
- 如果某角色只需要部分功能,應設計獨立的 Applet,而非在組件層做權限檢查
- Mock 環境的目的是開發與測試,不需要模擬權限拒絕
決策 (Decision)
核心設計
┌─────────────────────────────────────────┐
│ UI 層:Role → Applet │
│ 「誰可以進入哪個功能模組」 │
│ - 前端只需要 Role │
│ - RoleGuard 是唯一的權限檢查點 │
│ - 進入 Applet 後所有功能都可用 │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ API 層:後端 Spring Security │
│ 「對資源可以讀還是寫還是執行」 │
│ - 前端不做權限判斷 │
│ - 前端收到 403 就顯示無權限 │
│ - Mock 只驗證 token,不做權限檢查 │
└─────────────────────────────────────────┘
Role 定義
全系統統一使用 Spring Security 格式(ROLE_XXX):
| Role | 說明 |
|---|---|
ROLE_ADMIN | 系統管理員 |
ROLE_OWNER | 店主 |
ROLE_MANAGER | 店長 |
ROLE_SALES | 銷售人員 |
ROLE_ACCOUNTANT | 會計 |
ROLE_PURCHASER | 採購 |
ROLE_FLORIST | 花藝師 |
ROLE_DELIVERY | 配送員 |
角色繼承(Role Hierarchy)
後端 Spring Security 使用角色繼承機制,前端收到扁平化的 roles 陣列:
ROLE_ADMIN
└── ROLE_OWNER
├── ROLE_MANAGER
│ ├── ROLE_SALES
│ └── ROLE_ACCOUNTANT
├── ROLE_PURCHASER
├── ROLE_FLORIST
└── ROLE_DELIVERY
扁平化 roles 陣列範例:
| 主要角色 | 回傳的 roles 陣列 |
|---|---|
ROLE_ADMIN | [ADMIN, OWNER, MANAGER, SALES, ACCOUNTANT, PURCHASER, FLORIST, DELIVERY] |
ROLE_OWNER | [OWNER, MANAGER, SALES, ACCOUNTANT, PURCHASER, FLORIST, DELIVERY] |
ROLE_MANAGER | [MANAGER, SALES, ACCOUNTANT] |
ROLE_SALES | [SALES] |
ROLE_FLORIST | [FLORIST] |
設計優點:
allowedRoles只需指定「最基本需要的角色」- 例如:訂單管理只需
[ROLE_SALES],店主自動有權限(因為繼承 SALES) - 大幅簡化前端配置
Role → Applet 對應(簡化版)
由於角色繼承,allowedRoles 只需指定最基本的角色:
| Applet | allowedRoles | 說明 |
|---|---|---|
| orders | [SALES] | OWNER/MANAGER 自動有權限 |
| customers | [SALES] | OWNER/MANAGER 自動有權限 |
| products | [PURCHASER, FLORIST] | OWNER 自動有權限 |
| sales | [SALES] | OWNER 自動有權限 |
| design | [FLORIST] | OWNER 自動有權限 |
| delivery | [DELIVERY] | OWNER 自動有權限 |
| reports | [ACCOUNTANT] | OWNER/MANAGER 自動有權限 |
| analytics | [ACCOUNTANT] | OWNER/MANAGER 自動有權限 |
| settings | [ADMIN] | 僅 ADMIN |
共用 Applet(空陣列 = 所有角色可見): home, calendar, messages
實施細節
1. 角色繼承映射
// src/types/auth.ts
export const ROLE_HIERARCHY: Record<RoleName, RoleName[]> = {
[Role.ADMIN]: [Role.ADMIN, Role.OWNER, Role.MANAGER, Role.SALES, Role.ACCOUNTANT, Role.PURCHASER, Role.FLORIST, Role.DELIVERY],
[Role.OWNER]: [Role.OWNER, Role.MANAGER, Role.SALES, Role.ACCOUNTANT, Role.PURCHASER, Role.FLORIST, Role.DELIVERY],
[Role.MANAGER]: [Role.MANAGER, Role.SALES, Role.ACCOUNTANT],
[Role.SALES]: [Role.SALES],
[Role.ACCOUNTANT]: [Role.ACCOUNTANT],
[Role.PURCHASER]: [Role.PURCHASER],
[Role.FLORIST]: [Role.FLORIST],
[Role.DELIVERY]: [Role.DELIVERY],
};
2. 路由守衛(檢查 roles 陣列交集)
// src/routes/role-guard.tsx
// 檢查用戶的 roles 陣列是否與 allowedRoles 有交集
const hasPermission =
allowedRoles.length === 0 ||
currentUser.roles.some((role) => allowedRoles.includes(role));
// 使用範例:只需指定最基本的角色
<RoleGuard allowedRoles={[Role.SALES]}>
<SalesApplet /> {/* OWNER 自動有權限 */}
</RoleGuard>
3. Applet 權限配置(簡化版)
// src/config/applet-registry.ts
import { Role } from '@/types/auth';
{
id: 'orders',
name: 'Order Management',
allowedRoles: [Role.SALES], // 簡化:OWNER/MANAGER 自動有權限
// ...
}
{
id: 'design',
name: 'Design Workbench',
allowedRoles: [Role.FLORIST], // 簡化:OWNER 自動有權限
// ...
}
4. Mock API 回傳扁平化 roles
// src/mocks/handlers/auth.ts
// 登入回應使用 ROLE_HIERARCHY 取得扁平化 roles
const roles = ROLE_HIERARCHY[user.role as RoleName] ?? [user.role];
return HttpResponse.json({
accessToken,
refreshToken,
user: {
...userWithoutPassword,
roles: [...roles], // 扁平化陣列
authorities: [...authorities],
},
});
5. usePermission Hook(僅提供用戶資訊)
// src/hooks/use-permission.ts
const { currentUser } = usePermission();
// 用於 UI 顯示用戶資訊
<span>Role: {currentUser?.role}</span>
6. Mock 認證(僅驗證 token)
// src/mocks/middleware/auth-middleware.ts
export function checkAuth(request: Request): AuthCheckResult {
// 只驗證 token 有效性
// 不做任何權限檢查
}
已移除的內容
| 項目 | 原因 |
|---|---|
Authority 常數定義 | 前端不需要 |
ROLE_AUTHORITIES 映射表 | 前端不需要 |
hasRole() hook | 不需要在組件層檢查 |
hasAnyRole() hook | 不需要在組件層檢查 |
canAccess() hook | 不需要在組件層檢查 |
AuthorityGuard 組件 | 不需要細粒度權限控制 |
hasAuthority() hook | 前端不需要 |
src/mocks/utils/permissions.ts | Mock 不做權限檢查 |
src/utils/role-utils.ts | 統一格式後不需要轉換 |
組件層 allowedRoles 配置 | 進入 Applet 即可使用所有功能 |
決策後果 (Consequences)
正面影響 (Positive)
- ✅ 大幅簡化 - 移除所有 Authority 相關邏輯和格式轉換
- ✅ 職責分離 - 前端只管 UI 入口,權限控制交給後端
- ✅ 維護成本降低 - 不需要維護 Role-to-Authority 映射和格式轉換
- ✅ Mock 更簡潔 - 只做認證,不模擬權限拒絕
- ✅ 概念清晰 - 進入 Applet 即可使用所有功能,無需判斷按鈕是否可用
- ✅ 格式統一 - 前端、Mock、後端使用同一格式(ROLE_XXX)
- ✅ 角色繼承 - 模擬 Spring Security Role Hierarchy,allowedRoles 只需指定最基本角色
- ✅ 配置簡化 - 例如訂單管理只需
[SALES],店主自動有權限
設計原則
-
⚠️ 如果某角色只需要部分功能
- 解法:為該角色設計專屬的 Applet(如 Sales Applet、Design Applet、Delivery Applet)
- 不要在組件層做權限判斷
-
⚠️ 前端無法阻止無權限的 API 呼叫
- 解法:這是正確的,權限控制由後端負責
- 前端收到 403 時顯示錯誤訊息即可
保留的內容
| 項目 | 原因 |
|---|---|
RoleName 類型 | RoleGuard 和菜單過濾需要 |
Role 常數 | 便於使用,避免字串硬編碼 |
VALID_ROLES 常數 | 角色驗證需要 |
ROLE_HIERARCHY 常數 | 角色繼承映射,Mock API 使用 |
RoleGuard 組件 | Applet 層級存取控制(使用 roles 陣列交集檢查) |
ProtectedRoute 組件 | 認證檢查 |
menu-config.ts 的 allowedRoles | Applet 層級菜單過濾 |
applet-registry.ts 的 allowedRoles | Applet 層級應用啟動器過濾(簡化版) |
checkAuth 函數 | Mock 認證(token 有效性) |
影響範圍 (Impact)
修改的檔案
| 檔案 | 變更 |
|---|---|
src/types/auth.ts | 新增 ROLE_HIERARCHY 角色繼承映射 |
src/routes/role-guard.tsx | 使用 roles 陣列交集檢查 |
src/config/applet-registry.ts | 簡化 allowedRoles(只需最基本角色) |
src/components/application-launcher/ | 使用 roles 陣列過濾 |
src/mocks/utils/jwt.ts | 使用 ROLE_HIERARCHY 生成扁平化 roles |
src/mocks/handlers/auth.ts | 回傳扁平化 roles 陣列 |
src/features/iam/me-slice.ts | 移除格式轉換邏輯 |
src/hooks/use-permission.ts | 只保留 currentUser |
已刪除的檔案
src/mocks/utils/permissions.ts- Mock 不做權限檢查src/utils/role-utils.ts- 統一格式後不需要轉換src/routes/authority-guard.tsx- 不需要細粒度權限控制
參考資料 (References)
- Spring Security - Authorities
- US-002:角色權限控制(對應的 User Story,文檔路徑待建立)
最後更新: 2025-12-20 決策者: 開發團隊