Merge pull request 'chore(openspec): 補歸檔 2026-06-11-audit-remediation change' (#29) from fix/audit-remediation into master
Deploy Production / deploy (push) Successful in 16s
Run Tests / test (push) Successful in 1m53s

Reviewed-on: #29
This commit was merged in pull request #29.
This commit is contained in:
2026-06-11 10:46:30 +00:00
9 changed files with 234 additions and 0 deletions
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-06-11
@@ -0,0 +1,35 @@
# Design — 2026-06-11 稽核修補
## D1admin 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,不受影響。
## D2rate-limit 漂移以實作(10/min)為基準
測試已在 commit 0dabc4e 有意配合實作改為 10 次,且 P2 帳號鎖定上線後已涵蓋暴力破解防護(per-email 計數),IP-based throttle 的角色退為防濫用,放寬至 10/min 可容納共享 IP(公司/學校 NAT)場景。故更新規格而非改回實作。Admin 維持 3/min 不變。
## D3is_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 一律 403Admin 也不可繞過狀態機(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 tokenrole=admin),依角色查詢指定用戶的基本資料與對應 profile。
#### Scenario: 查詢存在的用戶
- **WHEN** 管理員以有效 id 查詢對應角色的用戶
- **THEN** 回傳 HTTP 200 與該用戶資料
#### Scenario: id 不存在或角色不符
- **WHEN** 查詢的 id 不存在,或該用戶角色與端點不符(如以 check-member 查 provider
- **THEN** 回傳 HTTP 404
@@ -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 與登入頻率限制。此規格定義測試套件必須覆蓋的場景,任何認證行為變更時,對應測試必須同步失敗以攔截回歸。
@@ -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
@@ -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/registerP0
- [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),非回歸