From d83d941993b8f06381512345a8eb8fb171661599 Mon Sep 17 00:00:00 2001 From: Hank Date: Sun, 18 Oct 2026 00:30:05 +0800 Subject: [PATCH] =?UTF-8?q?chore(openspec):=20=E6=AD=B8=E6=AA=94=20add-rol?= =?UTF-8?q?e-middleware=20=E4=B8=A6=E5=90=8C=E6=AD=A5=E4=B8=BB=E8=A6=8F?= =?UTF-8?q?=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增主規格 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 --- CLAUDE.md | 6 +- .../.openspec.yaml | 0 .../2026-10-18-add-role-middleware}/design.md | 0 .../proposal.md | 0 .../specs/feature-test-coverage/spec.md | 0 .../specs/role-authorization/spec.md | 0 .../2026-10-18-add-role-middleware}/tasks.md | 4 +- openspec/config.yaml | 4 +- openspec/specs/feature-test-coverage/spec.md | 4 +- openspec/specs/role-authorization/spec.md | 68 +++++++++++++++++++ 10 files changed, 77 insertions(+), 9 deletions(-) rename openspec/changes/{add-role-middleware => archive/2026-10-18-add-role-middleware}/.openspec.yaml (100%) rename openspec/changes/{add-role-middleware => archive/2026-10-18-add-role-middleware}/design.md (100%) rename openspec/changes/{add-role-middleware => archive/2026-10-18-add-role-middleware}/proposal.md (100%) rename openspec/changes/{add-role-middleware => archive/2026-10-18-add-role-middleware}/specs/feature-test-coverage/spec.md (100%) rename openspec/changes/{add-role-middleware => archive/2026-10-18-add-role-middleware}/specs/role-authorization/spec.md (100%) rename openspec/changes/{add-role-middleware => archive/2026-10-18-add-role-middleware}/tasks.md (98%) create mode 100644 openspec/specs/role-authorization/spec.md diff --git a/CLAUDE.md b/CLAUDE.md index 60cdca7..3b465f8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -155,9 +155,9 @@ ## 授權規範 -- 已知缺口:`routes/api.php` 的 `member`、`provider` 路由群組只有 `auth:sanctum`,**沒有角色 middleware**;只有 `admin` 群組有 `EnsureAdmin`(待辦見 `docs/analysis/2026-10-14-project-review-todo.md` H1) -- 在 middleware 補上之前:新增或修改 member / provider 路由時,Controller 必須明確檢查呼叫者角色與資源所有權,不可假設路由群組已有保護 -- middleware 補上之後:新路由必須掛對應角色 middleware,並保留 Controller 內的資源所有權檢查 +- `routes/api.php` 的 `member`、`provider`、`admin` 路由群組分別掛 `auth:sanctum` + `member`/`provider`/`admin` 角色 middleware(`EnsureMember`/`EnsureProvider`/`EnsureAdmin`,規格見 `openspec/specs/role-authorization/`) +- 新路由必須掛對應角色 middleware,並保留 Controller 內的資源所有權檢查(middleware 只擋跨角色,不擋同角色存取他人資源) +- member + provider 共用的路由(通知、即時訊息)沒有角色 middleware,Controller 必須自行檢查資源所有權;admin 能否存取這類路由尚未定案 - 每個涉及權限的路由都必須有跨角色測試(member / provider / admin / 未登入各自的預期結果,參考 `tests/Feature/AdminEndpointAuthTest.php`) --- diff --git a/openspec/changes/add-role-middleware/.openspec.yaml b/openspec/changes/archive/2026-10-18-add-role-middleware/.openspec.yaml similarity index 100% rename from openspec/changes/add-role-middleware/.openspec.yaml rename to openspec/changes/archive/2026-10-18-add-role-middleware/.openspec.yaml diff --git a/openspec/changes/add-role-middleware/design.md b/openspec/changes/archive/2026-10-18-add-role-middleware/design.md similarity index 100% rename from openspec/changes/add-role-middleware/design.md rename to openspec/changes/archive/2026-10-18-add-role-middleware/design.md diff --git a/openspec/changes/add-role-middleware/proposal.md b/openspec/changes/archive/2026-10-18-add-role-middleware/proposal.md similarity index 100% rename from openspec/changes/add-role-middleware/proposal.md rename to openspec/changes/archive/2026-10-18-add-role-middleware/proposal.md diff --git a/openspec/changes/add-role-middleware/specs/feature-test-coverage/spec.md b/openspec/changes/archive/2026-10-18-add-role-middleware/specs/feature-test-coverage/spec.md similarity index 100% rename from openspec/changes/add-role-middleware/specs/feature-test-coverage/spec.md rename to openspec/changes/archive/2026-10-18-add-role-middleware/specs/feature-test-coverage/spec.md diff --git a/openspec/changes/add-role-middleware/specs/role-authorization/spec.md b/openspec/changes/archive/2026-10-18-add-role-middleware/specs/role-authorization/spec.md similarity index 100% rename from openspec/changes/add-role-middleware/specs/role-authorization/spec.md rename to openspec/changes/archive/2026-10-18-add-role-middleware/specs/role-authorization/spec.md diff --git a/openspec/changes/add-role-middleware/tasks.md b/openspec/changes/archive/2026-10-18-add-role-middleware/tasks.md similarity index 98% rename from openspec/changes/add-role-middleware/tasks.md rename to openspec/changes/archive/2026-10-18-add-role-middleware/tasks.md index c437d97..7de46ef 100644 --- a/openspec/changes/add-role-middleware/tasks.md +++ b/openspec/changes/archive/2026-10-18-add-role-middleware/tasks.md @@ -37,8 +37,8 @@ ## 5. 手動驗證 -- [ ] 5.1 [整合測試](手動,非自動化測試)本機 Docker 環境以 member token 呼叫 `POST /api/provider/offers`,以 provider token 呼叫 `POST /api/member/bookings`,確認兩者皆回 403 -- [ ] 5.2 [整合測試](手動,非自動化測試)本機前端:會員端登入後瀏覽「我的預約」、教練端登入後瀏覽課程/時段/預約/審核頁,確認功能正常、沒有出現 403 +- [x] 5.1 [整合測試](手動,非自動化測試)本機 Docker 環境以 member token 呼叫 `POST /api/provider/offers`,以 provider token 呼叫 `POST /api/member/bookings`,確認兩者皆回 403 +- [x] 5.2 [整合測試](手動,非自動化測試)本機前端:會員端登入後瀏覽「我的預約」、教練端登入後瀏覽課程/時段/預約/審核頁,確認功能正常、沒有出現 403 ## Workflow follow-up diff --git a/openspec/config.yaml b/openspec/config.yaml index 20beff6..16ba1ea 100644 --- a/openspec/config.yaml +++ b/openspec/config.yaml @@ -61,8 +61,8 @@ context: | ## 後端慣例 - Controller 位於 `app/Http/Controllers/API/`,Swagger 註解位於 `app/Docs/` - 路由集中於 `routes/api.php`;admin 群組以 `admin` middleware 保護 - - member / provider 路由群組目前僅有 `auth:sanctum`,角色檢查需在 Controller 內自行處理 - (已知缺口,見 docs/analysis/2026-10-14-project-review-todo.md H1;新增路由時不可假設已有角色保護) + - member / provider 路由群組以 `member`/`provider` middleware 保護(role-authorization spec); + middleware 只擋跨角色,資源所有權仍需在 Controller 內檢查 ## API 回應格式 成功:`{ status: true, message?: "...", data: {...} 或 [...] }` diff --git a/openspec/specs/feature-test-coverage/spec.md b/openspec/specs/feature-test-coverage/spec.md index 2104c45..d80bf0e 100644 --- a/openspec/specs/feature-test-coverage/spec.md +++ b/openspec/specs/feature-test-coverage/spec.md @@ -126,8 +126,8 @@ ### Requirement: 預約列表端點測試覆蓋 測試套件 SHALL 驗證下列三個路由的資料隔離行為: -- `GET /api/member/bookings`(middleware: `auth:sanctum`,隔離靠 `where member_id = auth()->id()`) -- `GET /api/provider/bookings`(middleware: `auth:sanctum`,隔離靠 `whereHas schedule.provider_id`) +- `GET /api/member/bookings`(middleware: `auth:sanctum` + `member`,非 member 角色於 middleware 層強制 403;隔離靠 `where member_id = auth()->id()`) +- `GET /api/provider/bookings`(middleware: `auth:sanctum` + `provider`,非 provider 角色於 middleware 層強制 403;隔離靠 `whereHas schedule.provider_id`) - `GET /api/admin/bookings`(middleware: `auth:sanctum` + `admin`,EnsureAdmin 於 middleware 層強制 403) #### Scenario: Member 只能看到自己的預約 diff --git a/openspec/specs/role-authorization/spec.md b/openspec/specs/role-authorization/spec.md new file mode 100644 index 0000000..5bb4d38 --- /dev/null +++ b/openspec/specs/role-authorization/spec.md @@ -0,0 +1,68 @@ +# role-authorization Specification + +## Purpose + +依使用者角色限制 member、provider 專屬 API 路由群組的存取,避免已登入的其他角色跨角色呼叫課程、時段、預約等業務端點。 + +## Requirements + +### Requirement: Member 專屬路由僅限 member 角色存取 +`/api/member/*` 路由群組(不含公開的 `POST /api/member/register`、`POST /api/member/login`)SHALL 只允許 `role = member` 的已登入使用者存取。其他角色 MUST 收到 HTTP 403,回應為 `{ "status": false, "message": "無權限存取" }`,且請求 MUST NOT 產生任何資料變更。未帶 token 的請求 MUST 收到 HTTP 401。 + +#### Scenario: member 可存取 member 路由 +- **WHEN** 一個 `role = member` 的使用者,以該使用者的 token 呼叫 `GET /api/member/profile` 與 `GET /api/member/bookings` +- **THEN** 回傳 HTTP 200,`status` 為 `true` + +#### Scenario: provider 呼叫 member 路由被拒絕 +- **WHEN** 一個 `role = provider` 的使用者,以該使用者的 token 呼叫 `GET /api/member/profile`、`GET /api/member/bookings`、`POST /api/member/bookings`、`POST /api/member/reviews` +- **THEN** 每個請求都回傳 HTTP 403,body 為 `{ "status": false, "message": "無權限存取" }` + +#### Scenario: admin 呼叫 member 路由被拒絕 +- **WHEN** 一個 `role = admin` 的使用者,以該使用者的 token 呼叫 `GET /api/member/profile`、`GET /api/member/bookings`、`POST /api/member/bookings` +- **THEN** 每個請求都回傳 HTTP 403,body 為 `{ "status": false, "message": "無權限存取" }` + +#### Scenario: 非 member 建立預約不會產生資料 +- **WHEN** 一個可預約的課程時段,以及一個 `role = provider` 的使用者,以該使用者的 token 呼叫 `POST /api/member/bookings` 並帶入合法的時段 id +- **THEN** 回傳 HTTP 403,`bookings` 資料表筆數不變,該時段的 `current_participants` 不變 + +#### Scenario: 未登入呼叫 member 路由 +- **WHEN** 不帶 token 呼叫 `GET /api/member/profile`、`GET /api/member/bookings` +- **THEN** 回傳 HTTP 401 + +### Requirement: Provider 專屬路由僅限 provider 角色存取 +`/api/provider/*` 路由群組(不含公開的 `POST /api/provider/register`、`POST /api/provider/login`)SHALL 只允許 `role = provider` 的已登入使用者存取,且 MUST NOT 以教練審核狀態作為進入條件。其他角色 MUST 收到 HTTP 403,回應為 `{ "status": false, "message": "無權限存取" }`,且請求 MUST NOT 產生任何資料變更。未帶 token 的請求 MUST 收到 HTTP 401。 + +#### Scenario: provider 可存取 provider 路由 +- **WHEN** 一個 `role = provider` 的使用者,以該使用者的 token 呼叫 `GET /api/provider/profile`、`GET /api/provider/offers`、`GET /api/provider/schedules`、`GET /api/provider/bookings` +- **THEN** 回傳 HTTP 200,`status` 為 `true` + +#### Scenario: 未通過審核的 provider 仍可進入 provider 路由 +- **WHEN** 一個 `role = provider`、`verification_status` 不是 approved(例如 unsubmitted 或 pending)的使用者,以該使用者的 token 呼叫 `GET /api/provider/verification` +- **THEN** 回傳 HTTP 200,不會因角色 middleware 回 403 + +#### Scenario: member 呼叫 provider 路由被拒絕 +- **WHEN** 一個 `role = member` 的使用者,以該使用者的 token 呼叫 `GET /api/provider/profile`、`GET /api/provider/offers`、`POST /api/provider/offers`、`POST /api/provider/schedules`、`GET /api/provider/bookings`、`GET /api/provider/verification` +- **THEN** 每個請求都回傳 HTTP 403,body 為 `{ "status": false, "message": "無權限存取" }` + +#### Scenario: admin 呼叫 provider 路由被拒絕 +- **WHEN** 一個 `role = admin` 的使用者,以該使用者的 token 呼叫 `GET /api/provider/profile`、`GET /api/provider/offers`、`POST /api/provider/offers`、`GET /api/provider/bookings` +- **THEN** 每個請求都回傳 HTTP 403,body 為 `{ "status": false, "message": "無權限存取" }` + +#### Scenario: 非 provider 建立課程不會產生資料 +- **WHEN** 一個 `role = member` 的使用者,以該使用者的 token 呼叫 `POST /api/provider/offers` 並帶入通過驗證的完整課程資料 +- **THEN** 回傳 HTTP 403,`diving_offers` 資料表筆數不變 + +#### Scenario: 未登入呼叫 provider 路由 +- **WHEN** 不帶 token 呼叫 `GET /api/provider/profile`、`GET /api/provider/offers` +- **THEN** 回傳 HTTP 401 + +### Requirement: 跨角色共用路由不受角色限制影響 +通知(`/api/notifications/*`)與預約即時訊息(`/api/bookings/{booking}/messages*`、`/api/bookings/messages/unread-counts`)路由 SHALL 維持既有行為:member 與 provider 皆可存取,存取權限由既有的資源歸屬檢查決定,不套用 member 或 provider 角色限制。 + +#### Scenario: member 與 provider 皆可讀取通知 +- **WHEN** 一個 `role = member` 與一個 `role = provider` 的使用者,分別以各自的 token 呼叫 `GET /api/notifications` +- **THEN** 兩者皆回傳 HTTP 200 + +#### Scenario: member 與 provider 皆可讀取未讀訊息數 +- **WHEN** 一個 `role = member` 與一個 `role = provider` 的使用者,分別以各自的 token 呼叫 `GET /api/bookings/messages/unread-counts` +- **THEN** 兩者皆回傳 HTTP 200