Merge pull request 'chore(openspec): 歸檔 add-role-middleware 並同步主規格' (#72) from chore/archive-add-role-middleware into master
Reviewed-on: #72
This commit was merged in pull request #72.
This commit is contained in:
@@ -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`)
|
||||
|
||||
---
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
@@ -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: {...} 或 [...] }`
|
||||
|
||||
@@ -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 只能看到自己的預約
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user