Compare commits
2 Commits
0dffca31bb
...
520ec487ee
| Author | SHA1 | Date | |
|---|---|---|---|
| 520ec487ee | |||
| a97a7d096c |
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-06-11
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Design — 2026-06-11 稽核修補
|
||||||
|
|
||||||
|
## D1:admin register 封鎖方式——artisan command 而非 middleware 防護
|
||||||
|
|
||||||
|
候選方案:
|
||||||
|
- (a) 移除公開路由,改 `php artisan app:create-admin`(採用)
|
||||||
|
- (b) 端點移入 `auth:sanctum + admin` middleware group(僅既有 admin 可開新 admin)
|
||||||
|
|
||||||
|
選 (a) 的理由:(b) 有「第一位管理員」雞生蛋問題,且保留 HTTP 入口就保留攻擊面;管理員建立頻率極低,不值得為其維護一個網路端點。command 密碼門檻設 min:8(高於一般使用者 min:6),因管理權限影響全平台。Seeder 直接以 Model 建立 admin,不受影響。
|
||||||
|
|
||||||
|
## D2:rate-limit 漂移以實作(10/min)為基準
|
||||||
|
|
||||||
|
測試已在 commit 0dabc4e 有意配合實作改為 10 次,且 P2 帳號鎖定上線後已涵蓋暴力破解防護(per-email 計數),IP-based throttle 的角色退為防濫用,放寬至 10/min 可容納共享 IP(公司/學校 NAT)場景。故更新規格而非改回實作。Admin 維持 3/min 不變。
|
||||||
|
|
||||||
|
## D3:is_verified 最小語意——列表過濾而非禁止上架
|
||||||
|
|
||||||
|
候選方案:
|
||||||
|
- (a) 未驗證教練課程從公開 index/show 排除(採用)
|
||||||
|
- (b) 未驗證教練 `store()` 回 403(禁止建立課程)
|
||||||
|
|
||||||
|
選 (a) 的理由:對教練的破壞較小(仍可登入、預先準備課程內容,通過驗證即曝光),可逆性高;(b) 會讓新教練在審核期間完全無事可做。實作為 `DivingOffer::scopeVisibleToPublic`(單一來源,index/show 共用):`provider_id` 為 null(平台自有資料)不受限,否則要求 `provider.providerProfile.is_verified = true`(無 profile 視同未驗證)。
|
||||||
|
|
||||||
|
快取一致性:公開列表有 180 秒 Redis tag 快取,`toggleProviderVerified` 後必須 `Cache::tags(['diving_offers'])->flush()`,否則切換最長延遲 3 分鐘生效。測試以「先打列表進快取 → toggle → 再打列表」驗證立即生效。
|
||||||
|
|
||||||
|
## D4:預約測試的保護目標(Rule 9——測試編碼 WHY)
|
||||||
|
|
||||||
|
- **狀態機**:終態(completed/rejected/expired/cancelled)不可再轉移——保護名額帳務與評價資格(只有 completed 能評價)
|
||||||
|
- **防超賣**:pending 不佔名額是刻意設計(教練未承諾前不鎖位),防線在 confirm 時 `lockForUpdate` 二次驗證;不變式為「confirmed 總人數 ≤ 容量」
|
||||||
|
- **Scheduler**:48h 過期與日期完成的邊界——過早 expire 砍掉教練來得及確認的單、過早 complete 讓未上課預約取得評價資格
|
||||||
|
- **聊天授權**:HTTP 端點與 presence channel 雙防線都必須擋非參與方與非 confirmed 狀態
|
||||||
|
- **Admin 邊界**:非 admin 一律 403;Admin 也不可繞過狀態機(rejected 不可 complete)
|
||||||
|
|
||||||
|
## D5:分支基底
|
||||||
|
|
||||||
|
`fix/audit-remediation` 基底為 `test/auth-tests-coverage`(而非 master),因 PR #27 的 auth 測試實際尚未併入 master,而本次修補需在其測試之上修改 `AuthLoginTest`。merge 本 change 的 PR #28 會一併帶入 PR #27 內容。
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
2026-06-11 規格與實作稽核(`docs/analysis/2026-06-11-spec-implementation-audit.md`)發現四項問題:
|
||||||
|
|
||||||
|
1. **P0**:`POST /api/admin/register` 完全公開且無任何防護,任何人一個 HTTP 請求即可註冊管理員帳號,使 P0~P2 認證安全強化形同虛設。該端點不在任何規格內(規格外實作)。
|
||||||
|
2. **P1**:`provider_profiles.is_verified` 從未被業務邏輯強制執行——教練免審核即可上架課程、被公開曝光,Admin 的驗證開關是純展示功能,且無任何規格定義其語意。
|
||||||
|
3. **P1**:`login-rate-limiting` 規格寫 5/min,實作與測試為 `throttle:10,1`(規格漂移,測試已在 commit 0dabc4e 改為配合實作但規格未同步)。
|
||||||
|
4. **P2**:預約狀態機、防超賣、Scheduler 自動轉移——平台核心業務——零測試覆蓋。
|
||||||
|
|
||||||
|
> 註:本 change 為事後補歸檔。稽核報告實質扮演 proposal 角色,實作先於本文件完成(branch `fix/audit-remediation`,PR #28)。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- **移除** `POST /api/admin/register` 路由、`AuthController::registerAdmin`、對應 Swagger 文件
|
||||||
|
- **新增** `php artisan app:create-admin` command(密碼門檻 min:8),管理員帳號僅限主機端建立
|
||||||
|
- **新增** `DivingOffer::visibleToPublic` scope:公開 index/show 排除未驗證教練的課程(`provider_id` null 不受限);`toggle-verified` 後 flush `diving_offers` 快取
|
||||||
|
- **同步** `login-rate-limiting` 規格門檻至實作值 10/min
|
||||||
|
- **新增測試**:`AdminAccountCreationTest`(4)、`DivingOfferVisibilityTest`(7)、`BookingLifecycleTest`(17)、`BookingOversellTest`(3)、`BookingSchedulerTest`(6)、`BookingChatAuthTest`(8)、`AdminEndpointAuthTest`(5)
|
||||||
|
- **移除** `AuthLoginTest` 中測已刪端點的 2 個 admin register 測試
|
||||||
|
- **規格清理**:補 `auth-test-coverage` Purpose、修正 `member-portal-ui` repo 描述
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- `provider-verification`:定義 `is_verified` 的最小業務語意——未驗證教練的課程不對公開端點曝光、切換立即生效、教練自有管理端點不受限
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `admin-auth`:新增「管理員帳號建立途徑」requirement(公開註冊端點關閉、僅限 `app:create-admin`);補「管理員查詢指定用戶資料」端點規格
|
||||||
|
- `login-rate-limiting`:Member/Provider 門檻 5/min → 10/min(與實作一致,帳號鎖定已涵蓋暴力破解防護)
|
||||||
|
- `auth-test-coverage`:admin register 場景改寫為「公開註冊端點保持關閉」;補歸檔遺留的 TBD Purpose
|
||||||
|
- `member-portal-ui`:前端位置由「獨立 repo」修正為本 repo `frontend/` 目錄
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- **行為變更**:未驗證教練(含 DemoSeeder 中 1 位 `is_verified=false` 的展示教練)課程從公開列表消失、詳情回 404;依賴 `POST /api/admin/register` 的外部工具需改用 `app:create-admin`
|
||||||
|
- **影響檔案**:`routes/api.php`、`AuthController`、`AuthApiDoc`、`DivingOffer`、`DivingOfferController`、`AdminUserController`、新增 `app/Console/Commands/CreateAdminUser.php`、`tests/Feature/`(新增 7 檔)
|
||||||
|
- **已知限制**(留待完整教練審核流程):`/diving-offers/{id}/schedules`、`/diving-offers/{id}/reviews` 與預約建立流程未套用相同過濾
|
||||||
|
- 驗證:容器內 `php artisan test` 146 passed / 378 assertions
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 管理員帳號建立途徑
|
||||||
|
管理員帳號 SHALL 僅能透過主機端 `php artisan app:create-admin` command 或資料庫 seeder 建立。系統 MUST NOT 提供任何公開的管理員註冊 HTTP 端點(原 `POST /api/admin/register` 已於 2026-06-11 因 P0 安全漏洞移除)。command 建立的密碼門檻為至少 8 碼,高於一般使用者。
|
||||||
|
|
||||||
|
#### Scenario: 公開註冊端點保持關閉
|
||||||
|
- **WHEN** 任何人(含未認證請求)送出 `POST /api/admin/register`
|
||||||
|
- **THEN** 回傳 HTTP 404,且不建立任何帳號
|
||||||
|
|
||||||
|
#### Scenario: 主機端建立管理員成功
|
||||||
|
- **WHEN** 操作者於主機執行 `php artisan app:create-admin {name} {email} --password={password}` 且資料合法
|
||||||
|
- **THEN** 建立 role=admin 的 User 與對應 AdminProfile
|
||||||
|
|
||||||
|
#### Scenario: 密碼過弱或 email 重複
|
||||||
|
- **WHEN** command 收到少於 8 碼的密碼或已存在的 email
|
||||||
|
- **THEN** command 以失敗結束,不建立任何帳號
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 管理員查詢指定用戶資料
|
||||||
|
後端 SHALL 提供 `GET /api/admin/check-member/{id}` 與 `GET /api/admin/check-provider/{id}`(需 Bearer token,role=admin),依角色查詢指定用戶的基本資料與對應 profile。
|
||||||
|
|
||||||
|
#### Scenario: 查詢存在的用戶
|
||||||
|
- **WHEN** 管理員以有效 id 查詢對應角色的用戶
|
||||||
|
- **THEN** 回傳 HTTP 200 與該用戶資料
|
||||||
|
|
||||||
|
#### Scenario: id 不存在或角色不符
|
||||||
|
- **WHEN** 查詢的 id 不存在,或該用戶角色與端點不符(如以 check-member 查 provider)
|
||||||
|
- **THEN** 回傳 HTTP 404
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Auth 登入/註冊/登出測試覆蓋(三角色)
|
||||||
|
(僅列出本次變更的 scenario;其餘 scenario 不變)
|
||||||
|
|
||||||
|
原「Admin 註冊成功」與「Admin 重複 Email 註冊失敗」兩個 scenario 移除(端點已刪),改為:
|
||||||
|
|
||||||
|
#### Scenario: Admin 公開註冊端點保持關閉
|
||||||
|
- **WHEN** 任何人送出 `POST /api/admin/register`(該端點已於 2026-06-11 因 P0 漏洞移除,見 admin-auth 規格「管理員帳號建立途徑」)
|
||||||
|
- **THEN** 回傳 HTTP 404,且不建立任何帳號;帳號建立改由 `app:create-admin` command 覆蓋測試
|
||||||
|
|
||||||
|
另補主規格歸檔時遺留的 TBD Purpose:
|
||||||
|
|
||||||
|
> 驗證三角色(member / provider / admin)認證流程的測試覆蓋契約:登入/註冊/登出、帳號鎖定(P2)、OAuth state 驗證、token refresh 與登入頻率限制。此規格定義測試套件必須覆蓋的場景,任何認證行為變更時,對應測試必須同步失敗以攔截回歸。
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: 登入頻率限制
|
||||||
|
後端 SHALL 對所有登入端點套用 IP-based 頻率限制,超過限制時回傳 HTTP 429。Member 與 Provider 每 IP 每分鐘最多 10 次(原定 5 次;帳號鎖定機制上線後已涵蓋暴力破解防護,放寬以容納共享 IP 場景,與實作 `throttle:10,1` 一致);Admin 因影響範圍更廣,限制為每 IP 每分鐘最多 3 次。
|
||||||
|
|
||||||
|
#### Scenario: Member / Provider 正常登入不受影響
|
||||||
|
- **WHEN** 同一 IP 在 1 分鐘內對 `/api/member/login` 或 `/api/provider/login` 送出 10 次以內的請求
|
||||||
|
- **THEN** 請求正常處理,回傳對應的登入結果(200 成功或 401 失敗)
|
||||||
|
|
||||||
|
#### Scenario: Member / Provider 超過頻率限制
|
||||||
|
- **WHEN** 同一 IP 在 1 分鐘內送出第 11 次 member 或 provider 登入請求
|
||||||
|
- **THEN** 回傳 HTTP 429,並帶有 `Retry-After` header 指示等待時間
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: 專案基礎建設
|
||||||
|
前端 SHALL 建立於本 repo 的 `frontend/` 目錄(原規劃獨立 repo,後併入主 repo 以簡化版控與部署),使用 Vue 3 + Vite + Tailwind CSS + Vue Router 4 + Pinia + Axios,並設定 `.env` 指定後端 API base URL。
|
||||||
|
|
||||||
|
#### Scenario: 開發環境啟動
|
||||||
|
- **WHEN** 開發者執行 `npm run dev`
|
||||||
|
- **THEN** 應用在 `http://localhost:5173` 啟動,無編譯錯誤
|
||||||
|
|
||||||
|
#### Scenario: API base URL 設定
|
||||||
|
- **WHEN** `.env` 中設定 `VITE_API_URL=http://localhost:80`
|
||||||
|
- **THEN** 所有 Axios 請求以此為 base URL
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
# provider-verification Specification
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
定義 `provider_profiles.is_verified` 的最小業務語意:未通過平台驗證的教練,其課程不對公開端點曝光。在完整教練審核流程(證照上傳、審核佇列、駁回原因)實作前,先以此約束堵住「未審核教練可公開曝光」的風險。
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 未驗證教練的課程不對公開端點曝光
|
||||||
|
公開課程端點(`GET /api/diving-offers`、`GET /api/diving-offers/{id}`)SHALL 僅回傳符合以下任一條件的課程:(a) `provider_id` 為 null(平台自有資料);(b) 課程所屬 Provider 的 `provider_profiles.is_verified = true`。未驗證教練的課程在列表中 SHALL 被排除,在詳情端點 SHALL 回傳 404。
|
||||||
|
|
||||||
|
#### Scenario: 已驗證教練的課程正常曝光
|
||||||
|
- **WHEN** 匿名使用者請求 `GET /api/diving-offers`
|
||||||
|
- **THEN** 已驗證教練(is_verified=true)的課程出現在結果中
|
||||||
|
|
||||||
|
#### Scenario: 未驗證教練的課程從列表排除
|
||||||
|
- **WHEN** 匿名使用者請求 `GET /api/diving-offers`
|
||||||
|
- **THEN** 未驗證教練(is_verified=false 或無 ProviderProfile)的課程不出現在結果中
|
||||||
|
|
||||||
|
#### Scenario: 未驗證教練的課程詳情回 404
|
||||||
|
- **WHEN** 匿名使用者請求 `GET /api/diving-offers/{id}`,該課程屬於未驗證教練
|
||||||
|
- **THEN** 回傳 HTTP 404
|
||||||
|
|
||||||
|
#### Scenario: provider_id 為 null 的課程不受限
|
||||||
|
- **WHEN** 匿名使用者請求公開課程端點,課程的 `provider_id` 為 null
|
||||||
|
- **THEN** 課程正常曝光
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 驗證狀態切換立即生效
|
||||||
|
管理員透過 `PUT /api/admin/providers/{id}/toggle-verified` 切換驗證狀態後,公開課程列表的快取(`diving_offers` cache tag)SHALL 立即失效,下次請求反映最新可見性。
|
||||||
|
|
||||||
|
#### Scenario: 取消驗證後課程立即從公開列表消失
|
||||||
|
- **WHEN** 管理員將教練 is_verified 由 true 切為 false
|
||||||
|
- **THEN** 下一次 `GET /api/diving-offers` 請求不包含該教練的課程(不受 180 秒快取影響)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 教練自有管理端點不受可見性限制
|
||||||
|
Provider 對自己課程的管理端點(`/api/provider/offers*`)與 Admin 管理端點(`/api/admin/offers*`)SHALL 不受公開可見性過濾影響,未驗證教練仍可登入、編輯與管理自己的課程。
|
||||||
|
|
||||||
|
#### Scenario: 未驗證教練仍可管理自己的課程
|
||||||
|
- **WHEN** 未驗證教練以有效 token 請求 `GET /api/provider/offers`
|
||||||
|
- **THEN** 回傳該教練的全部課程
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
已知限制(留待完整教練審核流程處理):`GET /api/diving-offers/{id}/schedules`、`GET /api/diving-offers/{id}/reviews` 與預約建立流程尚未套用相同過濾,知道課程 id 的使用者仍可間接互動。完整審核流程(verification_status enum、證照上傳、審核佇列)見 `docs/analysis/2026-06-11-future-roadmap-feasibility.md` §2.1。
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
## 1. T1.1 封鎖 admin/register(P0)
|
||||||
|
|
||||||
|
- [x] 1.1 移除 `routes/api.php` 的公開 `POST /api/admin/register` 路由
|
||||||
|
- [x] 1.2 移除 `AuthController::registerAdmin()` 方法
|
||||||
|
- [x] 1.3 移除 `app/Docs/AuthApiDoc.php` 對應 Swagger 區塊
|
||||||
|
- [x] 1.4 新增 `app/Console/Commands/CreateAdminUser.php`(`app:create-admin`,密碼 min:8,支援 --position / --department,未帶 --password 時互動式輸入)
|
||||||
|
- [x] 1.5 [整合測試] `AdminAccountCreationTest`:端點回 404 不建帳號、command 建立 admin+profile、重複 email 失敗、弱密碼失敗(4 案例)
|
||||||
|
- [x] 1.6 更新 `admin-auth` 規格:新增「管理員帳號建立途徑」requirement
|
||||||
|
- [x] 1.7 確認前端無 admin 註冊入口(無引用)、Seeder 不受影響
|
||||||
|
|
||||||
|
## 2. T1.2 同步 login-rate-limiting 規格
|
||||||
|
|
||||||
|
- [x] 2.1 規格門檻 5/min → 10/min(與 `throttle:10,1` 實作及 AuthRateLimitTest 一致),註記放寬理由
|
||||||
|
|
||||||
|
## 3. T2.1 is_verified 最小業務語意(P1)
|
||||||
|
|
||||||
|
- [x] 3.1 `DivingOffer` 新增 `scopeVisibleToPublic`(provider_id null 或教練已驗證)
|
||||||
|
- [x] 3.2 `DivingOfferController` 公開 index / show 套用 scope
|
||||||
|
- [x] 3.3 `AdminUserController::toggleProviderVerified` 後 flush `diving_offers` 快取 tag
|
||||||
|
- [x] 3.4 新增 `provider-verification` 規格(含已知限制 Notes)
|
||||||
|
- [x] 3.5 [整合測試] `DivingOfferVisibilityTest`:列表含已驗證/排除未驗證/null 不受限、詳情 404、toggle 立即生效(快取失效)、教練自有端點不受限(7 案例)
|
||||||
|
|
||||||
|
## 4. T3.1 預約核心測試(P2)
|
||||||
|
|
||||||
|
- [x] 4.1 [整合測試] `BookingLifecycleTest`:狀態機合法/非法轉移、pending 不佔名額、價格快照、24h 取消截止、名額釋放、跨用戶授權邊界(17 案例)
|
||||||
|
- [x] 4.2 [整合測試] `BookingOversellTest`:confirm 時防超賣不變式、full 擋新預約、釋放後可再確認(3 案例)
|
||||||
|
- [x] 4.3 [整合測試] `BookingSchedulerTest`:`app:expire-pending-bookings` 48h 邊界、`app:complete-finished-bookings` 日期邊界與通知(6 案例)
|
||||||
|
- [x] 4.4 [整合測試] `BookingChatAuthTest`:HTTP 端點與 presence channel 雙防線(8 案例)
|
||||||
|
- [x] 4.5 [整合測試] `AdminEndpointAuthTest`:六個 admin GET 端點 401/403/200 矩陣、Admin 不可繞過狀態機(5 案例)
|
||||||
|
|
||||||
|
## 5. T4 規格清理
|
||||||
|
|
||||||
|
- [x] 5.1 補 `auth-test-coverage` 歸檔遺留的 TBD Purpose
|
||||||
|
- [x] 5.2 `auth-test-coverage` admin register 場景改寫為「公開註冊端點保持關閉」
|
||||||
|
- [x] 5.3 同步移除 `AuthLoginTest` 中測已刪端點的 2 個測試
|
||||||
|
- [x] 5.4 修正 `member-portal-ui` 前端 repo 描述(實際位於本 repo `frontend/`)
|
||||||
|
- [x] 5.5 `admin-auth` 補 check-member / check-provider 查詢端點規格
|
||||||
|
|
||||||
|
## 6. 驗證
|
||||||
|
|
||||||
|
- [x] 6.1 容器內 `php artisan test` 全綠:146 passed / 378 assertions
|
||||||
|
- [x] 6.2 確認本機 CourseImageTest 失敗為環境因素(laragon PHP 缺 GD),非回歸
|
||||||
Reference in New Issue
Block a user