security(auth): 移除公開 admin/register 端點,改由 app:create-admin 建立管理員
P0 漏洞:原 POST /api/admin/register 無任何防護,任何人可註冊管理員帳號。 - 移除路由、AuthController::registerAdmin、對應 Swagger 文件 - 新增 app:create-admin artisan command(密碼門檻 min:8) - 補 AdminAccountCreationTest 防止端點被重新打開 - 同步 admin-auth 規格,並收錄 2026-06-11 稽核報告與路線圖文檔 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\AdminProfile;
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class CreateAdminUser extends Command
|
||||
{
|
||||
protected $signature = 'app:create-admin
|
||||
{name : 管理員姓名}
|
||||
{email : 登入用電子郵件}
|
||||
{--password= : 密碼(至少 8 碼,未提供時互動式輸入)}
|
||||
{--position= : 職位}
|
||||
{--department= : 部門}';
|
||||
|
||||
protected $description = '建立管理員帳號(公開 /api/admin/register 端點已移除,管理員一律由主機端建立)';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$password = $this->option('password') ?: $this->secret('請輸入密碼(至少 8 碼)');
|
||||
|
||||
$validator = Validator::make([
|
||||
'name' => $this->argument('name'),
|
||||
'email' => $this->argument('email'),
|
||||
'password' => $password,
|
||||
], [
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|string|email|max:255|unique:users',
|
||||
// 管理權限影響全平台,密碼門檻高於一般使用者的 min:6
|
||||
'password' => 'required|string|min:8',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
foreach ($validator->errors()->all() as $error) {
|
||||
$this->error($error);
|
||||
}
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$user = User::create([
|
||||
'name' => $this->argument('name'),
|
||||
'email' => $this->argument('email'),
|
||||
'password' => Hash::make($password),
|
||||
'role' => 'admin',
|
||||
]);
|
||||
|
||||
AdminProfile::create([
|
||||
'user_id' => $user->id,
|
||||
'position' => $this->option('position'),
|
||||
'department' => $this->option('department'),
|
||||
]);
|
||||
|
||||
$this->info("管理員帳號已建立:{$user->email}(id={$user->id})");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -825,58 +825,6 @@ class AuthApiDoc
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理員註冊
|
||||
*
|
||||
* @OA\Post(
|
||||
* path="/admin/register",
|
||||
* summary="管理員註冊",
|
||||
* description="建立新的管理員帳號",
|
||||
* operationId="registerAdmin",
|
||||
* tags={"管理員"},
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"name", "email", "password", "password_confirmation"},
|
||||
* @OA\Property(property="name", type="string", example="張管理", description="使用者姓名"),
|
||||
* @OA\Property(property="email", type="string", format="email", example="admin@example.com", description="電子郵件"),
|
||||
* @OA\Property(property="password", type="string", format="password", example="password123", description="密碼"),
|
||||
* @OA\Property(property="password_confirmation", type="string", format="password", example="password123", description="確認密碼"),
|
||||
* @OA\Property(property="phone", type="string", example="0912345678", description="電話號碼"),
|
||||
* @OA\Property(property="position", type="string", example="系統管理員", description="職位"),
|
||||
* @OA\Property(property="department", type="string", example="IT部門", description="部門")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=201,
|
||||
* description="管理員註冊成功",
|
||||
* @OA\JsonContent(
|
||||
* @OA\Property(property="status", type="boolean", example=true),
|
||||
* @OA\Property(property="message", type="string", example="管理員註冊成功"),
|
||||
* @OA\Property(
|
||||
* property="data",
|
||||
* type="object",
|
||||
* @OA\Property(property="user", type="object", ref="#/components/schemas/User"),
|
||||
* @OA\Property(property="token", type="string", example="1|abcdef1234567890"),
|
||||
* @OA\Property(property="token_type", type="string", example="Bearer")
|
||||
* )
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=422,
|
||||
* description="驗證失敗",
|
||||
* @OA\JsonContent(
|
||||
* @OA\Property(property="status", type="boolean", example=false),
|
||||
* @OA\Property(property="message", type="string", example="驗證失敗"),
|
||||
* @OA\Property(property="errors", type="object")
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function registerAdmin()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理員登入
|
||||
*
|
||||
|
||||
@@ -846,59 +846,6 @@ class AuthController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理員註冊
|
||||
*/
|
||||
public function registerAdmin(Request $request)
|
||||
{
|
||||
// 驗證請求數據
|
||||
$validator = Validator::make($request->all(), [
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|string|email|max:255|unique:users',
|
||||
'password' => 'required|string|min:6|confirmed',
|
||||
'phone' => 'nullable|string|max:20',
|
||||
'position' => 'nullable|string|max:100',
|
||||
'department' => 'nullable|string|max:100',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'status' => false,
|
||||
'message' => '驗證失敗',
|
||||
'errors' => $validator->errors()
|
||||
], 422);
|
||||
}
|
||||
|
||||
// 創建用戶
|
||||
$user = User::create([
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'password' => Hash::make($request->password),
|
||||
'phone' => $request->phone,
|
||||
'role' => 'admin', // 強制為管理員角色
|
||||
]);
|
||||
|
||||
// 創建管理員資料
|
||||
AdminProfile::create([
|
||||
'user_id' => $user->id,
|
||||
'position' => $request->position,
|
||||
'department' => $request->department,
|
||||
]);
|
||||
|
||||
// 創建 API token
|
||||
$token = $user->createToken('auth_token')->plainTextToken;
|
||||
|
||||
return response()->json([
|
||||
'status' => true,
|
||||
'message' => '管理員註冊成功',
|
||||
'data' => [
|
||||
'user' => $user,
|
||||
'token' => $token,
|
||||
'token_type' => 'Bearer',
|
||||
]
|
||||
], 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理員登入
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
# CFDivePlatform 未來發展可行性評估
|
||||
|
||||
> 評估日期:2026-06-11
|
||||
> 前提:核心閉環(瀏覽 → 預約 → 聊天 → 完課 → 評價)已完成並歸檔;本文評估下一階段候選項目的可行性、依賴與建議順序。
|
||||
> 搭配閱讀:《2026-06-11-spec-implementation-audit.md》(修補計畫應先於所有新功能執行,尤其 P0-1)。
|
||||
|
||||
---
|
||||
|
||||
## 一、候選項目總覽
|
||||
|
||||
| # | 項目 | 可行性 | 預估工時 | 商業價值 | 建議優先序 |
|
||||
|---|------|--------|---------|---------|-----------|
|
||||
| 1 | 教練審核流程(完整版) | 高 | 3~5 天 | 高(信任基礎) | ★ 第 1 |
|
||||
| 2 | 金流整合(綠界/藍新) | 中高 | 8~12 天 | 極高(變現核心) | ★ 第 2 |
|
||||
| 3 | 即時聊天室 VPS 部署驗收 | 高 | 0.5~1 天 | 中(已開發未交付) | 隨時可做 |
|
||||
| 4 | CI/CD(Gitea Actions)修復 | 中 | 1~2 天 | 中(工程效率) | 金流前必做 |
|
||||
| 5 | 課程搜尋強化(地區/價格/日期篩選) | 高 | 2~3 天 | 中高 | 第 3 |
|
||||
| 6 | 行動端體驗(PWA) | 中 | 5~8 天 | 中 | 觀望 |
|
||||
|
||||
---
|
||||
|
||||
## 二、逐項評估
|
||||
|
||||
### 2.1 教練審核流程(完整版)— 可行性:高
|
||||
|
||||
**現況基礎**:`provider_profiles.is_verified` 欄位、Admin `toggle-verified` 端點、Admin Panel UI 均已存在;缺的是審核「流程」與約束力(見稽核報告 P1-1)。
|
||||
|
||||
**範圍建議**:
|
||||
1. 教練註冊後狀態為 `pending_review`(建議將 boolean `is_verified` 升級為 enum `verification_status`:pending / approved / rejected)。
|
||||
2. 證照/資料上傳:複用既有 `CourseImageController` 的檔案處理模式(local storage,已有上傳驗證先例)。
|
||||
3. Admin 審核佇列頁 + 通過/駁回(含駁回原因),複用既有 Admin Panel 的列表/操作 UI 模式。
|
||||
4. 審核結果通知:複用 `notification-core` + `notification-email` 既有管線,只需新增 Notification class 與 trigger。
|
||||
|
||||
**技術風險**:低。所有積木(檔案上傳、通知、Admin UI、權限 middleware)都已存在,純組裝工作。
|
||||
**主要決策點**:未通過審核的教練能做到哪一步(可登入編輯資料但不可上架?完全不可登入?)——需產品決策,建議前者。
|
||||
**依賴**:先完成稽核報告 T2.1(最小語意),再擴充為完整流程,避免重工。
|
||||
|
||||
### 2.2 金流整合 — 可行性:中高(工程可行,依賴外部申請流程)
|
||||
|
||||
**金流商比較(台灣市場)**:
|
||||
|
||||
| 面向 | 綠界 ECPay | 藍新 NewebPay |
|
||||
|------|-----------|---------------|
|
||||
| 申請門檻 | 個人/公司皆可,測試環境免申請即用 | 偏好公司戶,測試環境需註冊 |
|
||||
| 文件/社群資源 | Laravel 套件與範例多(生態最成熟) | 文件完整但第三方資源較少 |
|
||||
| 手續費 | 信用卡約 2.75%~ | 相近 |
|
||||
| 結論 | **建議首選**,沙箱即開即用、整合範例多 | 備選 |
|
||||
|
||||
**對現有架構的影響(這是工時主要來源)**:
|
||||
1. **預約狀態機需擴充**:現有七狀態未含付款概念,需插入 `awaiting_payment`、`paid` 等狀態或建立獨立 `payments` 資料表(建議後者:獨立 Payment model + 與 Booking 一對多,狀態機改動最小,符合開放封閉原則)。
|
||||
2. **Webhook 端點**:綠界以 server-side POST 回傳付款結果,需公開端點 + CheckMacValue 驗證 + 冪等處理(重複通知不可重複入帳)——此為資安重點,須走 CLAUDE.md 安全確認流程。
|
||||
3. **退款政策**:取消預約的退款規則(全額/比例/不退)是產品決策,直接影響狀態機與對帳邏輯,**須在開工前定案**。
|
||||
4. **金額快照**:現有 booking 已記錄價格快照(`booking-lifecycle` 規格),付款金額有可靠依據,這是現成優勢。
|
||||
|
||||
**前置條件**:
|
||||
- [ ] 確定金流商並取得測試商店(綠界沙箱可立即取得)。
|
||||
- [ ] 退款政策定案。
|
||||
- [ ] **稽核報告 P0-1 必須先修**(金流上線後,管理端漏洞的損害會從資料外洩升級為金錢損失)。
|
||||
- [ ] 建議 CI 先行(2.4),金流程式碼回歸成本高,不應依賴手動測試。
|
||||
|
||||
**工時拆解(粗估)**:Payment model + 狀態整合 3 天、ECPay 串接與 Webhook 3 天、前端付款流程 2 天、測試(含冪等與失敗路徑)2~4 天。
|
||||
|
||||
### 2.3 即時聊天室 VPS 部署驗收 — 可行性:高
|
||||
|
||||
程式碼已 merge(commit 5ba598c 後系列),記憶中部署狀態未確認。剩餘工作為運維執行:rebuild frontend、restart reverb、`.env` 五個值(細節在既有部署清單中)。建議排入下次部署窗口一次完成,避免長期掛著「已完成未交付」狀態。
|
||||
|
||||
### 2.4 CI/CD(Gitea Actions)修復 — 可行性:中
|
||||
|
||||
已知 runner 的 self-hosted 標籤問題尚未解決。價值在於:測試套件已有 97 案例且將隨修補計畫 Phase 3 成長,沒有 CI 的測試套件會逐漸失去約束力。建議在金流開工前修復,作為金流開發的安全網。風險:Gitea runner 在 Windows/Docker 環境的相容性問題可能再耗時,若兩天內無法解決,備案是改用簡單的 pre-push hook 或部署前手動 gate。
|
||||
|
||||
### 2.5 課程搜尋強化 — 可行性:高
|
||||
|
||||
現有 `GET /api/diving-offers` 已支援關鍵字與分頁;擴充地區/價格區間/可預約日期篩選屬增量開發。注意事項:日期篩選需 join schedules 並留意 N+1 與索引(`db-index-optimization` 規格已有索引設計先例可循);快取層(`api-cache-layer`)的 cache key 需納入新篩選參數。
|
||||
|
||||
### 2.6 行動端 PWA — 可行性:中,建議觀望
|
||||
|
||||
Vue 3 + Vite 加 PWA plugin 技術上直接,但通知推播(Web Push)與 iOS 限制會吃掉多數工時,且現階段使用者基數未驗證。建議待金流上線、有真實流量後再評估。
|
||||
|
||||
---
|
||||
|
||||
## 三、建議路線圖
|
||||
|
||||
```
|
||||
Week 1 修補計畫 Phase 1~4(P0 封鎖 + 規格同步 + 預約測試)
|
||||
└─ 並行:聊天室 VPS 部署驗收(2.3)
|
||||
Week 2 教練審核流程完整版(2.1)+ CI 修復(2.4)
|
||||
Week 3~4 金流整合(2.2):Payment model → ECPay 沙箱 → Webhook → 前端
|
||||
Week 5 金流測試強化 + 課程搜尋強化(2.5)
|
||||
```
|
||||
|
||||
**關鍵判斷**:
|
||||
1. 教練審核排在金流之前——付費平台上架未經審核的教練,發生糾紛時平台責任更重;審核機制是金流的信任前提。
|
||||
2. 金流是唯一含外部依賴(商店申請、Webhook 公網可達性)的項目,沙箱申請應在 Week 1 就先送出,與其他工作並行等待。
|
||||
3. 所有新功能開工前,稽核報告的 P0-1(admin register 漏洞)必須先關閉。
|
||||
|
||||
## 四、不建議現在做的事
|
||||
|
||||
- **微服務化 / 抽換架構**:單體 Laravel 在現階段流量下完全足夠,拆分只會增加部署與除錯成本。
|
||||
- **多金流商抽象層**:先用介面包一層 `PaymentGatewayContract` 即可(依賴反轉),但不要實作第二家金流——沒有需求前是投機性程式碼(違反 Simplicity First)。
|
||||
- **評價 AI 審核 / 推薦系統**:資料量不足,無訓練與評估基礎。
|
||||
@@ -0,0 +1,140 @@
|
||||
# CFDivePlatform 規格與實作分析報告暨修補計畫
|
||||
|
||||
> 分析日期:2026-06-11
|
||||
> 分析範圍:`openspec/specs/`(32 份規格)vs 後端實作(Laravel 11)、前端(`frontend/` Vue 3)、測試(`tests/`)
|
||||
> 分析基準:branch `test/auth-tests-coverage`(commit ca5b843,與 master 同步)
|
||||
|
||||
---
|
||||
|
||||
## 一、現況總覽
|
||||
|
||||
### 1.1 規格狀態
|
||||
|
||||
- OpenSpec 規格共 **32 份**,全部 change 已歸檔(`openspec/changes/` 僅剩 `archive/`,無進行中 change)。
|
||||
- 涵蓋範圍:認證安全(lockout / rate-limiting / OAuth state / token refresh)、三角色入口(Member / Coach / Admin)、預約生命週期、評價系統、通知系統、即時聊天、Swagger 文件、效能優化。
|
||||
|
||||
### 1.2 實作狀態
|
||||
|
||||
| 模組 | 規格 | 實作 | 測試 |
|
||||
|------|------|------|------|
|
||||
| 認證(三角色登入/註冊/登出) | ✅ | ✅ | ✅ 39 案例 |
|
||||
| 帳號鎖定 + OAuth state | ✅ | ✅ | ✅ 15 案例 |
|
||||
| Token Refresh | ✅ | ✅ | ✅ 11 案例 |
|
||||
| 課程 CRUD + 圖片上傳 | ✅ | ✅ | ✅ 14 案例 |
|
||||
| 開課時段管理 | ✅ | ✅ | ❌ 無 |
|
||||
| 預約生命週期(七狀態機、防超賣、Scheduler) | ✅ | ✅ | ❌ 無 |
|
||||
| 評價系統(匿名、投票、rating 重算) | ✅ | ✅ | ✅ 32 案例 |
|
||||
| 通知系統(站內 + Email) | ✅ | ✅ | ❌ 無 |
|
||||
| 即時聊天 + Presence | ✅ | ✅ | ❌ 無 |
|
||||
| Admin 管理端點(stats / users / offers / bookings / reviews) | ✅ | ✅ | ❌ 無 |
|
||||
| 教練審核流程 | ❌ 無規格 | ⚠️ 僅半套(見 P1-1) | ❌ 無 |
|
||||
| 金流整合 | ❌ 無規格 | ❌ 未實作 | — |
|
||||
|
||||
整體而言規格與實作的對應度高,路由、端點、middleware 配置大致與規格一致。以下列出本次稽核發現的偏差與風險。
|
||||
|
||||
---
|
||||
|
||||
## 二、發現問題(依嚴重度排序)
|
||||
|
||||
### 🔴 P0-1:`POST /api/admin/register` 完全公開,任何人可註冊管理員帳號
|
||||
|
||||
- **位置**:`routes/api.php:106`、`AuthController::registerAdmin()`(`AuthController.php:852`)
|
||||
- **事實**:該路由無任何 middleware、無邀請碼、無「僅限第一位管理員」檢查;request 通過基本驗證後直接 `role => 'admin'` 建立帳號。
|
||||
- **影響**:攻擊者一個 HTTP 請求即可取得全平台管理權限(停權用戶、刪課程、刪評價、看全部個資),**使 P0~P2 所有認證安全強化形同虛設**。
|
||||
- **規格對應**:`admin-auth` 規格只定義「管理員登入」,**從未定義公開註冊端點**——此端點本身就是規格外實作。
|
||||
- **附帶問題**:密碼規則僅 `min:6`,低於一般管理帳號標準。
|
||||
|
||||
### 🟠 P1-1:`is_verified`(教練驗證)從未被任何業務邏輯強制執行
|
||||
|
||||
- **事實**:`is_verified` 只出現在兩處——Admin 的 `toggleProviderVerified()`(`AdminUserController.php:144`)與 Model 屬性定義。教練**註冊後不需任何審核即可登入、上架課程、被公開列表曝光、接受預約**。
|
||||
- **影響**:Admin 後台的「驗證教練」開關是純展示功能,平台對教練資質零把關。這就是記憶中「教練審核流程未實作」的實際狀態:開關存在、約束不存在。
|
||||
- **規格對應**:目前**沒有任何規格**定義 `is_verified` 的業務語意(未驗證教練能做什麼/不能做什麼),屬規格空洞。
|
||||
|
||||
### 🟠 P1-2:登入頻率限制規格與實作不一致(規格漂移)
|
||||
|
||||
- **規格**(`openspec/specs/login-rate-limiting/spec.md`):Member / Provider 每 IP 每分鐘最多 **5 次**。
|
||||
- **實作**(`routes/api.php:31,67`):`throttle:10,1`,即 **10 次**。
|
||||
- **測試**(`AuthRateLimitTest.php`):已在 commit 0dabc4e 改為斷言 10 次,**與實作一致、與規格矛盾**。
|
||||
- **影響**:規格失去單一真實來源(single source of truth)地位。未來任何人依規格實作或稽核都會得出錯誤結論。Admin 的 3 次限制則規格實作一致。
|
||||
|
||||
### 🟡 P2-1:核心業務流程(預約)零測試覆蓋
|
||||
|
||||
- 現有 97 個測試案例集中在認證(70)、評價(32 中含投票)、圖片(14)。
|
||||
- **預約狀態機(七狀態)、防超賣鎖定、Scheduler 自動過期/完成**(`ExpirePendingBookings`、`CompleteFinishedBookings`)——平台最核心、含金量最高、最容易因修改而回歸的邏輯——**沒有任何一條測試**。
|
||||
- 同樣無測試:時段管理(時段重疊/容量)、通知觸發、聊天授權(presence channel 參與方驗證)、全部 Admin 端點(含 P0-1 的權限邊界)。
|
||||
- 依 CLAUDE.md Rule 9,這些測試需編碼「為什麼」:防超賣測試保護的是金錢與信任,狀態機測試保護的是不可逆操作的合法轉移。
|
||||
|
||||
### 🟡 P2-2:規格維護債
|
||||
|
||||
1. `auth-test-coverage/spec.md` 的 Purpose 仍是 `TBD - created by archiving change`,歸檔後未補。
|
||||
2. `member-portal-ui` 規格寫「前端 SHALL 建立於獨立 repo」,實際前端在本 repo 的 `frontend/` 目錄——規格描述已過時。
|
||||
3. `admin-auth` 規格未涵蓋實際存在的 `/admin/register`、`/admin/check-member/{id}`、`/admin/check-provider/{id}` 端點。
|
||||
|
||||
---
|
||||
|
||||
## 三、修補計畫
|
||||
|
||||
### Phase 1 — 立即(建議今天,0.5 天內)
|
||||
|
||||
**T1.1 封鎖 `POST /api/admin/register`**(對應 P0-1)
|
||||
|
||||
建議方案(擇一,建議 a):
|
||||
- (a) **移除公開路由**,改為 artisan command `php artisan admin:create`(僅能從主機執行),生產環境的管理員一律由既有管理員或主機端建立。
|
||||
- (b) 移入 `auth:sanctum + admin` middleware group(僅既有 admin 可開新 admin)——但需先用 (a) 或 seeder 解決第一位管理員的雞生蛋問題。
|
||||
|
||||
驗收標準:
|
||||
- [ ] 未認證請求 `POST /api/admin/register` 回傳 404 或 401(不再是 201)。
|
||||
- [ ] 補 Feature test:未授權者無法建立 admin 帳號。
|
||||
- [ ] Demo seeder 既有的 admin 帳號不受影響。
|
||||
- [ ] 同步新增/更新 `admin-auth` 規格段落,明確定義管理員帳號建立途徑。
|
||||
|
||||
**T1.2 解決 rate-limiting 規格漂移**(對應 P1-2)
|
||||
|
||||
- 先決定基準:5/min(規格)或 10/min(實作)。10/min 是後來有意調整(測試已配合修改),建議**以實作為準、更新規格**;若當初是誤改,則改回 5 並還原測試。
|
||||
- 驗收標準:規格、`routes/api.php`、`AuthRateLimitTest` 三者數字一致。
|
||||
|
||||
### Phase 2 — 短期(1~2 天)
|
||||
|
||||
**T2.1 賦予 `is_verified` 最小業務語意**(對應 P1-1,為完整教練審核流程鋪路)
|
||||
|
||||
最小可行約束(不做完整審核流程,僅堵住風險):
|
||||
- 未驗證 Provider 的課程不出現在公開列表 `GET /api/diving-offers`(或:未驗證 Provider 無法將課程上架/`store()` 回 403,二擇一,依產品決策)。
|
||||
- 課程詳情頁顯示教練 `is_verified` 徽章(前端已有欄位可用)。
|
||||
- 驗收標準:
|
||||
- [ ] 新增規格 `provider-verification`(最小版),定義未驗證教練的能力邊界。
|
||||
- [ ] Feature test:未驗證教練的課程不可被公開查詢(或不可建立)。
|
||||
- [ ] Admin `toggle-verified` 後行為立即生效(注意 `api-cache-layer` 的快取失效)。
|
||||
|
||||
> 完整審核流程(證照上傳、審核佇列、駁回原因)屬新功能,見《未來發展可行性評估》文檔。
|
||||
|
||||
### Phase 3 — 中期(2~3 天)
|
||||
|
||||
**T3.1 補預約核心測試**(對應 P2-1,優先序由高至低)
|
||||
|
||||
1. `BookingLifecycleTest`:七狀態機合法/非法轉移(pending→confirmed→completed、reject、cancel、不可逆狀態防護)。
|
||||
2. `BookingOversellTest`:併發/邊界情境下 confirmed 不超過時段容量;pending 不佔名額。
|
||||
3. `BookingSchedulerTest`:`app:expire-pending-bookings` 與 `app:complete-finished-bookings` 的時間邊界。
|
||||
4. `BookingChatAuthTest`:非參與方無法讀寫訊息、無法訂閱 presence channel。
|
||||
5. `AdminEndpointTest`:非 admin token 存取 `/api/admin/*` 一律 403。
|
||||
|
||||
驗收標準:以上測試全綠且任一狀態機規則被改壞時至少一條測試會失敗(mutation 自查)。
|
||||
|
||||
### Phase 4 — 規格清理(0.5 天,可與 Phase 3 並行)
|
||||
|
||||
- [ ] 補 `auth-test-coverage` 的 Purpose。
|
||||
- [ ] 更新 `member-portal-ui` 的 repo 描述。
|
||||
- [ ] `admin-auth` 補 `check-member` / `check-provider` 端點描述(`register` 端點依 T1.1 決策寫入或標記移除)。
|
||||
|
||||
---
|
||||
|
||||
## 四、風險與依賴
|
||||
|
||||
| 項目 | 風險 | 緩解 |
|
||||
|------|------|------|
|
||||
| T1.1 | 前端 Admin 註冊頁(若存在)會失效 | 檢查 `frontend/src/views/admin/`,同步移除入口 |
|
||||
| T2.1 | 公開列表過濾可能影響 demo 資料展示 | Demo seeder 將教練預設 `is_verified=true` |
|
||||
| T3.1 | 測試需 MySQL 容器運行 | 沿用既有 `php artisan test` 容器內流程 |
|
||||
|
||||
## 五、規格同步狀態聲明
|
||||
|
||||
本報告本身不改動程式碼。執行修補計畫時需同步的規格文件:`admin-auth`(T1.1)、`login-rate-limiting`(T1.2)、新增 `provider-verification`(T2.1)、`auth-test-coverage` 與 `member-portal-ui`(Phase 4)。
|
||||
@@ -1,5 +1,22 @@
|
||||
## 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 提供 `POST /api/admin/login`(現有 AuthController 方法),驗證 email/password 並確認 role=admin,回傳有效期 7 天的 Bearer token。
|
||||
|
||||
|
||||
+1
-2
@@ -102,8 +102,7 @@ Route::middleware(['auth:sanctum'])->prefix('provider')->group(function () {
|
||||
Route::put('/bookings/{id}/complete', [ProviderBookingController::class, 'complete']);
|
||||
});
|
||||
|
||||
// 管理員註冊/登入
|
||||
Route::post('/admin/register', [AuthController::class, 'registerAdmin']);
|
||||
// 管理員登入(帳號建立僅限主機端 php artisan app:create-admin,不提供公開註冊)
|
||||
Route::middleware('throttle:3,1')->post('/admin/login', [AuthController::class, 'loginAdmin']);
|
||||
|
||||
// 管理員專屬 API(需登入)
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* 管理員帳號建立途徑防護。
|
||||
*
|
||||
* 公開的 POST /api/admin/register 曾允許任何人註冊管理員帳號(P0 漏洞,
|
||||
* 2026-06-11 稽核移除)。這些測試確保該端點不會被重新打開,
|
||||
* 且唯一合法途徑是主機端的 app:create-admin command。
|
||||
*/
|
||||
class AdminAccountCreationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
// ── 公開註冊端點必須保持關閉 ─────────────────────────────
|
||||
|
||||
public function test_public_admin_register_endpoint_is_closed(): void
|
||||
{
|
||||
$response = $this->postJson('/api/admin/register', [
|
||||
'name' => 'Evil Admin',
|
||||
'email' => 'evil@example.com',
|
||||
'password' => 'password123',
|
||||
'password_confirmation' => 'password123',
|
||||
]);
|
||||
|
||||
$response->assertStatus(404);
|
||||
$this->assertDatabaseMissing('users', ['email' => 'evil@example.com']);
|
||||
$this->assertDatabaseMissing('users', ['email' => 'evil@example.com', 'role' => 'admin']);
|
||||
}
|
||||
|
||||
// ── artisan command 為唯一建立途徑 ───────────────────────
|
||||
|
||||
public function test_create_admin_command_creates_admin_with_profile(): void
|
||||
{
|
||||
$this->artisan('app:create-admin', [
|
||||
'name' => 'Ops Admin',
|
||||
'email' => 'ops@example.com',
|
||||
'--password' => 'StrongPass99',
|
||||
])->assertSuccessful();
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'email' => 'ops@example.com',
|
||||
'role' => 'admin',
|
||||
]);
|
||||
|
||||
$admin = User::where('email', 'ops@example.com')->first();
|
||||
$this->assertDatabaseHas('admin_profiles', ['user_id' => $admin->id]);
|
||||
}
|
||||
|
||||
public function test_create_admin_command_rejects_duplicate_email(): void
|
||||
{
|
||||
User::factory()->create(['email' => 'exists@example.com']);
|
||||
|
||||
$this->artisan('app:create-admin', [
|
||||
'name' => 'Dup Admin',
|
||||
'email' => 'exists@example.com',
|
||||
'--password' => 'StrongPass99',
|
||||
])->assertFailed();
|
||||
|
||||
$this->assertDatabaseMissing('users', [
|
||||
'email' => 'exists@example.com',
|
||||
'role' => 'admin',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_create_admin_command_rejects_weak_password(): void
|
||||
{
|
||||
$this->artisan('app:create-admin', [
|
||||
'name' => 'Weak Admin',
|
||||
'email' => 'weak@example.com',
|
||||
'--password' => 'short',
|
||||
])->assertFailed();
|
||||
|
||||
$this->assertDatabaseMissing('users', ['email' => 'weak@example.com']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user