Files
CFDivePlatform/openspec/changes/archive/2026-10-18-add-role-middleware/design.md
T
a620906209 d83d941993
Run Tests / test (pull_request) Successful in 34s
chore(openspec): 歸檔 add-role-middleware 並同步主規格
- 新增主規格 role-authorization,更新 feature-test-coverage 的 middleware 描述
- 勾選手動驗證任務 5.1、5.2(23/23)
- CLAUDE.md 授權規範與 openspec/config.yaml 移除「member/provider 只有 auth:sanctum」的過時說明

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 00:30:05 +08:00

6.0 KiB
Raw Blame History

Design

Context

動機見 proposal.md(Why)。目前現況:

  • bootstrap/app.php 只註冊一個角色 middleware alias:admin → App\Http\Middleware\EnsureAdmin。它在 !$request->user() || !$request->user()->isAdmin() 時回 403 { status: false, message: "無權限存取" }
  • App\Models\User 已有 isMember()、isProvider()、isAdmin(),都是直接比對 role 字串
  • /api/member/*、/api/provider/* 群組內,只有 AuthController 的 logout/refresh/profile/change-password 自己檢查角色,回應同樣是 403 { status: false, message: "無權限存取" };MemberBookingController、ReviewController(store/update/destroy)、ProviderOfferController、ScheduleController、ProviderBookingController、CourseImageController、ProviderVerificationController 只檢查資源所有權,不檢查角色
  • 前端 axios.js、coachAxios.js 的攔截器只對 401 做 refresh 重試,403 直接交給呼叫端;adminAxios.js 沒有呼叫 /member/*、/provider/*
  • 部署流程(deploy.yml)每次都會執行 route:cache,路由變更隨部署生效

Goals / Non-Goals

Goals:

  • 在路由層擋下所有跨角色呼叫 member/provider 專屬端點
  • 角色不符時的回應與現有 EnsureAdmin、AuthController 完全一致,前端不需要改

Non-Goals:

  • 不檢查 is_active(停用帳號沿用既有行為,與角色 middleware 無關)、教練審核狀態(審核狀態的限制維持在既有業務邏輯,例如 visibleToPublic scope)
  • 不移除 AuthController 內既有的角色檢查(重構屬待辦 M1)
  • 不調整跨角色共用路由(通知、即時訊息、/reviews/{id}/helpful、/logout、/user),包含「admin 能否存取這些路由」也不在此次決定
  • 不更動 EnsureAdmin

Decisions

D1:新增 EnsureMember、EnsureProvider 兩個 middleware,而非參數化的 EnsureRole

  • 做法:在 app/Http/Middleware/ 新增兩個 class,結構與 EnsureAdmin 相同,分別呼叫 isMember()、isProvider();在 bootstrap/app.php 註冊 alias member、provider
  • 理由:既有慣例是每個角色一個 middleware(EnsureAdmin + alias admin)。若新增參數化的 role:member,專案內會同時存在兩種寫法;要統一就得改 EnsureAdmin 的使用處,超出此次範圍
  • 替代方案:EnsureRole 搭配 role:member,provider。程式碼較少,但會跟既有的 admin alias 形成兩種寫法並存,因此不採用

D2:403 回應沿用 { status: false, message: "無權限存取" }

  • 理由:與 EnsureAdmin、AuthController::getUnauthorizedResponse() 一致,前端與 Swagger 已記載的 403 不需變動
  • middleware 內同時處理 $request->user() 為 null 的情況(與 EnsureAdmin 相同的防禦寫法);實際上 auth:sanctum 排在前面,未登入會先回 401

D3:middleware 只比對角色,不檢查審核狀態

  • 理由:未審核(unsubmitted)、審核中(pending)、被駁回(rejected)的教練都需要進入 /api/provider/verification* 送審或補件,也需要在審核通過前先建立課程。若在 middleware 檢查審核狀態,這些流程會整個斷掉

D4:掛在既有群組的 middleware 陣列,順序為 ['auth:sanctum', 'member']/['auth:sanctum', 'provider']

  • 理由:與 admin 群組 ['auth:sanctum', 'admin'] 寫法一致;auth:sanctum 先執行,保證未登入回 401、角色不符回 403
  • 公開的 register/login 路由定義在群組外,不受影響(已核對 routes/api.php)

D5:Controller 內既有的資源所有權檢查全部保留

  • 理由:CLAUDE.md 授權規範要求 middleware 補上後仍保留 Controller 內的所有權檢查;角色 middleware 只解決「角色錯」,不解決「同角色存取別人的資源」

安全確認

  • 輸入驗證:此次不新增外部輸入;既有端點的 $request->validate() 不變。middleware 只讀取 Sanctum 認證後的 $request->user()->role,沒有讀取任何請求參數
  • SQL/shell 拼接:無
  • 資料庫存取:middleware 不額外查詢資料庫($request->user() 由 Sanctum 認證時已載入);無 raw query,無 N+1
  • 權限檢查層級:路由層(member、provider middleware),加上 Controller 內既有的資源所有權檢查(例如 MemberBookingController::show 比對 member_id、ProviderOfferController 比對 provider_id)
  • 未授權觸發可能性:修正後,非 member 角色無法存取 /api/member/*,非 provider 角色無法存取 /api/provider/*;未登入一律 401
  • 各路由所掛 middleware:
    • /api/member/*(register、login 除外):auth:sanctum + member
    • /api/provider/*(register、login 除外):auth:sanctum + provider
    • 其他路由不變
  • 前端:不涉及 DOM 渲染;前端呼叫已帶各自的 Bearer token,不需改

Risks / Trade-offs

  • [既有測試可能以錯誤角色呼叫 member/provider 路由,加上 middleware 後改回 403] → 實作時先跑完整 php artisan test,逐一判斷失敗的測試是「測試本身用錯角色」(修正測試)還是「真的依賴跨角色行為」(停下來回報)
  • [AuthController 內的角色檢查與 middleware 重複] → 行為一致、無害;移除留給待辦 M1,避免此次範圍擴大
  • [admin 被擋在 member/provider 路由之外,若未來後台想借用這些端點會失敗] → 已核對前端 admin 端目前沒有呼叫;未來需要時應在 /api/admin/* 新增端點
  • [跨角色共用路由(通知、即時訊息)對 admin 的行為未定義] → 不在此次範圍;即時訊息的 authorizeParticipant 對 admin 已回 false,通知只會讀到 admin 自己的資料

Migration Plan

  • 無 migration、無新套件、無環境變數
  • 部署:merge 後由 deploy.yml 重建 image,並執行 route:cache,新的路由 middleware 即生效
  • 回滾:revert 此次 commit 再部署即可,沒有資料需要還原