security(openspec): 新增 add-role-middleware change 規劃
Run Tests / test (pull_request) Successful in 36s

待辦 H1:member/provider 路由群組補上角色 middleware。
新增 role-authorization 規格,並修改 feature-test-coverage 的
「預約列表端點測試覆蓋」。spec-reviewer 與 design-hardener 均 PASS。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-17 23:28:06 +08:00
parent 1839e9008f
commit 6feb79bc1a
6 changed files with 244 additions and 0 deletions
@@ -0,0 +1,25 @@
# Spec Delta
## MODIFIED Requirements
### Requirement: 預約列表端點測試覆蓋
測試套件 SHALL 驗證下列三個路由的資料隔離行為:
- `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 只能看到自己的預約
- **WHEN** Member A 呼叫 `GET /api/member/bookings`,系統中同時存在 Member A 與 Member B 各一筆預約
- **THEN** 回傳 data 只包含 Member A 的 booking id;Member B 的 booking id 不在 data 中
#### Scenario: Provider 只能看到屬於自己課程的預約
- **WHEN** Provider A 呼叫 `GET /api/provider/bookings`,系統中有屬於 Provider A 與 Provider B 課程各一筆預約
- **THEN** 回傳 data 只包含 Provider A 課程的 booking id;Provider B 課程的 booking id 不在 data 中
#### Scenario: Admin 可查看所有預約
- **WHEN** Admin 呼叫 `GET /api/admin/bookings`,系統中有 2 筆不同 Member 的預約
- **THEN** 回傳 data 包含兩筆 booking id
#### Scenario: 未認證請求被拒絕
- **WHEN** 未帶 token 呼叫 `GET /api/member/bookings`
- **THEN** 回傳 HTTP 401
@@ -0,0 +1,68 @@
# Spec Delta
## Purpose
依使用者角色限制 member、provider 專屬 API 路由群組的存取,避免已登入的其他角色跨角色呼叫課程、時段、預約等業務端點。
## ADDED 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