diff --git a/openspec/changes/archive/2026-06-11-audit-remediation/.openspec.yaml b/openspec/changes/archive/2026-06-11-audit-remediation/.openspec.yaml new file mode 100644 index 0000000..e0c0898 --- /dev/null +++ b/openspec/changes/archive/2026-06-11-audit-remediation/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-11 diff --git a/openspec/changes/archive/2026-06-11-audit-remediation/design.md b/openspec/changes/archive/2026-06-11-audit-remediation/design.md new file mode 100644 index 0000000..d940c07 --- /dev/null +++ b/openspec/changes/archive/2026-06-11-audit-remediation/design.md @@ -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 內容。 diff --git a/openspec/changes/archive/2026-06-11-audit-remediation/proposal.md b/openspec/changes/archive/2026-06-11-audit-remediation/proposal.md new file mode 100644 index 0000000..aa45e9a --- /dev/null +++ b/openspec/changes/archive/2026-06-11-audit-remediation/proposal.md @@ -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 diff --git a/openspec/changes/archive/2026-06-11-audit-remediation/specs/admin-auth/spec.md b/openspec/changes/archive/2026-06-11-audit-remediation/specs/admin-auth/spec.md new file mode 100644 index 0000000..ce80313 --- /dev/null +++ b/openspec/changes/archive/2026-06-11-audit-remediation/specs/admin-auth/spec.md @@ -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 diff --git a/openspec/changes/archive/2026-06-11-audit-remediation/specs/auth-test-coverage/spec.md b/openspec/changes/archive/2026-06-11-audit-remediation/specs/auth-test-coverage/spec.md new file mode 100644 index 0000000..84a49d2 --- /dev/null +++ b/openspec/changes/archive/2026-06-11-audit-remediation/specs/auth-test-coverage/spec.md @@ -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 與登入頻率限制。此規格定義測試套件必須覆蓋的場景,任何認證行為變更時,對應測試必須同步失敗以攔截回歸。 diff --git a/openspec/changes/archive/2026-06-11-audit-remediation/specs/login-rate-limiting/spec.md b/openspec/changes/archive/2026-06-11-audit-remediation/specs/login-rate-limiting/spec.md new file mode 100644 index 0000000..66a6dcf --- /dev/null +++ b/openspec/changes/archive/2026-06-11-audit-remediation/specs/login-rate-limiting/spec.md @@ -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 指示等待時間 diff --git a/openspec/changes/archive/2026-06-11-audit-remediation/specs/member-portal-ui/spec.md b/openspec/changes/archive/2026-06-11-audit-remediation/specs/member-portal-ui/spec.md new file mode 100644 index 0000000..8dc615a --- /dev/null +++ b/openspec/changes/archive/2026-06-11-audit-remediation/specs/member-portal-ui/spec.md @@ -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 diff --git a/openspec/changes/archive/2026-06-11-audit-remediation/specs/provider-verification/spec.md b/openspec/changes/archive/2026-06-11-audit-remediation/specs/provider-verification/spec.md new file mode 100644 index 0000000..6a84b58 --- /dev/null +++ b/openspec/changes/archive/2026-06-11-audit-remediation/specs/provider-verification/spec.md @@ -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。 diff --git a/openspec/changes/archive/2026-06-11-audit-remediation/tasks.md b/openspec/changes/archive/2026-06-11-audit-remediation/tasks.md new file mode 100644 index 0000000..0ea5e15 --- /dev/null +++ b/openspec/changes/archive/2026-06-11-audit-remediation/tasks.md @@ -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),非回歸