Merge pull request 'chore(openspec): 歸檔 add-role-middleware 並同步主規格' (#72) from chore/archive-add-role-middleware into master
Deploy Production / deploy (push) Successful in 1m40s
Run Tests / test (push) Successful in 37s

Reviewed-on: #72
This commit was merged in pull request #72.
This commit is contained in:
2026-10-17 16:40:54 +00:00
10 changed files with 77 additions and 9 deletions
+3 -3
View File
@@ -155,9 +155,9 @@
## 授權規範 ## 授權規範
- 已知缺口:`routes/api.php` 的 `member`、`provider` 路由群組只有 `auth:sanctum`,**沒有角色 middleware**;只有 `admin` 群組有 `EnsureAdmin`(待辦見 `docs/analysis/2026-10-14-project-review-todo.md` H1) - `routes/api.php` 的 `member`、`provider`、`admin` 路由群組分別掛 `auth:sanctum` + `member`/`provider`/`admin` 角色 middleware(`EnsureMember`/`EnsureProvider`/`EnsureAdmin`,規格見 `openspec/specs/role-authorization/`)
- 在 middleware 補上之前:新增或修改 member / provider 路由時,Controller 必須明確檢查呼叫者角色與資源所有權,不可假設路由群組已有保護 - 新路由必須掛對應角色 middleware,並保留 Controller 內的資源所有權檢查(middleware 只擋跨角色,不擋同角色存取他人資源)
- middleware 補上之後:新路由必須掛對應角色 middleware,並保留 Controller 內的資源所有權檢查 - member + provider 共用的路由(通知、即時訊息)沒有角色 middleware,Controller 必須自行檢查資源所有權;admin 能否存取這類路由尚未定案
- 每個涉及權限的路由都必須有跨角色測試(member / provider / admin / 未登入各自的預期結果,參考 `tests/Feature/AdminEndpointAuthTest.php`) - 每個涉及權限的路由都必須有跨角色測試(member / provider / admin / 未登入各自的預期結果,參考 `tests/Feature/AdminEndpointAuthTest.php`)
--- ---
@@ -37,8 +37,8 @@
## 5. 手動驗證 ## 5. 手動驗證
- [ ] 5.1 [整合測試](手動,非自動化測試)本機 Docker 環境以 member token 呼叫 `POST /api/provider/offers`,以 provider token 呼叫 `POST /api/member/bookings`,確認兩者皆回 403 - [x] 5.1 [整合測試](手動,非自動化測試)本機 Docker 環境以 member token 呼叫 `POST /api/provider/offers`,以 provider token 呼叫 `POST /api/member/bookings`,確認兩者皆回 403
- [ ] 5.2 [整合測試](手動,非自動化測試)本機前端:會員端登入後瀏覽「我的預約」、教練端登入後瀏覽課程/時段/預約/審核頁,確認功能正常、沒有出現 403 - [x] 5.2 [整合測試](手動,非自動化測試)本機前端:會員端登入後瀏覽「我的預約」、教練端登入後瀏覽課程/時段/預約/審核頁,確認功能正常、沒有出現 403
## Workflow follow-up ## Workflow follow-up
+2 -2
View File
@@ -61,8 +61,8 @@ context: |
## 後端慣例 ## 後端慣例
- Controller 位於 `app/Http/Controllers/API/`,Swagger 註解位於 `app/Docs/` - Controller 位於 `app/Http/Controllers/API/`,Swagger 註解位於 `app/Docs/`
- 路由集中於 `routes/api.php`;admin 群組以 `admin` middleware 保護 - 路由集中於 `routes/api.php`;admin 群組以 `admin` middleware 保護
- member / provider 路由群組目前僅有 `auth:sanctum`,角色檢查需在 Controller 內自行處理 - member / provider 路由群組以 `member`/`provider` middleware 保護(role-authorization spec);
(已知缺口,見 docs/analysis/2026-10-14-project-review-todo.md H1;新增路由時不可假設已有角色保護) middleware 只擋跨角色,資源所有權仍需在 Controller 內檢查
## API 回應格式 ## API 回應格式
成功:`{ status: true, message?: "...", data: {...} 或 [...] }` 成功:`{ status: true, message?: "...", data: {...} 或 [...] }`
+2 -2
View File
@@ -126,8 +126,8 @@
### Requirement: 預約列表端點測試覆蓋 ### Requirement: 預約列表端點測試覆蓋
測試套件 SHALL 驗證下列三個路由的資料隔離行為: 測試套件 SHALL 驗證下列三個路由的資料隔離行為:
- `GET /api/member/bookings`(middleware: `auth:sanctum`,隔離靠 `where member_id = auth()->id()`) - `GET /api/member/bookings`(middleware: `auth:sanctum` + `member`,非 member 角色於 middleware 層強制 403;隔離靠 `where member_id = auth()->id()`)
- `GET /api/provider/bookings`(middleware: `auth:sanctum`,隔離靠 `whereHas schedule.provider_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) - `GET /api/admin/bookings`(middleware: `auth:sanctum` + `admin`,EnsureAdmin 於 middleware 層強制 403)
#### Scenario: Member 只能看到自己的預約 #### Scenario: Member 只能看到自己的預約
+68
View File
@@ -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