W101 Admin CLI(公司後台終端介面)PRD
PRD 代號:
pm_54延續:pm_41(wport CLI v2 企業線)、pm_47(wport CLI v3 個人線・OAuth device flow)、pm_48(MCP connector・BR-039 通道來源審計)、pm_37(Enterprise REST) 參考架構:hotfire-digital/wport-cli(@wport/cli0.9.2)——本 PRD 沿用其分層、契約與安全慣例,但另發套件、另發 registry(ADR-003) 目標 repo:W101-AMS(後端,主要工作量)、W101-Admin-Web(GUI parity 對照基準+新增 1 個 web 觸點)、新建W101-Admin-CLI產品原則(沿 pm_41/pm_47,本 PRD 縮限):Agent-first、deny-list 制、限量取代禁令 —— 但 admin 線因權限最高,額外加上「通道不得擴權」硬約束(BR-API-CLI-019) 本 PRD 的核心事實:GUI parity 在此案做不到也不該做——前端 18 個路由模組中僅約 2/3 有真實後端端點,其餘為 mock-only(§3.4、§13.2)
1. 文件資訊
- 文件類型:CLI + OAuth + 後端 API 擴充需求規格書
- 適用對象:CLI 工程師、後端(AMS)、前端(Admin-Web,1 個 web 觸點)、DevOps、資安、產品、QA、內部營運
- 最後更新:2026/09/09
- 版本:1.2.0
- PRD 識別碼:
pm_54 - 對應 Storybook:待產出(§9 W-1 裝置驗證頁;文字規格為現階段 SoT,見 §17 sync-fix #9)
- Storybook(local-prd):N/A — 尚未產出,以本 PRD §9 為 SoT
- Storybook(online):N/A
- 文件入口:
doc/feature/pm_54/admin-cli-prd.md - 建立日期:2026/08/25
- 套件名稱:
@wport/admin-cli(私有 registry)/執行檔wportadm(ADR-003) - 代號對照:
pm_41= 企業線 CLI|pm_47= 個人線 CLI|pm_48= MCP|本 PRDpm_54= 後台 admin 線 CLI
檔頭的 版本 與 最後更新 等於 §1.1 修訂紀錄最後一列。
1.1 修訂紀錄
| 版本 | 日期 | 摘要 |
|---|---|---|
| 1.0.0 | 2026/08/25 | 初版。依 wport-cli@c49a610/W101-AMS@bf0fb57/W101-Admin-Web@0ee5ee9 實查撰寫。三項 Eric 拍板:①認證採 admin device flow OAuth(ADR-001)②v1 範圍=AMS 已實作端點全包(ADR-002)③私有 registry 發布(ADR-003)。含 14 條 ADR、7 條新 BR 提案(BR-API-CLI-018~024)。三項關鍵發現(皆為實查所得,非推論):①AMS 現況發 7 天全權限 JWT、無 refresh、無單一 session 撤銷 ②AMS 全 src/ 無 X-Source 收錄,與 BR-039 有落差 ③role 快取於 token 且 80 個 @Auth 端點無一宣告 permissions,DB 權限層形同虛設(§19 問題 9) |
| 1.0.1 | 2026/08/26 | PRD 代號自 pm_53 改為 pm_54(撞號更正,內容零變動)。pm_53 已於 2026/08/20 配發給「職缺生命週期與 SEO 索引健康度」PRD(ericlu-sys/job-posting 分支,當時尚未推送至 origin,故 v1.0.0 撰寫時的撞號檢查只掃 refs/remotes/origin 而漏看)。教訓已回寫至 §19 問題 10:配號前須掃全 ref + 其他 worktree,不能只看已推送分支 |
| 1.0.2 | 2026/08/26 | 併入原本要另開 PRD 的授權模型查核結果,但不指定修法(Eric:授權怎麼做交給後端構思;亦符合 gen-prd Rule 31 的 PRD/Spec 界線)。三處變更:(一) §2 台帳新增 8 列實查事實——RBAC 四張表與四個角色代碼皆存在、hasRoleCode() 正確,但 resolveAdminIdentity() 只做二分、getPermissions() 為寫死 stub、建立管理員的 DTO 無角色欄位且從不寫 admin_user_role、新帳號 MFA 預設關閉、API 回應含明碼密碼。(二) §3.4 N-4 補誠實範圍——該條約束的是「CLI 不新增旁路」,不代表 admin 線一定過得了 MFA(現況 MFA 為每帳號可選)。(三) §19 問題 1 改寫為六項現況+三項產品層要求(MFA 強制、預設最小權限、可降權與撤銷),機制留白;問題 9 併入其中;sync-fix #16 改為單一交辦項 |
| 1.0.3 | 2026/08/26 | 依 Eric 對 web 觸點範圍的裁決補齊:(一) §9 新增 W-2「CLI 存取頁」(設定 > 帳號設定 新增分頁,與 MFA 設定相鄰)與 W-3「管理員列表 CLI 治理視圖」,兩者皆標 v2;並記錄 W-2 不能放 會員管理 > 管理員列表 的原因——該區塊全部端點限 SUPER_ADMIN,一般 admin 打不開,自助安裝的雞生蛋問題不會被解掉。(二) §14 版本凍結表補 W-2/W-3 兩列與延後理由。(三) §19 新增問題 11,記錄「軟刪除管理員=緊急撤銷路徑」這條替代方案的成立依據與其 all-or-nothing 限制,避免後人重新推導。另釐清:安裝憑證屬套件存取權,與後台存取權為不同信任域,不牴觸 ADR-001 與核彈 N-4 |
| 1.1.0 | 2026/09/09 | 依 Eric 補充真實需求來源,取代原本純推導的策略理由。(一) §3.1 新增「實際驅動力」表:D-1 IDE 已成為同仁工作介面、D-2 每月股東報告需抓後台控制台資料、D-3 行銷 UTM 管理。(二) §3.2 驅動力表新增兩列:IDE 的使用者已擴散到行銷與客服組(不只 RD),以及本案沿用 pm_41/pm_47 既有 CLI 底盤而非從零開始。(三) §2 台帳新增 3 列並補 wport_skills 為引用 repo——實查發現股東報告管線已在取 AMS 主控台,但認證是「以 chrome-token.mjs 從 Chrome 撈未過期 token」,撈不到則手動登入或在 .env 放明碼 AMS_EMAIL/AMS_PASSWORD。此現況使 D-2 同時成為功能需求與資安改善,並與 §19 問題 1(7 天 JWT)為同一件事的兩面 |
| 1.2.0 | 2026/09/09 | 北極星改寫(Eric:核心是同仁都已在 IDE,讓大家更好做事)。原 US-1「30 家公司審核自動化」是從「哪些工作可自動化」推導出來的,不是同仁實際提出的需求;改以 D-1 為北極星——讓後台從「要切出去做的系統」變成「工作流裡的一個命令」,衡量標準是完成一件後台工作需不需要離開編輯器。原審核情境降為三個代表情境之一(S-3),與 S-1 股東月報取數、S-2 行銷 UTM 並列。連帶依「修正擴散」原則全文回掃 US-1 的 7 處引用(§3.5 S1/S6、§6.4 標題、§8.4、ADR-008、§14、§19 問題 7)全部改指 S-3;§3.5 S1 一併改寫為「後台工作可全程於終端完成,不需離開編輯器」 |
2. 開發進度與設計來源
開發進度
- CLI PR #:[TBD](新建 repo
W101-Admin-CLI) - 後端 PR #(
W101-AMS):[TBD](admin device flow AS + session 撤銷 +X-Source收錄 + audit channel) - 前端 PR #(
W101-Admin-Web):[TBD](§9 W-1 裝置驗證頁 + 已授權 CLI session 清單)
事實基礎(實查證據台帳)
本表是本 PRD 的證據台帳(gen-prd Rule 33)。凡斷言「既有程式怎麼運作」的句子都必須在此有一列; 沒有列的斷言,該句自帶
(推論,未實查)。來源欄格式固定為repo@<short-sha> path:line。
Repo 基準(Rule 34 前置檢查,2026/08/25 執行 git fetch):
| Repo | Branch | Sha | 日期 |
|---|---|---|---|
wport-cli | origin/main | c49a610 | 2026/08/24 |
W101-Admin-Web | origin/develop | 0ee5ee9 | 2026/08/12 |
W101-AMS | origin/dev | bf0fb57 | 2026/08/12 |
wport_skills | main | 8be06d1 | 2026/08/26 |
基準選擇說明:本機
W101-Admin-Web停在main(落後origin/develop130 commits)、W101-AMS停在prod(與origin/dev相差 155/24)。實查git diff --stat後確認該落差主要為 squash-merge vs merge-commit 的歷史差異,src/實際內容差異僅限gallery兩個 view 與company模組的 chat-rooms repository,皆不在本 PRD 斷言範圍內。所有行號一律以git show origin/<branch>:<path>取得,未取自本機工作樹。
斷言 → 證據:
| 事實 | 來源 |
|---|---|
AMS 掛 global prefix,值為 ams-api,故 wire 路徑一律 /ams-api/... | W101-AMS@bf0fb57 src/main.ts:40;config/env/.env.example:14 |
Admin-Web 前端亦以 /ams-api 為 API prefix | W101-Admin-Web@0ee5ee9 .env.development:26、.env.production |
admin 登入成功發 7 天 JWT('7d'),無 refresh token | W101-AMS@bf0fb57 src/modules/admin/service/admin.service.ts:139(未啟用 MFA 路徑)、:120(信任裝置路徑)、:180(MFA 驗證後) |
| 未啟用信任裝置時,登入先發 5 分鐘 TEMP token(另一組 secret),需再過 MFA | W101-AMS@bf0fb57 src/modules/admin/service/admin.service.ts:130-133 |
MFA 為 TOTP,驗證窗格 window: 2 | W101-AMS@bf0fb57 src/modules/admin/service/admin.service.ts:167,171 |
信任裝置有效期 30 天,以 cookie device_token 承載 | W101-AMS@bf0fb57 src/modules/admin/service/admin.service.ts:186 |
JWT payload 僅含 { id, email, role };驗證時不忽略過期、從 Authorization Bearer 取值 | W101-AMS@bf0fb57 src/modules/auth/jwt.strategy.ts:11,12,35-37 |
授權採 @Auth(roles, permissions?),內部串 JwtAuthGuard + RolesPermissionsGuard 兩層 | W101-AMS@bf0fb57 src/modules/auth/decorators/auth.decorator.ts:7-8 |
角色檢查走 Permission.checkRolePermission(比對 token payload 的 role);功能權限另查 DB | W101-AMS@bf0fb57 src/modules/auth/guards/roles-permissions.guard.ts:30-47、:41 |
DB 權限查詢僅在端點宣告 permissions 時才執行;現況 80 個 @Auth 端點中有 0 個帶第二參數,故實務上只跑 role 檢查 | W101-AMS@bf0fb57 src/modules/auth/guards/roles-permissions.guard.ts:39;git grep "@Auth(\[" -- 'src/**/*.controller.ts' 計數(2026/08/25 執行) |
每請求都會查帳號狀態(checkAccountStatus),故停用帳號即時生效;但 role 直接取自 token payload | W101-AMS@bf0fb57 src/modules/auth/jwt.strategy.ts:26,37 |
RBAC 四張表在 admin_management schema 全部存在:role/permission/role_permission/admin_user_role | W101-AMS@bf0fb57 src/database/entities/admin_management/entities/Role.ts、Permission.ts、RolePermission.ts、AdminUserRole.ts |
系統定義四個管理員角色代碼:superadmin/admin/manager/support | W101-AMS@bf0fb57 src/common/constants/admin-roles.constants.ts:5-9 |
hasRoleCode() 以 admin_user_role join role.code 查角色,實作正確 | W101-AMS@bf0fb57 src/modules/admin/repository/admin.repository.ts:21 |
但 resolveAdminIdentity() 只問「是不是 superadmin」,回傳二選一;manager/support 執行期等同完整 admin | W101-AMS@bf0fb57 src/modules/admin/service/admin.service.ts:40-43 |
getPermissions() 是寫死 stub,對所有管理員回傳固定三個字串,註解自承「還沒時間實作,先返回假數據」 | W101-AMS@bf0fb57 src/modules/admin/service/admin.service.ts:267-272,註解在 :269 |
CreateAdminUserDto 與 UpdateAdminUserDto 各只有 email 一個欄位,無角色、無狀態 | W101-AMS@bf0fb57 src/modules/admin/dto/create-admin-user.dto.ts、update-admin-user.dto.ts |
createAdminUser() 全程未寫入 admin_user_role;新管理員 mfaEnabled: false、status: ACTIVE,且 API 回應含明碼隨機密碼 | W101-AMS@bf0fb57 src/modules/admin/service/admin-user.service.ts:22-66,關鍵在 :46,49,65 |
登入時 if (admin.mfaEnabled) 為偽即直接發完整 token(MFA 為每帳號可選) | W101-AMS@bf0fb57 src/modules/admin/service/admin.service.ts:106 |
刪除管理員為軟刪除(寫 deletedAt),且不得刪除自己 | W101-AMS@bf0fb57 src/modules/admin/service/admin-user.service.ts:130-141 |
isActive() 要求 deletedAt IS NULL 且 status === ACTIVE;此檢查經 checkAccountStatus 在每個請求執行,故軟刪除或停用後下一請求即 401 | W101-AMS@bf0fb57 src/modules/admin/repository/admin.repository.ts:193-199;src/modules/admin/service/admin.service.ts:279-281;src/modules/auth/jwt.strategy.ts:26 |
帳號設定頁已為分頁結構(BasicSetting / SafetySetting),且 SafetySetting 即 MFA 綁定所在 | W101-Admin-Web@0ee5ee9 src/views/setting/account/account.vue:3-4,52-53;src/views/setting/account/SafetySetting.vue:18,23,37 |
會員管理 選單包含公司列表/求職者列表/管理員列表三項 | W101-Admin-Web@0ee5ee9 src/router/modules/members.ts:14,20,26,32 |
auth/mfa controller 路徑 | W101-AMS@bf0fb57 src/modules/auth/controllers/mfa.controller.ts:17 |
admin/enterprise-api-keys controller 路徑 | W101-AMS@bf0fb57 src/modules/enterprise-api-key/controllers/enterprise-api-key-admin.controller.ts:27 |
admin/partner-keys controller 路徑 | W101-AMS@bf0fb57 src/modules/partner-key/controllers/partner-key-admin.controller.ts:35 |
marketing/posts controller 路徑 | W101-AMS@bf0fb57 src/modules/marketing/controllers/posts.controller.ts:26 |
siteadmin/v2 controller 路徑 | W101-AMS@bf0fb57 src/modules/siteadmin/controllers/siteadmin-dashboard.controller.ts:12 |
scheduler controller 路徑 | W101-AMS@bf0fb57 src/scheduling/controllers/scheduler.controller.ts:11 |
後台 admin 與前台 account 完全分離,admin 登入走 /admin/* + MFA | W101-Admin-Web@0ee5ee9 CLAUDE.md:15 |
參考 CLI 顯示金鑰一律遮罩至末四碼(maskKey) | wport-cli@c49a610 packages/cli/src/lib/credentials-store.ts:73 |
全域 ValidationPipe 開 whitelist + forbidNonWhitelisted(多帶欄位直接 400) | W101-AMS@bf0fb57 src/main.ts:78-79 |
Swagger 僅在 NODE_ENV !== 'production' 掛載於 /ams-api/api-doc | W101-AMS@bf0fb57 src/main.ts:90,110 |
| 既有節流常數:DEFAULT 5/60s、STRICT 5/900s、ADMIN_LIST 20/60s、ADMIN_ACTION 10/60s | W101-AMS@bf0fb57 src/common/constants/throttle.constants.ts:6-7,11,16,21 |
後台操作稽核表已存在,欄位含 adminUserId/ipAddress/requestId/changedFields/metadata/remarks | W101-AMS@bf0fb57 src/modules/admin-operation-logs/interfaces/admin-operation-log.interface.ts:42,51,53 |
| 稽核字典已定義 module/action/table 三組 enum | W101-AMS@bf0fb57 src/modules/admin-operation-logs/interfaces/admin-operation-log.interface.ts:59,71,87 |
AMS 全 src/ 無任何 X-Source 收錄(grep -rn "X-Source|x-source" src/ 零結果) | W101-AMS@bf0fb57(negative grep,2026/08/25 執行) |
job/v2 僅有 search 與 :enc_id/deactivate,無單筆詳情端點 | W101-AMS@bf0fb57 src/modules/job/controllers/job.controller.ts:14,23,57 |
company/v2 提供 search/detail/info 更新/文件審核/刪除 | W101-AMS@bf0fb57 src/modules/company/controllers/company.controller.ts:29,38,75,104,141,192 |
resume/v2 提供 search/detail/停用狀態切換 | W101-AMS@bf0fb57 src/modules/resume/controllers/resume.controller.ts:14,23,57,84 |
account/v2 提供 search/detail/狀態切換 | W101-AMS@bf0fb57 src/modules/account/controllers/account.controller.ts:16,25,59,86 |
admin/users 全部端點限 SUPER_ADMIN | W101-AMS@bf0fb57 src/modules/admin/controller/admin-user.controller.ts:17,25,55,80,111,143 |
前端管理員頁確實打 /admin/users 與 /auth/mfa/admin/disable/{id} | W101-Admin-Web@0ee5ee9 src/api/members/admins.ts:7,19,52,64 |
Admin-Web 本地 pnpm dev 為純 Mock 模式,未匹配的請求會 throw、不 fallback 真實 API | W101-Admin-Web@0ee5ee9 CLAUDE.md:47、mock/README.md:25,162 |
| Admin-Web 真實後端不包 envelope,直接回 flat shape | W101-Admin-Web@0ee5ee9 mock/README.md:11 |
sub_accounts(子帳號)已廢棄,新 code 不得使用 | W101-Admin-Web@0ee5ee9 CLAUDE.md:22 |
legacy endpoint(/members/jobSeekersData、/members/merchantsXxx、/job/* 單數)標 @deprecated,逐步移除 | W101-Admin-Web@0ee5ee9 CLAUDE.md:23 |
| 後台三環境:dev-admin/staging-admin/admin.wport.me | W101-Admin-Web@0ee5ee9 CLAUDE.md:51-53 |
| 參考 CLI 的 exit code 契約為 0/2/3/4/5 | wport-cli@c49a610 packages/cli/src/lib/errors.ts:6-10 |
| 參考 CLI 的設定解析優先序:flag > env var > config > 預設;base URL 刻意不落地 config(SSRF 面) | wport-cli@c49a610 packages/cli/src/lib/global-opts.ts:38,58-61,64 |
參考 CLI 憑證檔採 atomic write + 0o600,並依 build channel 分檔 | wport-cli@c49a610 packages/cli/src/lib/credentials-store.ts:63-64,160,163 |
| 參考 CLI 對所有終端輸出做 ANSI/控制字元淨化,防 escape injection | wport-cli@c49a610 packages/cli/src/lib/output.ts:19,43-44,100,108 |
參考 CLI 已具備 X-Source 出口與 Idempotency-Key/If-Match 寫入 header | wport-cli@c49a610 packages/core/src/enterprise-transport.ts:61,93-95 |
| 股東月報資料管線的四個來源之一為 AMS 主控台 | wport_skills@8be06d1 src/gen-shareholder-report/SKILL.md:42 |
該管線取得 AMS 存取權的方式:先以 chrome-token.mjs 從 Chrome 撈尚未過期的 token | wport_skills@8be06d1 src/gen-shareholder-report/SKILL.md:45 |
撈不到時的替代方案為請 Eric 手動登入 admin.wport.me,或於 .env 放明碼 AMS_EMAIL / AMS_PASSWORD | wport_skills@8be06d1 src/gen-shareholder-report/SKILL.md:46 |
參考 CLI 以 CLI_SOURCE = 'cli' 單一常數收斂來源標記(pm_48 交付 B/BR-039) | wport-cli@c49a610 packages/cli/src/lib/global-opts.ts:17 |
設計稿
- Figma:N/A(唯一新畫面為 §9 W-1 裝置驗證頁,沿用 Admin-Web 既有 Naive UI 版型)
3. 功能摘要(Feature Summary)
3.1 功能概述
- 功能名稱:W101 Admin CLI(
wportadm) - 主要使用者角色:
- 內部後台管理員(
admin/super_admin):以終端機執行審核、治理、稽核查詢等重複性後台工作 - AI Agent(Claude/Cursor,代表已授權的管理員操作):執行「查詢 → 判讀 → 逐筆處置 → 留下理由」的審核流水線
- CI/排程(受限):僅讀取類與明確授權的維運任務(如
scheduler status)
- 內部後台管理員(
- 核心目標:
- 把後台重複性審核工作(公司文件審核、職缺下架、履歷停用、帳號狀態)從「一筆一筆點 GUI」變成可腳本化、可稽核、可被 Agent 驅動的命令
- 把金鑰治理(企業 API Key、合作夥伴 Key)的事故處理時間從分鐘級壓到秒級——
rotate/revoke一行指令 - 建立 wport 的 admin 線通道治理基礎:可撤銷的短期憑證、通道來源標記、逐筆稽核,補上目前 GUI session(7 天 JWT)做不到的三件事
- 沿用 pm_41/pm_47 的終端契約(exit code、
--output json、--fields/--minimal),內部工程師與 Agent 零學習成本
實際驅動力(Eric 2026/09/09)
以下三項是本案的真實需求來源,不是推導出來的策略理由:
| # | 驅動力 | 現況 | CLI 帶來的改變 |
|---|---|---|---|
| D-1 | IDE 已成為同仁的工作介面,未來在 IDE 內與各服務互動是必然趨勢 | 後台只有 GUI,任何取數都得離開編輯器、切到瀏覽器、手動點選與複製 | 資料與操作直接進到工作流所在的地方,不必離開 IDE |
| D-2 | 每月股東報告需要抓後台控制台的資料 | 資料管線已在取 AMS 主控台,但認證方式是從 Chrome 撈尚未過期的 token,撈不到就請 Eric 手動登入一次,再不然就在 .env 放明碼的 AMS_EMAIL / AMS_PASSWORD(見 §2 台帳) | wportadm dashboard stats --output json 直接取數;認證改用短期、可撤銷、可稽核的 device flow token,不再需要撈瀏覽器 token,也不必把 admin 帳密落地 |
| D-3 | 行銷規劃的 UTM 管理 | marketing/utm 端點已存在,但只能透過 GUI 逐筆操作 | wportadm marketing utm list/create/update/delete,可腳本化、可批次產生 |
D-2 同時是資安改善:目前之所以「撈瀏覽器 token」這招可行,正是因為 admin JWT 效期長達 7 天(§2 台帳)。這條驅動力與 §19 問題 1 是同一件事的兩面 —— 一個講「不好用」,一個講「不安全」。
北極星:「同仁已經在 IDE 裡工作,就讓後台的事也能在 IDE 裡做完」
這是本案的核心命題,其餘情境都是它的具體形態。
公司同仁——不只 RD,行銷與客服組也是——一天多數時間待在 IDE。目前只要碰到後台的事, 就得中斷手上的工作:切到瀏覽器、登入、逐層點選、手動複製貼回來。這個切換成本本身不大, 但它發生得很頻繁,而且擋住了自動化——凡是只能用滑鼠完成的事,就無法被腳本或 Agent 接手。
CLI 要達成的不是「多一種操作方式」,而是讓後台從「要切出去做的系統」變成「工作流裡的一個命令」。 衡量標準很簡單:同仁完成一件後台工作,需不需要離開編輯器。
這個命題對 CLI 的四個硬需求(皆納 v1):
- 機器可讀輸出:
--output json/--fields/--minimal,讓結果能直接進下一段腳本或交給 Agent - 可分頁批次讀取:不必為了取一份清單而逐頁點選
- 寫入可稽核:
--reason必填並寫入admin_operation_logs,通道可辨識(channel=admin_cli) - 認證不打斷工作流:一次
login,之後 token 自動 refresh,不需反覆登入或手動搬憑證
⚠️ 第 3 點目前做不到:AMS 稽核表雖有
metadata可擴充,但全src/無X-Source收錄,也沒有 channel 欄位語意。這是 v1 的 Blocking 後端工作(§17 sync-fix #3)。
三個代表情境(對應 §3.1 的 D-1~D-3)
| # | 情境 | 現況痛點 | CLI 後 |
|---|---|---|---|
| S-1 | 股東月報取數(D-2) | 資料管線已在取 AMS 主控台,但得靠 chrome-token.mjs 從 Chrome 撈 token、或在 .env 放明碼帳密(§2 台帳) | wportadm dashboard stats --output json 直接進管線;認證改用短期可撤銷 token |
| S-2 | 行銷 UTM 管理(D-3) | marketing/utm 端點已存在,但只能 GUI 逐筆建立 | marketing utm list/create 可腳本化、可批次產生並回收結果 |
| S-3 | 審核流水線(延伸情境) | 30 家待審公司只能逐家點開判讀 | 把審核標準交給 Agent:companies search --status pending → 逐家 view → approve/reject --reason "...",全程留稽核 |
S-3 是延伸而非主軸(v1.1.0 調整):初版曾以審核自動化為北極星,但那是從「哪些工作可自動化」推導出來的, 不是同仁實際提出的需求。真正的需求是 D-1——人已經在 IDE 裡了。審核自動化是這個前提成立後自然長出來的用法之一, 不是目的本身。
3.2 為什麼現在做(策略理由)
| 驅動力 | 說明 | 不做的代價 |
|---|---|---|
| IDE 已是全公司的工作介面,不只 RD | 工程師不用說,行銷與客服組也開始熟悉 IDE。當同仁一天多數時間都在編輯器裡,「跟 IDE 相性好」就從加分項變成基本要求——而 CLI 是與 IDE/Agent 相性最好的介面形態 | 後台永遠是那個要「切出去做」的系統;非工程同仁的自動化需求無處著力 |
| 我們已經有實作 CLI 的經驗 | pm_41(企業線)與 pm_47(個人線)已交付至 @wport/cli 0.9.2,分層、exit code 契約、憑證儲存、輸出淨化、OAuth device flow 全部驗證過一輪。本案是沿用既有底盤,不是從零開始 | 既有投資無法攤提;下次要做時人與經驗都更遠 |
| 審核是可自動化的重複勞動 | 公司文件審核、職缺下架、履歷停用皆為「有明確標準 + 逐筆判定 + 需留理由」的工作,正是 Agent 最擅長的形態;目前 100% 靠人點 GUI | 營運人力隨公司數線性增長;審核延遲直接影響企業客戶開通速度 |
| 金鑰事故的 MTTR | 企業 API Key 外洩時,現況要登入後台、找到該公司、進金鑰頁、點輪替;CLI 是 wportadm keys rotate <enc_id> --reason "leak" | 事故處理時間受限於「有沒有人能開瀏覽器」 |
| 通道治理的最後一塊 | pm_41/pm_47/pm_48 已把企業線、個人線、MCP 的通道治理(BR-039 來源審計、BR-040 跨通道 deny list)打好底盤,admin 線是唯一還沒有通道概念的 | 權限最高的通道反而最不可治理 |
| 現況 GUI session 才是風險本體 | 實查:登入發 7 天全權限 JWT、無 refresh、無法單獨撤銷單一 session(admin.service.ts:139)。筆電遺失只能改密碼 | 這個風險不會因為「不做 CLI」而消失;做 CLI 正好逼我們把可撤銷 session 建起來 |
| 稽核基礎設施已備妥 | admin_operation_logs 的 module/action/table 字典與 changedFields/remarks 欄位都已存在,只差通道語意 | 已投入的稽核建設無法涵蓋新通道 |
| 時機成本 | AMS 端點已收斂到 /v2 新架構(company/v2、job/v2、resume/v2、account/v2),legacy 端點正在退場 | 等 legacy 完全移除後再做,CLI 要重寫一次對映 |
關於「權限最高所以不該做 CLI」的反向論證:這個直覺是對的方向、錯的結論。真正的風險不是「多一個通道」,而是「通道無法治理」。現況 GUI 通道的憑證是 7 天全權限、不可撤銷、不分來源;本 PRD 交付的 admin CLI 通道反而是第一個具備短 TTL、可撤銷、可限流、可逐筆稽核的 admin 通道。若因噎廢食不做,GUI 的既有風險一樣留在原地(§12 ADR-001 Consequences)。
3.3 SaaS Benchmark(高權限管理型 CLI 的認證慣例,RD 可自行驗證)
| 平台 | CLI 登入方式 | 長期 API key? | Session 可撤銷? |
|---|---|---|---|
GitHub CLI(gh auth login) | OAuth device flow(預設) | 可選 PAT | 是(Settings → Applications) |
Auth0 CLI(auth0 login) | OAuth device flow | 可選 M2M client | 是 |
Stripe CLI(stripe login) | 瀏覽器授權,發限時 restricted key | 是(但 restricted) | 是(Dashboard) |
| Vercel CLI | 瀏覽器授權 / email OTP | 是 | 是 |
| Cloudflare Wrangler | OAuth 瀏覽器授權 | 可選 API token | 是 |
| Shopify CLI | 瀏覽器 OAuth | Partner token | 是 |
| Heroku CLI | 瀏覽器授權 | 是 | 是 |
1Password CLI(op) | 桌面 app 生物辨識,短期 session | 否(刻意) | 是 |
| AWS CLI | 長期 access key 或 SSO device flow | 是 | 需靠 IAM 治理層 |
結論:管理型高權限 CLI 已收斂到 device-code / 瀏覽器 OAuth + 短 TTL + 可逐一撤銷 session。唯一大量使用長期 key 的 AWS,是因為其上有 IAM 這一整套獨立治理層在兜底——wport 沒有等價物,因此不核發長期 admin key(ADR-001、BR-API-CLI-018)。此結論與 pm_47 個人線「不發個人 API 金鑰」的裁決同源。
3.4 範圍界定(三層:✅ v1 會做 / 🕐 v2 以後 / ☢️ 絕對不做)
✅ v1(必含)
A. 認證與 session 治理
wportadm login:admin OAuth device flow(開瀏覽器、MFA 在瀏覽器完成)wportadm logout/whoamiwportadm sessions list/sessions revoke [--all-others]- token 自動 refresh(過期前緩衝換新),refresh token rotation
B. 審核與治理命令(對映 AMS 已實作端點)
companies:search/view/update/approve/reject/deletejobs:search/deactivate(無 view,後端缺端點,見 §17 #6)resumes:search/view/disable/enableaccounts:search/view/statusadmins:list/create/update/delete/reset-password/mfa-disable(限SUPER_ADMIN)mfa:status/initialize/verify-setup/disable/rebind/devices list/devices remove(操作者本人)keys(企業 API Key):list/view/create/rename/rotate/quota/revoke/usage/audit-logs/companies search/companies memberspartners(合作夥伴):keys CRUD+regenerate/services bind|unbind|batch/tags CRUD/companies+tagsmarketing:posts/tasks/utm 各自 list/create/update/deletedashboard statsscheduler status/scheduler run <taskName>
C. 通用契約
- exit code 契約(沿用 0/2/3/4/5,新增 6 = 需重新認證,ADR-006)
--output table|json、--fields、--minimal、--api、--timeout、--no-color--confirm(破壞性操作)、--reason(所有寫入必填,ADR-007)wportadm doctor:解析後設定、連通性、schema 指紋、目前身分與權限wportadm config set|get|path|reset
D. 後端配套(AMS,皆為 v1 Blocking)
- admin device flow 授權伺服器(4 個端點,§8.1)
- CLI session 清單與撤銷 API
X-Source收錄 + 稽核channel語意(補 BR-039 落差)- 寫入端點支援
Idempotency-Key
E. 前端配套(Admin-Web)
- W-1 裝置驗證頁
/cli/activate - 帳號設定內「已授權的 CLI session」清單(可撤銷)
🕐 v2 以後(明確不在 v1)
- 前端 mock-only 模組的 CLI 化:
purchase(訂單/方案/點數)、platform(廣告/通知/推薦設定)、system(角色/選單/備份紀錄)、gallery、drive、messages(線上回報/聯繫我們)、history、developer(Loki) → 這些前端頁面存在,但 AMS 無對應 controller,屬 mock-only(§13.2 對照表)。CLI 化的前置是後端先補端點,不在本 PRD 範圍 - 互動式審核 wizard(無 flag 時引導)
- Shell auto-completion
- Standalone binary(免 Node runtime)
- CLI 自身 i18n(v1 全繁中 + 英文 flag 名)
- MCP server 化(admin 線接 pm_48 架構)
☢️ 核彈 Deny List(admin 線永久禁止,對齊 BR-040)
| # | 禁止項 | 理由 |
|---|---|---|
| N-1 | 無差別批次刪除/停用 —— 不提供 --file 批次刪除、不提供萬用字元或「全選」語意的破壞性操作 | 一個手滑等於全站事故;上限也救不了 |
| N-2 | 全站 PII 批次匯出 —— 不提供 accounts export / resumes export 全量下載 | 個資外洩的單點;讀取一律逐筆或分頁 |
| N-3 | 自我提權與自我鎖定 —— CLI 不得修改操作者自身的 role/permission/帳號狀態 | 權限邊界必須由他人授予 |
| N-4 | 繞過 MFA 的憑證取得 —— CLI 不新增任何免 MFA 取得 admin token 的路徑(含「CI 專用長期 key」) | 開後門等於沒有 |
⚠️ N-4 的誠實範圍:這條約束的是「CLI 不得新增旁路」,不等於「admin 線一定過得了 MFA」。實查顯示 MFA 目前是每帳號可選——新建管理員
mfaEnabled: false,登入時該旗標為偽即直接發完整 token(§2 台帳)。因此 device flow 的瀏覽器授權步驟,遇到未啟用 MFA 的帳號一樣會直接放行。修這件事不在本 PRD 範圍(見 §19 問題 1)。
這四項是上限也救不了的類別,與 pm_41/pm_47 的核彈清單同一判準:不是「限量」能解決的,才進核彈清單;能限量的一律走 BR-API-CLI-021/022 的限量條款(memory:CLI 能力哲學=deny-list 制 + 限量取代禁令)。
3.5 成功條件
| # | 條件 | 量測 |
|---|---|---|
| S1 | 後台工作可全程於終端完成,不需離開編輯器 | S-1/S-2/S-3 三個情境各自在 dev 環境跑通一次,全程零開啟 GUI |
| S2 | 稽核 100% 覆蓋 | 每筆 CLI 寫入皆可在 admin_operation_logs 反查,且能以 channel 區分 CLI/GUI;抽驗 50 筆命中率 100% |
| S3 | 憑證可治理 | CLI session 可於 GUI 與 CLI 逐一撤銷;撤銷後殘存 ≤ 30s(§15) |
| S4 | 金鑰事故 MTTR 下降 | 企業 Key rotate/revoke 從「登入→導覽→操作」壓到單一命令;量測命令執行至生效 < 5s |
| S5 | 不擴權 | 滲透測試:以 admin(非 super_admin)token 呼叫 admins create 必得 403;CLI 無任何路徑可取得超出該 admin GUI 可見範圍的資料 |
| S6 | Agent 可用 | Claude 以 --output json + --fields 完成 S-3 全流程,無需人工介入解析 |
| S7 | 不污染既有通道 | 上線後 GUI 登入成功率、既有 API 錯誤率無統計顯著變化 |
4. 商業規則對齊(Business Rules Alignment)
4.1 本功能使用到的既有業務規則
| 規則 | 內容摘要 | 本 PRD 的關係 |
|---|---|---|
| BR-039 | 通道寫入 source 審計:跨通道(CLI/MCP/SDK/Enterprise API)寫入須記錄來源 | 本 PRD 發現 AMS 尚未實作;v1 Blocking 補齊(§17 #3) |
| BR-040 | 核彈 deny list 與破壞性操作 confirm 語意,跨通道共用;禁止開後門提高額度 | §3.4 核彈清單 N-1~N-4 依此制定;--confirm 語意沿用 |
| BR-API-CLI-001 | CLI 破壞性操作需 --confirm/互動確認(pm_41) | 沿用,並加嚴為「同時需 --reason」(BR-API-CLI-020) |
4.2 新規則提案(沿 BR-API-CLI 系列續編,自 018 起)
編號說明:主序列
BR-04x目前有搶號情形(pm_46提案 BR-039~043、pm_52提案 BR-042,而business-rules.mdcanonical 僅到 BR-041)。為避免再撞號,本 PRD 一律使用BR-API-CLI系列,續pm_47的 017 之後編號。
- BR-API-CLI-018(建議新增):Admin CLI 認證僅限 OAuth device flow;不核發任何長期 admin API key。access token TTL 1 小時;refresh token 30 天 sliding/90 天 absolute,且必須 rotation(每次換發、舊者失效)。偵測到已失效 refresh token 被重用時,撤銷該 token family 全部憑證並記 audit event。
- BR-API-CLI-019(建議新增・資安核心):Admin CLI token 繼承該 admin 既有 role/permission,不得擴權。後端須以
channel=admin_cli標記該通道,並得對此通道單獨套用 deny list 與流量上限;通道標記不得由 client 自行宣告為其他值(以 token 綁定為準,X-Source僅供交叉比對,不作為授權依據)。 - BR-API-CLI-020(建議新增):所有經 CLI 的寫入必須寫入
admin_operation_logs,metadata須含channel;破壞性與審核類操作必須帶--reason(1~200 字,必填),理由寫入remarks。缺--reason於 client 端即擋下(exit 2),不送出請求。 - BR-API-CLI-021(建議新增):Admin CLI 破壞性操作一律單筆 +
--confirm;不提供批次刪除、不提供萬用字元選取(對齊核彈 N-1)。非破壞性的批次讀取不受此限。 - BR-API-CLI-022(建議新增):Admin CLI 不提供全站 PII 匯出;
accounts/resumes讀取一律逐筆或分頁,單次分頁上限對齊 GUI 既有上限(RD 依實作端點定值)。 - BR-API-CLI-023(建議新增):Admin CLI 不得修改操作者自身的 role/permission/帳號狀態(防自我提權與自我鎖定)。後端須在 domain 層擋下(不僅 client 端),對
admins update|delete指向自身時回 403。 - BR-API-CLI-024(建議新增):Admin CLI session 可於後台 GUI 與 CLI 逐一撤銷;撤銷後殘存 ≤ 30 秒。每 admin active CLI session ≤ 5,超額時汰換最久未使用者並通知。
5. 資料實體與欄位(Data Entities & Fields)
本節描述需要追蹤什麼資料、為什麼,不規定 JSON 欄位名、雜湊演算法或 DB schema(gen-prd Rule 31)——那些屬 OpenAPI Spec,由 RD 決定。
5.1 實體列表
| # | 實體 | 新增/既有 | 目的 |
|---|---|---|---|
| E-1 | Admin CLI 授權請求(device authorization request) | 新增 | 承載 device flow 的待授權狀態,短生命週期 |
| E-2 | Admin CLI Session | 新增 | 一台裝置一份;治理單位=可列出、可撤銷的最小粒度 |
| E-3 | Admin Refresh Token Family | 新增 | 支撐 rotation 與「重用即全家撤銷」的偵測 |
| E-4 | 後台操作稽核記錄 | 既有,需擴充 | 既有表已具備操作者/目標/變更欄位/理由;缺通道語意 |
| E-5 | CLI 本機憑證檔 | 新增(client 端) | 存放 token;不上傳、不進 git |
5.2 實體資料需求說明
E-1 Admin CLI 授權請求 需追蹤:待授權的裝置識別、給人看的短代碼、發起時間與到期時間、輪詢節流間隔、目前狀態(待授權/已授權/已拒絕/已過期)、以及授權完成後綁定到哪位 admin。 生命週期:短(建議 ≤ 10 分鐘,RD 定值);過期即不可用,且不可重複兌換。 安全需求:短代碼須為人類可念但不可預測;輪詢須有節流,避免暴力兌換。
E-2 Admin CLI Session 需追蹤:屬於哪位 admin、建立時間、最後使用時間、來源裝置描述(作業系統/CLI 版本/建立時 IP)、目前是否有效、撤銷時間與撤銷者。 為什麼要有:這是 BR-API-CLI-024 的治理單位,也是補上現況「7 天 JWT 無法單獨撤銷」缺口的核心(§2 證據台帳)。 不追蹤:不記錄 CLI 執行過的命令內容(那是稽核表 E-4 的責任,避免雙寫與資料重複)。
E-3 Admin Refresh Token Family 需追蹤:同一次登入衍生的 refresh token 鏈、目前有效的那一枚、是否偵測到已失效者被重用。 為什麼要有:rotation 若無 family 概念,token 被竊後無法判斷是「合法 client 換新」還是「攻擊者重用舊的」。
E-4 後台操作稽核記錄(既有,需擴充)
既有已具備:操作者、模組、動作類型、目標表、目標 ID 與名稱、變更欄位明細、metadata、備註、IP、UA、requestId(admin-operation-log.interface.ts:42,51,53)。
本 PRD 需要新增的語意:這筆操作來自哪個通道(GUI/admin CLI/未來 MCP)。實作上可放 metadata 或獨立欄位,由 RD 決定;PRD 只要求「可用單一條件過濾出所有 CLI 操作」。
另需確保:remarks 對 CLI 破壞性操作為必填(BR-API-CLI-020)。
E-5 CLI 本機憑證檔
需追蹤:access token 與其到期時刻、refresh token、目前登入者的顯示名稱與 email、session 建立時間、目前連線的後端環境(dev/staging/prod)。
安全需求:檔案權限收斂至 owner-only、atomic write(沿用 wport-cli@c49a610 credentials-store.ts:160,163 的既有模式);不同環境分檔存放,避免誤用 dev 憑證打 prod(沿用同檔 :63-64 的 channel 分檔慣例)。
硬性禁止:任何流程不得 echo token 到終端、不得寫入 shell history、不得進 git。
6. CLI 命令面與資料需求(Command Surface & Data Needs)
6.1 命令樹(v1 目標)
wportadm
├─ login / logout / whoami # OAuth device flow(§6.3)
├─ sessions list | revoke [--all-others] # CLI session 治理
├─ doctor # 診斷:設定 / 連通性 / 身分 / 權限 / schema 指紋
├─ config set | get | path | reset
│
├─ companies # → company/v2
│ ├─ search [--status --keyword --page --page-size]
│ ├─ view <enc_id>
│ ├─ update <enc_id> --file patch.json --reason "..."
│ ├─ approve <enc_id> --reason "..." # document-approval
│ ├─ reject <enc_id> --reason "..." # document-approval
│ └─ delete <enc_id> --confirm --reason "..."
│
├─ jobs # → job/v2
│ ├─ search [--status --keyword --page --page-size]
│ └─ deactivate <enc_id> --confirm --reason "..."
│ # ⚠ 無 view:後端缺端點(§17 #6)
├─ resumes # → resume/v2
│ ├─ search / view <enc_id>
│ ├─ disable <enc_id> --confirm --reason "..."
│ └─ enable <enc_id> --reason "..."
│
├─ accounts # → account/v2(前台求職者會員)
│ ├─ search / view <enc_id>
│ └─ status <enc_id> --set <state> --confirm --reason "..."
│
├─ admins # → admin/users(限 SUPER_ADMIN)
│ ├─ list / create --file a.json / update <enc_id> --file p.json
│ ├─ delete <enc_id> --confirm --reason "..."
│ ├─ reset-password <enc_id> --reason "..."
│ └─ mfa-disable <enc_id> --confirm --reason "..."
│
├─ mfa # → auth/mfa(操作者本人)
│ ├─ status / initialize / verify-setup / disable / rebind
│ └─ devices list | remove <device_id>
│
├─ keys # → admin/enterprise-api-keys
│ ├─ list / view <enc_id> / create --file k.json
│ ├─ rename <enc_id> --name "..."
│ ├─ rotate <enc_id> --confirm --reason "..." [--reveal]
│ ├─ quota <enc_id> --set <n> --reason "..."
│ ├─ revoke <enc_id> --confirm --reason "..."
│ ├─ usage <enc_id> [--period YYYY-MM]
│ ├─ audit-logs <enc_id> [--page --page-size]
│ └─ companies search | members <enc_company_id>
│
├─ partners # → admin/partner-keys
│ ├─ keys list | view | create | update | regenerate | delete
│ ├─ services list | bind | unbind | batch | update
│ ├─ tags list | search | view | create | update | delete
│ └─ companies list | tags get | tags set
│
├─ marketing # → marketing/*
│ ├─ posts list | create | update | delete
│ ├─ tasks list | create | update | delete
│ └─ utm list | create | update | delete
│
├─ dashboard stats [--from --to] # → siteadmin/v2/dashboard/stats
└─ scheduler status | run <taskName> --confirm --reason "..."
6.2 全域契約
| 契約 | 值 | 說明 |
|---|---|---|
| Exit code | 0/2/3/4/5/6 | 沿用 pm_41;新增 6 = 需重新認證(ADR-006) |
| 設定優先序 | flag > env > config 檔 > 預設 | 沿用 wport-cli@c49a610 global-opts.ts:38 |
| API base | 不落地 config,僅 --api / WPORTADM_API_BASE | 沿用參考 CLI 的 SSRF 收斂決策(global-opts.ts:64) |
| 輸出 | TTY → table;pipe → json | 沿用 |
| 欄位投影 | --fields <list> / --minimal | 僅 JSON 模式;降 Agent token |
| 終端淨化 | 所有 table/訊息輸出過 ANSI 淨化 | 沿用 output.ts:43-44,100 |
| 來源標記 | 每請求帶 X-Source: admin_cli | BR-039;後端須先支援收錄 |
| 冪等 | 寫入端點帶 Idempotency-Key | 後端須先支援(§17 #4) |
| 破壞性 | --confirm + --reason 皆必填 | BR-API-CLI-020/021 |
| 憑證 | owner-only、atomic write、依環境分檔 | 沿用 credentials-store.ts:160,163 |
6.3 認證命令
# 首次登入:開瀏覽器完成授權(含 MFA),CLI 端輪詢取 token
wportadm login
# → 顯示短代碼與驗證網址;SSH 環境加 --no-browser 只印網址
wportadm whoami # 顯示身分、role、權限摘要、session 建立時間(離線)
wportadm sessions list # 列出此帳號所有 CLI session
wportadm sessions revoke <id> # 撤銷指定 session
wportadm sessions revoke --all-others # 保留當前,踢掉其餘(筆電遺失情境)
wportadm logout # 撤銷當前 session(server 端)+ 清除本機憑證
設計要點:MFA 一律在瀏覽器完成,CLI 端不收 TOTP 碼(核彈 N-4)。token 於過期前自動 refresh,使用者無感;refresh 失敗(撤銷/過期)時以 exit 6 明示需重跑 login,讓腳本能區分「重登即可」與「真的無權限(exit 3)」。
6.4 審核類命令(情境 S-3)
# 拉待審清單(Agent 讀 JSON)
wportadm companies search --status pending --output json --minimal
# 逐家取詳情
wportadm companies view <enc_id> --output json
# 判定並留下理由(--reason 必填,寫入稽核 remarks)
wportadm companies approve <enc_id> --reason "統編與登記文件相符,負責人一致"
wportadm companies reject <enc_id> --reason "登記文件過期(2024/12 到期)"
# 職缺下架(破壞性 → 需 --confirm)
wportadm jobs deactivate <enc_id> --confirm --reason "違反 BR-009 職缺內容規範"
# 履歷停用 / 復用
wportadm resumes disable <enc_id> --confirm --reason "檢舉屬實:冒用他人經歷"
wportadm resumes enable <enc_id> --reason "申訴成立,恢復"
6.5 金鑰治理命令(S4 主線)
wportadm keys list --output json
wportadm keys rotate <enc_id> --confirm --reason "疑似外洩,客戶回報" --reveal
wportadm keys revoke <enc_id> --confirm --reason "合約終止"
wportadm keys usage <enc_id> --period 2026-08
wportadm keys audit-logs <enc_id> --page 1
--reveal 僅在 rotate/create 當下輸出一次完整新金鑰;其餘任何輸出一律遮罩至末四碼(沿用 wport-cli@c49a610 credentials-store.ts:73 的 maskKey 慣例)。
7. 使用者動作與後端需求(User Actions & Backend Needs)
| # | 動作 | 後端需求 | 新建? |
|---|---|---|---|
| ACT-1 | login 取得授權 | device flow 四端點(§8.1) | 新建 |
| ACT-2 | 授權頁完成 MFA | 沿用既有 admin 登入 + MFA 流程(admin.service.ts:167) | 既有 |
| ACT-3 | token 自動 refresh | refresh 端點 + rotation + family 重用偵測 | 新建 |
| ACT-4 | sessions list/revoke | CLI session 查詢與撤銷 API | 新建 |
| ACT-5 | 所有讀取類命令 | 沿用既有 /v2 端點,無需改動 | 既有 |
| ACT-6 | 所有寫入類命令 | 端點沿用既有;須新增:X-Source 收錄、稽核 channel 語意、Idempotency-Key 支援 | 既有端點 + 擴充 |
| ACT-7 | --reason 寫入稽核 | 既有 remarks 欄位承接;須改為 CLI 通道必填 | 既有 + 擴充 |
| ACT-8 | 自我提權防護 | domain 層擋下指向自身的 role/status 變更 | 新建 |
| ACT-9 | 通道限流 | 對 channel=admin_cli 可單獨設限(沿用既有 Throttle 基礎設施) | 既有 + 擴充 |
ACT-6 的三件事必須分開看:①端點路徑與 DTO 不改(CLI 打的是既有
/v2端點)②X-Source收錄與稽核channel是必做(BR-039 落差,v1 Blocking)③Idempotency-Key是必做但可分批(僅破壞性與建立類端點需要,讀取類不需要)。
8. API Hints(提示用,非最終 API 設計)
路徑一律含 global prefix
/ams-api(W101-AMS@bf0fb57 src/main.ts:40)。以下為需求提示,最終契約由 RD 於 OpenAPI Spec 定案。
8.1 Admin Device Flow(新建)
| 用途 | 方法 | 路徑提示 | 認證 |
|---|---|---|---|
| 取得裝置代碼 | POST | /ams-api/admin/oauth/device/code | 無(僅需 client 識別) |
| 輪詢換 token | POST | /ams-api/admin/oauth/device/token | 無(憑 device_code) |
| Refresh token | POST | /ams-api/admin/oauth/token/refresh | 憑 refresh token |
| 撤銷 | POST | /ams-api/admin/oauth/revoke | Bearer |
RFC 8628 為對標基準(同 pm_47 個人線)。授權頁本身走 Admin-Web(§9),不在此列。
8.2 CLI Session 治理(新建)
| 用途 | 方法 | 路徑提示 | 權限 |
|---|---|---|---|
| 列出自己的 CLI session | GET | /ams-api/admin/cli-sessions | 本人 |
| 撤銷單一 session | DELETE | /ams-api/admin/cli-sessions/{id} | 本人 |
| 撤銷其餘全部 | POST | /ams-api/admin/cli-sessions/revoke-others | 本人 |
8.3 既有端點(CLI 直接使用,路徑實查自 controller)
| 命令群 | 既有 controller 路徑 | 來源 |
|---|---|---|
companies | company/v2 | W101-AMS@bf0fb57 src/modules/company/controllers/company.controller.ts:29 |
jobs | job/v2 | .../job/controllers/job.controller.ts:14 |
resumes | resume/v2 | .../resume/controllers/resume.controller.ts:14 |
accounts | account/v2 | .../account/controllers/account.controller.ts:16 |
admins | admin/users | .../admin/controller/admin-user.controller.ts:17 |
mfa | auth/mfa | .../auth/controllers/mfa.controller.ts:17 |
keys | admin/enterprise-api-keys | .../enterprise-api-key/controllers/enterprise-api-key-admin.controller.ts:27 |
partners | admin/partner-keys | .../partner-key/controllers/partner-key-admin.controller.ts:35 |
marketing posts | marketing/posts | .../marketing/controllers/posts.controller.ts:26 |
dashboard | siteadmin/v2 | .../siteadmin/controllers/siteadmin-dashboard.controller.ts:12 |
scheduler | scheduler | W101-AMS@bf0fb57 src/scheduling/controllers/scheduler.controller.ts:11 |
8.4 需注意的既有行為
- 全域
ValidationPipe開forbidNonWhitelisted(src/main.ts:79):CLI 送出 body 時多帶一個欄位就會 400。--file讀入的 JSON 必須先過本地 schema 驗證再送出,否則使用者會收到難以理解的 400。 - Swagger 僅非 production 掛載(
src/main.ts:90):CLI 的型別 codegen 只能從 dev/staging 取 spec,不能指望 prod 有/ams-api/api-doc。此點與參考 CLI 的gen:openapi:prod(從 prod swagger 取)做法相反,須改為從 dev 取或凍結 spec 進 repo(ADR-009)。 - 既有節流偏嚴:DEFAULT 為 5 次/60 秒(
throttle.constants.ts:6-7)。CLI 的批次讀取情境(S-3 拉 30 家公司)會撞到ADMIN_LIST20 次/60 秒的上限,須為 CLI 通道評估獨立額度(§17 #8)。
9. Web 觸點(Admin-Web,文字規格)
三個觸點只有 W-1 是 v1。W-2/W-3 列 v2,理由見各節與 §14 —— 兩者在 v1 都有可用的替代路徑, 不擋出貨。這個取捨的完整推導記於 §19 問題 11,避免後人重新煩惱一次。
W-1 裝置驗證頁 /cli/activate(v1,Blocking)
- 進入方式:
wportadm login開啟瀏覽器導向此頁(或使用者手動開啟後貼上代碼) - 未登入時:導向既有 admin 登入頁,完成帳密 + MFA 後回到本頁(沿用既有流程,
W101-Admin-Web@0ee5ee9 CLAUDE.md:15) - 頁面內容:
- 代碼輸入框(若網址已帶代碼則預填並唯讀)
- 顯示請求來源摘要:CLI 版本、作業系統、發起 IP、發起時間
- 明確文案:「授權後,這台裝置將能以你的身分(
{role})操作後台。你可以隨時在『帳號設定 → 已授權的 CLI』撤銷。」 - 兩個按鈕:授權 / 拒絕
- 結果狀態:授權成功、已拒絕、代碼過期、代碼無效 —— 四種各有明確文案與後續指引
- 安全要求:授權按鈕須防 CSRF;代碼比對失敗須節流,不得無限嘗試
W-2 CLI 存取頁(設定 > 帳號設定 新增分頁)(v2)
位置理由:src/views/setting/account/account.vue 已是分頁結構(BasicSetting / SafetySetting),
而 SafetySetting.vue 正是管 MFA 的地方(mfa_enabled / mfa_secret / 綁定流程)。CLI 的 device flow
本來就要過 MFA,兩者相鄰在使用者心智上連得起來。且此頁每個 admin 都要能開 —— 這是它不能放在
會員管理 > 管理員列表 的原因(該區塊全部端點限 SUPER_ADMIN,見 §2 台帳)。
- 安裝:取得套件的方式 + 可複製的指令
- 快速上手:
login→whoami→ 一兩個實際命令 - 我的已授權裝置:裝置描述、建立時間、最後使用時間、建立時 IP、是否為本次 session;可逐一撤銷、一鍵撤銷其餘
- 能力範圍說明:明講「後台部分頁面 CLI 沒有」(ADR-002 的後果)
- 與 CLI
sessions list/revoke讀寫同一組 API(§8.2),確保兩邊一致
為何列 v2:「我的裝置 + 撤銷」CLI 自己就有 sessions list/revoke,不需要畫面;「安裝說明與憑證」
在 v1 的使用者規模(數名內部 admin)以人工交付即可。殘留風險低:人工發出的安裝憑證即使離職後未收回,
持有者也只是能下載一支 CLI —— 沒有有效 admin 帳號它什麼都做不了。套件存取權與後台存取權是不同信任域,
這一點在 v1 與 v2 都成立(連帶說明:安裝憑證不是 admin API key,不牴觸 ADR-001 與核彈 N-4)。
W-3 管理員列表的 CLI 治理視圖(會員管理 > 管理員列表)(v2)
位置理由:該頁本來就是「管理其他 admin」的地方(建帳號、重設密碼、關閉他人 MFA 皆在此), 「撤銷他人 CLI session」屬同一類動作。
- 欄位:該管理員是否有活躍的 CLI session、最後使用時間
- 動作:
super_admin強制撤銷指定管理員的全部 CLI session - 權限:沿用該頁既有的
SUPER_ADMIN限制
為何列 v2:v1 有可用的緊急路徑 —— DELETE /ams-api/admin/users/{enc_id} 為軟刪除
(admin-user.service.ts:141),而 isActive() 要求 deletedAt IS NULL 且 status === ACTIVE
(admin.repository.ts:193-199),此檢查在 jwt.strategy.ts:26 每個請求都會跑。因此刪除管理員後
其 CLI session 於下一個請求即失效。
已知限制:刪帳號是 all-or-nothing,無法「只收 CLI 但保留 GUI」。「筆電遺失但人仍在職」的情境,
v1 只能刪帳號再重建 —— 這正是 W-3 的價值,但發生頻率不足以擋版。
10. Flowcharts
10.1 登入(Device Flow)
flowchart TD
A[wportadm login] --> B[POST device/code]
B --> C[顯示短代碼 + 驗證網址]
C --> D{有瀏覽器?}
D -->|是| E[自動開啟 /cli/activate]
D -->|--no-browser| F[僅印出網址,使用者自行開啟]
E --> G[Admin-Web /cli/activate]
F --> G
G --> H{已登入後台?}
H -->|否| I[導既有 admin 登入頁]
I --> J[帳密驗證]
J --> K{已啟用 MFA?}
K -->|是| L[TOTP 驗證]
K -->|否| M[直接通過]
L --> M
M --> G
H -->|是| N[顯示授權請求摘要]
N --> O{使用者決定}
O -->|授權| P[標記 request 已授權]
O -->|拒絕| Q[標記已拒絕]
B -.CLI 同時開始輪詢.-> R[POST device/token 輪詢]
R --> S{狀態?}
S -->|待授權| T[依 interval 等待後重試]
T --> R
S -->|已授權| U[取得 access + refresh token]
S -->|已拒絕| V[exit 3:使用者拒絕授權]
S -->|已過期| W[exit 3:代碼過期,請重跑 login]
S -->|輪詢過快| T
P --> S
Q --> S
U --> X[寫入本機憑證檔 owner-only]
X --> Y[印出身分與 role,exit 0]
10.2 一般命令執行(含 token 生命週期與稽核)
sequenceDiagram
participant U as 使用者/Agent
participant C as wportadm
participant S as 本機憑證檔
participant A as AMS
participant L as admin_operation_logs
U->>C: wportadm companies approve <id> --reason "..."
C->>C: 驗證參數(--reason 必填、破壞性需 --confirm)
alt 參數不合法
C-->>U: exit 2(不送出請求)
end
C->>S: 讀取 access token
alt token 即將過期
C->>A: POST token/refresh(rotation)
alt refresh 失敗(已撤銷/過期)
A-->>C: 401
C-->>U: exit 6(需重跑 login)
end
A-->>C: 新 access + 新 refresh
C->>S: atomic write 更新憑證
end
C->>A: PATCH company/v2/{id}/document-approval<br/>Bearer + X-Source: admin_cli + Idempotency-Key
A->>A: JwtAuthGuard → RolesPermissionsGuard
alt 權限不足
A-->>C: 403
C-->>U: exit 3(權限不足,非重登可解)
end
A->>A: domain 檢查(含自我提權防護)
A->>L: 寫稽核(channel=admin_cli、remarks=reason)
A-->>C: 200
C->>C: 輸出淨化(ANSI/控制字元)
C-->>U: table 或 json,exit 0
10.3 Edge Case 覆蓋矩陣
| 分類 | 情境 | CLI 行為 |
|---|---|---|
| Network | 後端不可達 | exit 4,訊息含目前 base URL 與 doctor 提示 |
| Network | 逾時 | 依 --timeout 中止,exit 4 |
| Network | 輪詢期間斷網 | 依 interval 重試至代碼過期;過期後 exit 3 |
| Performance | 撞到節流(429) | 讀取 Retry-After 並印出建議等待秒數,exit 3 |
| Data/Input | --file JSON 格式錯 | 本地驗證即擋,exit 2,指出出錯欄位 |
| Data/Input | --file 多帶欄位 | 本地擋下,避免後端 forbidNonWhitelisted 回難懂的 400(§8.4) |
| Data/Input | --reason 缺漏或超長 | exit 2,不送出請求 |
| Data/Input | enc_id 不存在 | exit 3,訊息明示查無此資源 |
| Data/Input | 輸出含終端跳脫序列 | 一律淨化後輸出(output.ts:43-44) |
| User Interrupt | 輪詢中 Ctrl+C | 不留半吊子憑證;已建立的授權請求自然過期 |
| User Interrupt | 寫入中斷線 | 憑 Idempotency-Key 可安全重試 |
| Auth/Lifecycle | access token 過期 | 自動 refresh,使用者無感 |
| Auth/Lifecycle | refresh token 過期/已撤銷 | exit 6,明示重跑 login |
| Auth/Lifecycle | session 被他人撤銷 | 下一請求 401 → exit 6(殘存 ≤ 30s) |
| Auth/Lifecycle | 帳號被停用 | exit 3;不得暗示帳號狀態細節給未授權者 |
| Auth/Lifecycle | refresh token 重用偵測 | 該 family 全撤,記 audit event,exit 6 |
| Auth/Lifecycle | 帳號中途被停用 | 下一請求即 401(jwt.strategy.ts:26 每請求查狀態)→ exit 6 |
| Auth/Lifecycle | role 中途被降級 | 不會即時生效——role 快取於 token,最長需等 1 小時 TTL 到期換新(§15)。需立即生效者由管理者撤銷該 session(≤30s)。降級生效後下一請求 403 → exit 3(非 6,避免誤導重登) |
| Logical | 對自己執行 admins delete | 本地擋 + 後端 403(BR-API-CLI-023) |
| Logical | 已審核的公司再次 approve | 後端回既有狀態衝突錯誤,exit 3 |
| Logical | 已撤銷的金鑰再次 revoke | 冪等視為成功或明確狀態衝突,RD 定調(§19 問題 3) |
| Logical | dev 憑證誤打 prod | 憑證依環境分檔,天然隔離;doctor 印出目前環境 |
11. 實作備註(Implementation Notes)
11.1 CLI 必做
- 沿用參考 repo 的 monorepo 分層(
core傳輸與型別/cli命令層),但不共用套件(ADR-003) - exit code 契約集中一處定義,任何命令不得自行
process.exit繞過 - 所有終端輸出走淨化 helper(
output.ts:43-44同款) - 憑證 atomic write + owner-only + 依環境分檔
--file輸入一律先過本地 schema 驗證再送出(§8.4)- token refresh 需處理併發:同一時刻多個命令不得各自 refresh 造成 rotation 互斥失敗
11.2 後端必做(AMS,v1 Blocking)
- device flow 授權伺服器(§8.1)
- CLI session 治理 API(§8.2)
X-Source收錄 + 稽核channel語意(BR-039 落差)- 寫入端點支援
Idempotency-Key - 自我提權/自我鎖定的 domain 層防護(BR-API-CLI-023)
- CLI 通道獨立節流額度評估(§8.4)
11.3 禁止事項
- 不得為 CLI 開任何免 MFA 的憑證取得路徑(核彈 N-4)
- 不得讓 client 自行宣告 channel 作為授權依據(BR-API-CLI-019)
- 不得把 admin token 寫入 log、shell history 或錯誤訊息
- 不得為了「方便」而放寬
forbidNonWhitelisted
11.4 相依性
| 相依 | 影響 | 阻塞? |
|---|---|---|
| AMS device flow AS | login 無法運作 | 是 |
| AMS session API | sessions 命令無法運作 | 是 |
AMS X-Source 收錄 | S2 稽核成功條件無法達成 | 是 |
| Admin-Web W-1 頁 | device flow 無授權介面 | 是 |
AMS Idempotency-Key | 斷線重試不安全 | 是(可分批) |
| 私有 registry 設定 | 無法散布 | 是(非技術) |
12. ADR(架構決策紀錄)
ADR-001:Admin CLI 採 OAuth Device Flow,不沿用現有登入、不發長期 key
- Status:Accepted
- Context:AMS 現況登入發 7 天全權限 JWT、無 refresh、無法單獨撤銷單一 session(
admin.service.ts:139)。CLI 若沿用,等於把最高權限憑證長期落地磁碟且不可治理。 - Decision:新建 admin device flow 授權伺服器;access token TTL 1h、refresh 30d sliding/90d absolute 且 rotation;MFA 一律在瀏覽器完成。不核發長期 admin API key。
- Consequences:後端工作量最大(新增 AS + session 治理)。換得可撤銷、可限流、可稽核的通道。既有 GUI 的 7 天 JWT 風險不在本 PRD 範圍內修復,列為 §19 問題 1。
- Decision maker:Eric|Date:2026-08-25
ADR-002:v1 範圍=AMS 已實作端點全包,明確放棄 GUI parity
- Status:Accepted
- Context:Admin-Web 有 18 個路由模組、34 個 api 檔,但 AMS 僅 12 組真端點;
purchase/platform/system等為 mock-only(§13.2)。pm_41/pm_47 的「GUI 能做什麼 CLI 就做什麼」在此無法成立。 - Decision:v1 覆蓋所有 AMS 已實作端點;mock-only 模組明列為 out of scope,其 CLI 化的前置是後端先補端點。
- Consequences:CLI 上線時「後台有些頁面 CLI 沒有」是預期行為,須在 README 與
doctor明示,避免被當成缺陷回報。 - Decision maker:Eric|Date:2026-08-25
ADR-003:獨立套件 + 私有 registry,不併入 @wport/cli
- Status:Accepted
- Context:
@wport/cli是公開 npm 套件,面向外部企業與求職者。admin CLI 握有後台最高權限,其命令面、端點結構、權限模型若公開發布等同送出攻擊地圖。 - Decision:另發
@wport/admin-cli(執行檔wportadm)至私有 registry(GitHub Packages 或 npm private scope),僅內部 admin 可安裝。 - Consequences:需維護第二套 release 流程;兩套 CLI 的共用邏輯(輸出淨化、憑證儲存、exit code)會有一定程度重複。刻意接受此重複——跨 registry 共用套件會把私有實作細節洩進公開相依鏈。
- Decision maker:Eric|Date:2026-08-25
ADR-004:通道不得擴權,token 繼承既有 role/permission
- Status:Accepted
- Context:admin CLI 若自帶權限模型,會出現「GUI 看不到但 CLI 做得到」的不一致,且無法沿用既有
RolesPermissionsGuard(roles-permissions.guard.ts:30-47)。 - Decision:CLI token 完全繼承該 admin 的 role 與 DB 權限;後端額外標記
channel=admin_cli,僅用於限縮(deny list、限流),不得用於放寬。 - Consequences:CLI 能力面自動隨既有權限模型演進,零額外維護。代價是無法為 CLI 設計「唯讀專用」的窄權限 token——列為 v2(§14)。
- Decision maker:Eric|Date:2026-08-25
ADR-005:channel 以 token 綁定為準,X-Source 僅供交叉比對
- Status:Accepted
- Context:BR-039 要求記錄寫入來源。若單靠 client 送的
X-Sourceheader,任何持有 token 者都能偽造來源,稽核即失效。 - Decision:通道身分由 token 本身承載(device flow 發出的 token 天然帶
admin_cli屬性);X-Sourceheader 仍照送,但僅作為交叉比對與除錯訊號,不作為授權或稽核的權威來源。 - Consequences:稽核可信度提高。若兩者不一致,應記為異常事件供監控。
- Decision maker:Eric|Date:2026-08-25
ADR-006:新增 exit code 6 = 需重新認證
- Status:Accepted
- Context:參考 CLI 的契約中,401(token 過期)與 403(權限不足)都落在 exit 3。對自動化腳本而言,前者「重登即可」、後者「重登也沒用」,混為一談會導致無窮重試迴圈。
- Decision:新增 exit 6 專指「憑證失效,需重跑
login」;403 維持 exit 3。 - Consequences:與
@wport/cli的 exit code 契約產生一處差異,須在兩份 README 明確對照。判定為值得的差異——admin CLI 的自動化情境比公開 CLI 更依賴這個區分。 - Decision maker:Eric|Date:2026-08-25
ADR-007:所有寫入必填 --reason,破壞性另需 --confirm
- Status:Accepted
- Context:稽核表已有
remarks欄位(admin-operation-log.interface.ts:42所屬 interface),但 GUI 未強制填寫,導致事後追查常常只看到「誰在何時改了什麼」,缺少「為什麼」。 - Decision:CLI 通道一律強制
--reason(1~200 字),寫入remarks;破壞性操作另需--confirm。缺漏於 client 端即擋(exit 2),不送出請求。 - Consequences:CLI 的稽核品質高於 GUI。這會凸顯 GUI 端未強制填理由的落差,建議後續對齊,列 §19 問題 2。
- Decision maker:Eric|Date:2026-08-25
ADR-008:破壞性操作不提供批次
- Status:Accepted
- Context:pm_47 個人線採「限量取代禁令」(批次投遞 ≤20/次)。但 admin 線的破壞性操作(刪公司、刪管理員)沒有等價的「限量後仍可接受」的失誤成本。
- Decision:破壞性操作一律單筆 +
--confirm;不提供--file批次、不提供萬用字元。非破壞性讀取不受限。 - Consequences:S-3 的 30 家審核需逐筆呼叫(Agent 自然會迴圈,非阻礙)。與 pm_47 的哲學差異須在文件說明:限量取代禁令適用於可回復的操作,不適用於不可回復的。
- Decision maker:Eric|Date:2026-08-25
ADR-009:型別 codegen 來源改為 dev 環境或凍結 spec
- Status:Accepted
- Context:參考 CLI 的預設 codegen 從 prod swagger 取(
gen:openapi:prod)。但 AMS 的 Swagger 僅在非 production 掛載(src/main.ts:90),prod 無/ams-api/api-doc。 - Decision:codegen 來源改為 dev/staging 環境;並將取得的 spec 凍結進 repo 作為 fixture,使 CI 可離線重現且能 diff 出後端 DTO 變動。
- Consequences:與參考 CLI 的做法相反,須在 README 明確說明原因,避免後人「照抄 prod 版」而困惑。
- Decision maker:Eric|Date:2026-08-25
ADR-010:不做 MCP,v1 只做 CLI
- Status:Accepted
- Context:pm_48 已建立 MCP connector 架構,理論上 admin 線可比照。
- Decision:v1 只交付 CLI。admin 線 MCP 化列 v2,且須先累積 CLI 通道的實際稽核與濫用資料。
- Consequences:Agent 需透過 CLI 而非原生 MCP tool 操作,體驗略遜但完全可用。換得「先在可控通道觀察一段時間」的安全緩衝。
- Decision maker:Eric|Date:2026-08-25
ADR-011:不做 mock 模式
- Status:Accepted
- Context:Admin-Web 本地開發為純 Mock 模式(
CLAUDE.md:47),CLI 是否比照? - Decision:不做。CLI 一律打真實後端,環境以
--api或環境變數切換,並依環境分檔存憑證。 - Consequences:CLI 的本地開發需要一個可用的 dev 後端。避免了「mock 與真實行為不一致」這類最難查的缺陷。
- Decision maker:Eric|Date:2026-08-25
ADR-012:jobs 無 view 子命令,不為此擋版
- Status:Accepted
- Context:實查
job/v2僅有search與deactivate,無單筆詳情端點(job.controller.ts:14,23,57)。 - Decision:v1 的
jobs命令群不提供view;以search的結果欄位替代。後端補端點後再加,不因此擋 v1 出貨。 - Consequences:
jobs deactivate前無法在 CLI 內取得完整職缺內容供判讀,Agent 需依賴 search 回傳欄位。列入 §17 sync-fix #6 追蹤。 - Decision maker:Eric|Date:2026-08-25
ADR-013:CLI session 上限 5,超額汰換最久未使用
- Status:Accepted
- Context:無上限則憑證累積無法治理;上限過低則多機工作(筆電/桌機/CI)受阻。
- Decision:每 admin active CLI session ≤ 5,超額時汰換最久未使用者並於下次登入告知。
- Consequences:正常使用不受影響。異常累積會自然浮現。數值屬營運參數,調整不需改版 PRD。
- Decision maker:Eric|Date:2026-08-25
ADR-014:不追蹤 CLI 執行過的命令內容
- Status:Accepted
- Context:session 實體是否要記錄「這個 session 跑過哪些命令」?
- Decision:不記錄。命令的後果已由既有
admin_operation_logs逐筆承接(含變更欄位與理由);session 只治理憑證生命週期。 - Consequences:避免雙寫與資料重複。代價是「純讀取類命令」不留稽核痕跡——判定可接受,與 GUI 瀏覽行為不留稽核一致。
- Decision maker:Eric|Date:2026-08-25
13. 四大一致性表格
13.1 欄位對照表
| 概念 | CLI 呈現 | 後端來源 | SoT | 版本 |
|---|---|---|---|---|
| 資源識別碼 | <enc_id> 位置參數 | 各 /v2 端點的 enc_id path param | 後端 | v1 |
| 操作理由 | --reason | 稽核 remarks | PRD(BR-API-CLI-020) | v1 |
| 通道 | X-Source: admin_cli(僅比對) | token 綁定之 channel | 後端(ADR-005) | v1 |
| 冪等鍵 | 自動產生,不外露 | Idempotency-Key header | 後端 | v1 |
| 操作者身分 | whoami 輸出 | JWT payload { id, email, role } | 後端 | v1 |
| 分頁 | --page / --page-size | 各端點既有分頁參數 | 後端 | v1 |
| 環境 | --api / 環境變數 | N/A(client 端) | PRD | v1 |
13.2 GUI 模組 vs 後端端點對照(本 PRD 的範圍界定依據)
| Admin-Web 模組 | 前端 api 檔 | AMS controller | v1 CLI |
|---|---|---|---|
| 會員-公司 | src/api/members/company.ts | company/v2 | ✅ companies |
| 會員-求職者 | src/api/members/job-seekers.ts | account/v2 | ✅ accounts |
| 會員-管理員 | src/api/members/admins.ts | admin/users | ✅ admins |
| 職缺 | src/api/jobs/all.ts | job/v2 | ✅ jobs(無 view) |
| 履歷 | src/api/resumes/all.ts | resume/v2 | ✅ resumes |
| 企業 API Key | src/api/enterprise-api-keys/index.ts | admin/enterprise-api-keys | ✅ keys |
| 合作夥伴 | src/api/partner/*.ts | admin/partner-keys | ✅ partners |
| 行銷規劃 | src/api/marketing/*.ts | marketing/* | ✅ marketing |
| 儀表板 | src/api/dashboard/console.ts | siteadmin/v2(部分) | ✅ dashboard(僅 stats) |
| 訂單管理 | src/api/purchase/*.ts | 無 | ❌ v2 |
| 平台-廣告/通知 | src/api/platform/*.ts | 無 | ❌ v2 |
| 系統-角色/選單/備份 | src/api/system/*.ts | 無 | ❌ v2 |
| 訊息管理 | src/api/messages/*.ts | 無 | ❌ v2 |
| 媒體庫 | src/api/gallery/media.ts | 無 | ❌ v2 |
| 雲端硬碟 | src/api/drive/file.ts | 無 | ❌ v2 |
| 歷史紀錄 | src/api/history/*.ts | 無 | ❌ v2 |
| 開發者-Loki | src/api/developer/loki.ts | 無 | ❌ v2 |
「AMS controller 無」=該前端模組目前由
mock/提供資料,無真實後端端點(W101-Admin-Web@0ee5ee9 mock/README.md:25)。
13.3 常數表
| 常數 | 值 | SoT | 版本 |
|---|---|---|---|
| access token TTL | 1 小時 | PRD(BR-API-CLI-018) | v1 |
| refresh token | 30 天 sliding/90 天 absolute | PRD(BR-API-CLI-018) | v1 |
| device code 有效期 | ≤ 10 分鐘(RD 定值) | RD | v1 |
| session 撤銷生效 | ≤ 30 秒 | PRD(BR-API-CLI-024) | v1 |
| active CLI session 上限 | 5 | PRD(ADR-013) | v1 |
--reason 長度 | 1~200 字 | PRD(BR-API-CLI-020) | v1 |
| exit code | 0/2/3/4/5/6 | PRD(ADR-006) | v1 |
| 既有 admin JWT | 7 天(現況,本 PRD 不改) | admin.service.ts:139 | 現況 |
| 既有 TEMP token | 5 分鐘 | admin.service.ts:133 | 現況 |
| 既有信任裝置 | 30 天 | admin.service.ts:186 | 現況 |
| 既有節流 DEFAULT | 5 次/60 秒 | throttle.constants.ts:6-7 | 現況 |
| 既有節流 ADMIN_LIST | 20 次/60 秒 | throttle.constants.ts:16 | 現況 |
13.4 事件字典
| 事件 | 觸發時機 | Payload 要點 | SoT | 版本 |
|---|---|---|---|---|
admin_cli.session_created | device flow 授權完成 | admin、裝置描述、IP、CLI 版本 | 後端 | v1 |
admin_cli.session_revoked | 撤銷(自撤/他撤/汰換) | session、撤銷者、原因分類 | 後端 | v1 |
admin_cli.refresh_reuse_detected | 偵測到失效 refresh 被重用 | token family、來源 IP | 後端 | v1 |
admin_cli.write | 任何經 CLI 的寫入 | 沿用既有稽核欄位 + channel + reason | 後端 | v1 |
admin_cli.source_mismatch | X-Source 與 token channel 不符 | 兩者值、admin、IP | 後端 | v1 |
13.5 術語字典
| 術語 | 定義 | 不是什麼 |
|---|---|---|
| admin | 後台管理員,與前台 account 完全分離(CLAUDE.md:15) | 不是前台的企業使用者 |
| account | 前台會員(求職者/企業使用者) | 不是後台管理員 |
| channel | 請求來源通道(web / admin_cli / 未來 mcp) | 不是 X-Source header 本身(ADR-005) |
| CLI session | 一台裝置的一份 CLI 授權,治理最小單位 | 不是 HTTP session,也不是瀏覽器 session |
| 破壞性操作 | 不可逆或需人工復原的操作(刪除、撤銷、停用) | 不含可自由來回切換的狀態變更 |
| GUI parity | 「GUI 有的 CLI 都有」 | 本 PRD 明確不追求(ADR-002) |
14. 版本凍結表
| 項目 | v1/v2 | Blocking? | 理由 | 決策者 | 日期 |
|---|---|---|---|---|---|
| Device flow 認證 | v1 | 是 | 無此則 CLI 無法登入 | Eric | 2026-08-25 |
| CLI session 治理 | v1 | 是 | ADR-001 的治理承諾 | Eric | 2026-08-25 |
X-Source 收錄 + channel 稽核 | v1 | 是 | BR-039 落差;S2 成功條件 | Eric | 2026-08-25 |
| W-1 裝置驗證頁 | v1 | 是 | device flow 無介面則不成立 | Eric | 2026-08-25 |
| W-2 CLI 存取頁(帳號設定) | v2 | 否 | 「我的裝置」CLI 已有 sessions 命令;安裝說明在 v1 規模可人工交付(§9 W-2) | Eric | 2026-08-26 |
| W-3 管理員列表 CLI 治理視圖 | v2 | 否 | v1 以軟刪除管理員作為緊急撤銷路徑,下一請求即失效(§9 W-3) | Eric | 2026-08-26 |
| 審核類命令群 | v1 | 是 | 情境 S-3 | Eric | 2026-08-25 |
| 金鑰治理命令群 | v1 | 是 | S4 主線 | Eric | 2026-08-25 |
Idempotency-Key | v1 | 是(可分批) | 斷線重試安全性 | Eric | 2026-08-25 |
| 自我提權防護 | v1 | 是 | BR-API-CLI-023 | Eric | 2026-08-25 |
| CLI 通道獨立節流 | v1 | 否 | 可先沿用既有額度觀察 | Eric | 2026-08-25 |
jobs view | v2 | 否 | 後端缺端點(ADR-012) | Eric | 2026-08-25 |
| mock-only 模組 CLI 化 | v2 | 否 | 後端須先補端點(ADR-002) | Eric | 2026-08-25 |
| 唯讀專用窄權限 token | v2 | 否 | ADR-004 Consequences | Eric | 2026-08-25 |
| admin 線 MCP | v2 | 否 | ADR-010 | Eric | 2026-08-25 |
| Shell completion/binary/i18n | v2 | 否 | DX polish | Eric | 2026-08-25 |
| Storybook mockup(W-1) | v1 | 否 | §9 文字規格已足以開工 | Eric | 2026-08-25 |
15. Token / API 契約完整性
| 項目 | 決定 |
|---|---|
| TTL | access 1 小時;refresh 30 天 sliding/90 天 absolute;device code ≤ 10 分鐘 |
| Refresh 策略 | 必須 rotation;舊 token 立即失效;以 family 追蹤 |
| 重用偵測 | 失效 refresh 被重用 → 撤銷該 family 全部憑證 + 記 admin_cli.refresh_reuse_detected |
| 撤銷政策 | 可由本人於 CLI 或 GUI 撤銷;logout 撤銷當前;revoke --all-others 保留當前 |
| 撤銷生效 | ≤ 30 秒(BR-API-CLI-024) |
| 過期行為 | access 過期 → 自動 refresh;refresh 失效 → exit 6,明示重跑 login |
| 即時權限重查 | 現況分兩層,須分開講:①帳號狀態每請求查 DB(jwt.strategy.ts:26)→ 停用即時生效;②role 直接取自 token payload(jwt.strategy.ts:37)→ 降權不會即時生效;③DB 功能權限查詢僅在端點宣告 permissions 時執行,現況 80 個 @Auth 端點皆未宣告(roles-permissions.guard.ts:39),故從未執行。本 PRD 的處置:access token TTL 壓到 1 小時,即為 role 降權的最大生效延遲上界;需要更快者,走 session 撤銷(≤30s)而非等 role 傳播(§19 問題 9) |
| 冪等 | 破壞性與建立類端點須支援 Idempotency-Key;讀取類不需要 |
| 通道權威 | token 綁定為準;X-Source 僅交叉比對(ADR-005) |
| 不擴權 | token 繼承既有 role/permission,channel 僅用於限縮(BR-API-CLI-019) |
| Session 上限 | 每 admin ≤ 5,超額汰換最久未使用(ADR-013) |
16. Mock Data Schema
N/A —— 本 PRD 不產 Storybook mockup(§9 為文字規格)。CLI 本身不做 mock 模式(ADR-011)。
17. Sync-Fix List
| # | 項目 | 負責 | Blocking? |
|---|---|---|---|
| 1 | 建立 W101-Admin-CLI repo,設定私有 registry 與 release 流程 | CLI + DevOps | 是 |
| 2 | AMS 實作 admin device flow AS(§8.1)與 session 治理 API(§8.2) | 後端 | 是 |
| 3 | AMS 補 X-Source 收錄 + 稽核 channel 語意(BR-039 落差,全 src/ 目前零實作) | 後端 | 是 |
| 4 | AMS 破壞性/建立類端點支援 Idempotency-Key | 後端 | 是(可分批) |
| 5 | AMS 加自我提權/自我鎖定的 domain 層防護(BR-API-CLI-023) | 後端 | 是 |
| 6 | AMS 補 job/v2 單筆詳情端點(現況僅 search + deactivate,ADR-012) | 後端 | 否(v2) |
| 7 | Admin-Web 實作 W-1 裝置驗證頁 + W-2 session 清單(§9) | 前端 | 是 |
| 8 | 評估 CLI 通道獨立節流額度(既有 DEFAULT 5/60s 對批次讀取偏嚴,§8.4) | 後端 | 否 |
| 9 | 產出 W-1 的 Storybook mockup(可用 gen-storybook-mockup-gen) | 前端 | 否 |
| 10 | business-rules.md 納入 BR-API-CLI-018~024(§4.2) | PM | 否 |
| 11 | codegen 來源設定為 dev/staging 並凍結 spec 進 repo(ADR-009) | CLI | 是 |
| 12 | README 明確說明「後台有些頁面 CLI 沒有」為預期行為(ADR-002 Consequences) | CLI | 否 |
| 13 | README 對照 @wport/cli 與 @wport/admin-cli 的 exit code 差異(ADR-006) | CLI | 否 |
| 14 | 評估 GUI 端是否比照強制填寫操作理由(ADR-007 Consequences) | 產品 | 否 |
| 15 | scripts/prd-lint.py 併入 main(目前僅存於 origin/ericlu-sys/prd-refine) | PM | 否 |
| 16 | 授權模型現況交後端設計(§19 問題 1 的六項現況+三項產品層要求);其中 DTO 缺角色欄位、建立時未寫 admin_user_role、API 回應含明碼密碼三項屬漏寫,可先行修復 | 後端 | 是(影響 ADR-004 的有效性) |
18. Email Template
N/A —— 本 PRD 不觸發任何郵件。
唯一相近的情境是 session 汰換(ADR-013)與 refresh 重用偵測(§15)。兩者 v1 以後台事件記錄呈現,不寄信;若後續要加安全通知信,屬獨立需求。
19. PRD 問題清單
| # | PRD 章節/規則 | 問題 | 建議修法 | 類別 | 影響 | SoT |
|---|---|---|---|---|---|---|
| 1 | §3.2、§3.4 N-4、ADR-004 | 既有授權模型的六項現況(全部實查,見 §2 台帳):①建立管理員的 DTO 只有 email,無角色欄位 ②createAdminUser() 從不寫入 admin_user_role ③無角色列時 resolveAdminIdentity 判為 ADMIN,通過幾乎所有 @Auth 守衛 ④getPermissions() 是寫死 stub,且 80 個端點無一宣告 permissions ⑤新帳號 mfaEnabled: false,登入不需 MFA ⑥完整 token 7 天、無 refresh、不可單獨撤銷。合起來的效果是:每個管理員實際上都是全權,且無法降權、停用或撤銷。這直接削弱本 PRD 的 ADR-004(「token 繼承既有 role/permission,不擴權」)——繼承一個無上限的權限,等於沒有約束 | 授權模型要怎麼修,由後端設計(scope、permission 表、或其他形態皆可,本 PRD 不指定機制)。本 PRD 只提出三項產品層要求:MFA 必須為強制、新帳號預設最小權限、必須可降權與撤銷。另有三項純屬漏寫、與授權架構無關,可逕行修復:DTO 缺角色欄位、建立時未寫 admin_user_role、API 回應含明碼密碼 | 資安 | 高 | 後端設計 |
| 2 | ADR-007 | CLI 強制填 --reason,GUI 未強制,導致同一稽核表的資料品質不一致 | 評估 GUI 對破壞性操作亦強制填理由(sync-fix #14) | 一致性 | 中 | 產品 |
| 3 | §10.3 | 對已撤銷的金鑰再次 revoke:應視為冪等成功,或回狀態衝突? | RD 於 OpenAPI Spec 定調並在 CLI 對應錯誤訊息 | 契約 | 低 | RD |
| 4 | §5.2 E-1 | device code 有效期寫「≤ 10 分鐘(RD 定值)」,未鎖死 | RD 定值後回寫 §13.3 常數表 | 契約 | 低 | RD |
| 5 | §4.2 | BR-API-CLI-018~024 為提案,尚未進 business-rules.md。另:§4.2 的編號說明引述了主序列的既有搶號情形——BR-039/BR-041 已配發卻被 pm_46 重複提案,BR-042 則同時被 pm_46/pm_52 當成新規則。此三個號碼非本 PRD 提案,僅為說明本 PRD 為何改用 BR-API-CLI 系列而引述 | 本 PRD 一律用 BR-API-CLI 系列規避,不碰主序列;BR-039/041/042 的重編屬既有跨 PRD 問題,由 PM 統一裁決(見 sync-fix #10)。在裁決前,本列即為這三個號碼的待辦掛點 | 流程 | 中 | PM |
| 6 | §13.2 | mock-only 模組的判定依據是「AMS 無對應 controller」,未逐一確認是否有其他後端服務承接 | RD 覆核;若有其他服務承接,回寫本表並重評 v1 範圍 | 事實 | 中 | RD |
| 7 | §8.4、§17 #8 | 既有節流 DEFAULT 5 次/60 秒對 S-3 的批次讀取偏嚴,但實際會不會撞到取決於端點各自套用哪組常數,本 PRD 未逐端點確認 | 實作前逐端點盤點 @Throttle 標註 | 效能 | 中 | RD |
| 8 | §17 #15 | prd-lint 目前不在 main,僅存於 origin/ericlu-sys/prd-refine(commit 33e887e);本 PRD 的 lint 以該分支取出的版本執行 | 併入 main 後所有 PRD 統一基準 | 流程 | 低 | PM |
| 9 | §15、§10.3 | role 快取於 token,降權不即時生效(jwt.strategy.ts:37);且 DB 功能權限查詢在現行端點上從未執行(roles-permissions.guard.ts:39,80 端點無一宣告 permissions)。本 PRD 以 1 小時 TTL 與 session 撤銷(≤30s)兜住 CLI 通道,但根因屬授權模型,非本 PRD 可解 | 併入問題 1 一併由後端設計;本 PRD 不預設修法 | 資安 | 高 | 後端設計 |
| 10 | §1.1 | v1.0.0 誤用 pm_53——該號已於 2026/08/20 配發給「職缺生命週期與 SEO 索引健康度」PRD。根因:撞號檢查只掃 refs/remotes/origin,而該 PRD 所在分支當時未推送 | 配號前掃全 ref(refs/heads + refs/remotes)+ 所有 worktree 工作區;已於 v1.0.1 改號為 pm_54 | 流程 | 中 | PM |
| 11 | §9、§14 | v1 僅交付 W-1;W-2/W-3 延後,且 v1 的安裝憑證為人工交付、無自助管道 | 已確認兩條替代路徑成立:①「我的裝置」由 CLI sessions list/revoke 涵蓋 ②撤銷他人以軟刪除管理員達成(admin-user.service.ts:141 → admin.repository.ts:193-199 → jwt.strategy.ts:26 每請求檢查,下一請求即 401)。殘留限制:刪帳號 all-or-nothing,無法只收 CLI 保留 GUI,「筆電遺失但人在職」須刪帳號再重建。使用者規模成長或出現該情境時即應排入 W-2/W-3 | 範圍 | 中 | PRD |
20. 下一步(Next Steps)
- 本 PRD 交
gen-prd-checker做對抗性審查(Backend RD/Security/SRE 三 persona)——admin 線是最高權限通道,這一關不可略過 - RD 依 §8 產出 AMS 的 OpenAPI Spec(device flow + session API + 既有端點的 channel/冪等擴充)
- 前端依 §9 產出 W-1/W-2;可先用
gen-storybook-mockup-gen出 mockup gen-trello-project-setup建卡(1 goal + FE/BE 兩張 delivery,另需第三張給 CLI repo)- §4.2 的 BR 提案送審後寫入
business-rules.md - §19 問題 1(GUI 通道的 7 天 JWT)獨立立案——本 PRD 只負責不讓新通道重蹈覆轍,不負責修舊的
附錄 A:三條 CLI 產品線對照
| 面向 | pm_41 企業線 | pm_47 個人線 | pm_54 後台線 |
|---|---|---|---|
| 使用者 | 企業 HR | 求職者 | 內部 admin |
| 套件 | @wport/cli(公開) | @wport/cli(同套件) | @wport/admin-cli(私有) |
| 認證 | API key wpk_live_ | OAuth device flow | OAuth device flow(MFA 在瀏覽器) |
| 長期憑證 | 有(可輪替) | 無 | 無(核彈 N-4) |
| 權限模型 | key 綁公司 | token 綁 user | 繼承既有 admin role,不擴權 |
| 批次寫入 | 有(jobs batch) | 有(apply ≤20) | 無(ADR-008) |
| 理由必填 | 否 | 否 | 是(ADR-007) |
| GUI parity | 追求 | 追求 | 明確放棄(ADR-002) |
| exit code | 0/2/3/4/5 | 0/2/3/4/5 | 0/2/3/4/5/6 |
關鍵差異的理由:前兩條線的使用者是客戶,設計目標是能力最大化(deny-list 制、限量取代禁令)。後台線的使用者是我們自己,設計目標是可治理——同樣的 deny-list 哲學,但判準從「會不會擋到使用者」換成「出事時查不查得出來、關不關得掉」。