雲端整合(Cloud Integrations)¶
功能群:證據管理|儲存後端與證據來源共用概念先讀 功能群總覽
事實基準:2026-07-05 從 FE / BE code 掃出(FR-016 Google Drive sync 三階段功能;API schema 逐條對照 marshmallow serializer 與 service 組裝碼)
變更紀錄¶
| 日期 | FR | 變更 |
|---|---|---|
| 2026-08-29 | CM-1412 | 操作權下放 Tenant Admin:連線/中斷/同步紀錄/重試/重建目錄/webhook 註冊六支端點的守門,由帳號層 root 旗標(is_super_admin)改為能力點 cloud_integration.{read,create,update,delete}(守門統一掛 route 層,service 層 is_admin 參數鏈拆除,錯誤碼改標準 GRC_403022)。FE 同步改用 hasCap() 判斷按鈕顯示(原用 userInfo.is_admin,會出現「畫面給了按鈕、點下去 403」),並補上原本完全沒守的同步紀錄「重試」鈕。無 migration——四個能力點 2026-04-27 即存在。詳見 §3 |
1. 功能描述¶
雲端整合頁是租戶層級(非單一專案)的 Google Drive 連線管理頁:tenant admin 在此完成 OAuth 授權、檢視連線狀態、查看背景同步 job 歷史,以及在資料夾結構損壞時觸發全租戶重建。連線建立後,Drive 成為專案資料夾結構與證據檔案的雲端來源(見 _overview §4);per-project 的資料夾初始化 / 修復操作則分散在專案清單頁與專案規劃頁 Tab5(見 §1.1 註記),不在本頁。
主要使用者:具 cloud_integration.* 能力的租戶管理員(CM-1412 起以能力點判定,不再是帳號層 root 旗標)。一般使用者可讀取連線狀態(頁面可進入),但寫入操作(連接/中斷/重建)需對應能力點。
角色速覽(完整定義見 _overview §5;本頁的角色基調與其餘系統/租戶層頁面一致):
| 角色 | 一句話 |
|---|---|
具 cloud_integration.* 能力者 |
租戶管理者 — 本頁寫入操作(連接/中斷/重建/重試)的授權對象,逐操作對應不同 action(見 §3) |
| super admin | 系統層級管理者 — disconnect 額外放行的第二種身分(見 §3) |
| 一般使用者 | 可進頁、可讀連線狀態,無任何寫入按鈕會出現作用(BE 仍會擋) |
1.1 功能總覽(本頁全部功能)¶
| # | 功能 | 說明 | 位置 | 詳述 |
|---|---|---|---|---|
| 1 | 連線狀態卡片 | 顯示 DISCONNECTED/CONNECTED/REVOKED/EXPIRED 四態,對應不同卡片內容 | GoogleDriveIntegrationCard | UC-CI-01 |
| 2 | 連接 Google Drive | OAuth popup 授權流程,完成後 postMessage 回主視窗 | 未連線態按鈕 | UC-CI-01 |
| 3 | 帳號與連線資訊顯示 | Google 帳號 email、連線時間+操作人、根資料夾連結(有才顯示)、最後同步時間(有才顯示) | 已連線態 dl 列表 | §5 |
| 4 | 中斷連線 | 確認對話框 → 停 webhook → HTTP revoke token → 清空 DB 欄位(三階段,皆 best-effort 容錯) | 已連線態右下角按鈕 | UC-CI-02 |
| 5 | 重新授權 | 連線異常(REVOKED/EXPIRED)時的醒目按鈕;已連線時收進「⋯」選單,走同一支 connect 流程 | 異常態 Message/「⋯」選單 | UC-CI-01(同流程) |
| 6 | 查看同步紀錄 | 開 dialog 顯示 drive_sync_jobs 分頁列表,可依狀態篩選、對 FAILED job 手動重試 |
「⋯」選單 → SyncJobHistoryDialog | UC-CI-04(列表本身為分頁顯示,見 §5/§6.3;重試邏輯 UC-CI-04) |
| 7 | 重建整個租戶資料夾結構 | Destructive 操作:清空全租戶 drive_folder_mappings+為每個進行中專案重新 enqueue 初始化;type-to-confirm(需輸入 REBUILD) |
「⋯」選單(限 admin) → 確認 dialog | UC-CI-03 |
| 8 | Per-project 資料夾初始化 | 對單一(尚未同步)專案手動觸發建立 Drive 資料夾 | 不在本頁——ProjectListView.vue「同步至 Drive」按鈕 |
§12 坑 1 |
| 9 | Per-project 驗證與修復 | 檢查單一專案下所有資料夾對應是否仍存在,標記失聯並重建 | 不在本頁——ProjectCloudIntegrationsPanel.vue(專案規劃頁 Tab5,見 專案雲端整合),manager-only |
§12 坑 1 |
| 10 | Webhook 手動註冊 | 補註冊 Drive Push channel+為未同步專案 backfill init(用於 Phase 3 前連線的舊租戶) | 無 FE 入口,僅 API 存在(維運操作) | UC-CI-05 |
UC(§2)展開本頁所有具實質後端邏輯的操作路徑:1/5(連接/重新授權,共用同一流程 UC-CI-01)、4(中斷 UC-CI-02)、7(重建租戶 UC-CI-03)、6(同步 job 重試 UC-CI-04)、10(webhook 手動註冊 backfill UC-CI-05)。純顯示類(1 連線狀態卡片、3 帳號資訊顯示)見 §5;同步 job 列表本身為分頁顯示見 §6.3。per-project 操作(8 init-folders、9 verify-and-repair)FE 入口不在本頁(分屬專案清單頁 / 專案規劃頁 Tab5),其 endpoint 於 §6.1 交叉列出、坑點見 §12 坑 1,本頁不重複展開以免維護 drift。
2. Use Case¶
角色速覽(完整定義見 _overview §5;本頁的角色基調與其餘系統/租戶層頁面一致):
| 角色 | 一句話 |
|---|---|
具 cloud_integration.* 能力者 |
租戶管理者 — 本頁寫入操作(連接/中斷/重建/重試)的授權對象,逐操作對應不同 action(見 §3) |
| super admin | 系統層級管理者 — disconnect 額外放行的第二種身分(見 §3) |
| 一般使用者 | 可進頁、可讀連線狀態,無任何寫入按鈕會出現作用(BE 仍會擋) |
流程圖視覺慣例:菱形 = 判斷、橘底 = 例外/擋下、紅框 = 錯誤(標 error code)、綠框 = 成功終點、虛線 = 可選路徑。
UC-CI-01 連接/重新授權 Google Drive¶
| 項目 | 內容 |
|---|---|
| 角色 | tenant admin |
| 前置條件 | 已登入;具 cloud_integration.create |
| 產出 / 後置條件 | tenant_drive_integrations 新增或更新一列(status=CONNECTED,token 加密存放);同 transaction 內註冊 Drive Push channel;best-effort 為斷線期間新建的專案補建資料夾 |
UC-CI-02 中斷連線¶
| 項目 | 內容 |
|---|---|
| 角色 | tenant admin 或 super admin |
| 前置條件 | 已登入;tenant_drive_integrations 存在該租戶的連線列 |
| 產出 / 後置條件 | Drive Push channel 停止;OAuth token 於 Google 端撤銷(best-effort);DB 欄位清空回 DISCONNECTED |
三階段皆各自容錯(stage 0/1 失敗只記 log,不擋 stage 2 執行):① webhook_channel_manager.stop_for_tenant(best-effort)② HTTP revoke refresh token(best-effort)③ _domain.disconnect(tenant_id) 清空 DB 欄位(實際生效的一步)。三步分屬三個獨立 @transaction 方法(google_drive_integration_service.py:187-221),中間穿插的 HTTP 呼叫不佔用 DB session。
UC-CI-03 重建整個租戶 Drive 資料夾結構(destructive)¶
| 項目 | 內容 |
|---|---|
| 角色 | tenant admin |
| 前置條件 | 已登入;具 cloud_integration.update;FE 要求輸入關鍵字 REBUILD 才能按下確認鈕 |
| 產出 / 後置條件 | 該租戶全部 drive_folder_mappings 被清空;tenant_drive_integrations.root_folder_id 重置為 null;每個現存專案重新 enqueue INIT_PROJECT_FOLDERS job |
上圖沿用版面骨架圖示意「⋯」選單 → 重建 dialog 的觸發路徑;重建本身無分支例外(僅
cloud_integration.update一道門),故不另畫獨立流程圖。
UC-CI-04 查看同步紀錄並手動重試 FAILED job¶
| 項目 | 內容 |
|---|---|
| 角色 | 具 cloud_integration.update 能力者 |
| 前置條件 | 已登入;具 cloud_integration.update;欲重試者須為 FAILED 狀態的 job |
| 產出 / 後置條件 | 該 FAILED job 重置為 PENDING(等下一輪背景 worker 輪詢重跑,非即時);不改動其他 job |
UC-CI-05 手動補註冊 Webhook 並 backfill(維運操作,無 FE 入口)¶
| 項目 | 內容 |
|---|---|
| 角色 | 具 cloud_integration.update 能力者;route 層 decorator 檢查 |
| 前置條件 | 已登入;具 cloud_integration.update;典型情境為 Phase 3 上線前就已連線、或斷線期間新建專案的舊租戶 |
| 產出 / 後置條件 | 註冊一個新 Drive Push channel(idempotent,已有 live channel 也不報錯);為未同步專案 backfill 補 enqueue init(略過已有 PROJECT scope 對應的專案) |
3. 權限矩陣¶
⚠️ 2026-08-29(CM-1412)整段改寫:本頁原本用帳號層 root 旗標(
is_super_admin/UserContextDTO.is_admin)守門,全系統只有 root admin 為 true,故 Tenant Admin 按任何操作都必被擋——且錯誤訊息還寫「需要 tenant admin 權限」,訊息與實際判定對不上。已改為與 SMTP 設定/LDAP 設定/Log 轉發設定 同一套能力點機制,守門統一掛 route 層。
| 操作 | FE 判定 | BE 強制 | BE 檢查位置 |
|---|---|---|---|
| 讀連線狀態(進頁/卡片顯示) | 無(僅要求登入) | 刻意不掛能力點,僅 @jwt_required() |
google_drive_integration_route.py(GET) |
| 產生 OAuth 授權 URL(連線/重新授權) | hasCap('cloud_integration.create') |
✅ require_capability("cloud_integration.create") |
google_drive_integration_route.py:73 |
| 中斷連線 | hasCap('cloud_integration.delete') |
✅ require_capability("cloud_integration.delete") |
google_drive_integration_route.py:54 |
| 查看同步 job 歷史 | hasCap('cloud_integration.read') |
✅ require_capability("cloud_integration.read") |
google_drive_sync_route.py:37 |
| 手動重試 sync job | hasCap('cloud_integration.update') |
✅ require_capability("cloud_integration.update") |
google_drive_sync_route.py:84 |
| 重建整個租戶資料夾 | hasCap('cloud_integration.update') |
✅ require_capability("cloud_integration.update") |
google_drive_sync_route.py:167 |
| Webhook 手動註冊+backfill | 無 FE 入口 | ✅ require_capability("cloud_integration.update")(原為 root-only) |
google_drive_sync_route.py:197 |
| Per-project 初始化資料夾 | 無角色判定 | 刻意不掛(僅 @jwt_required()) |
drive_sync_admin_service.py(docstring 明載理由) |
| Per-project 驗證與修復 | canEdit(manager 才可點) |
專案 manager → 否則 GRC_NOT_MANAGER |
drive_project_verify_service.py |
| OAuth callback(Google 導回) | — | 無角色檢查(state 一次性 Redis token 即身分綁定,見 §12 坑 2) |
google_drive_integration_route.py |
| Webhook 事件接收 | — | 無 JWT;以 X-Goog-Channel-Token 比對取代 |
google_drive_webhook_route.py |
守門順序:登入(@jwt_required)→ 授權照(@require_license("cloud_integration"))→ 能力點(@require_capability)。無能力點回標準能力點 403 GRC_403022,不再用會誤導人的 GRC_NOT_ADMIN。
service 層不再收 is_admin 參數——那條參數鏈已全部拆掉,守門集中在 route 層一處;disconnect 內另一條 _is_super_admin 旁路也一併移除(與傳進來的 is_admin 判的是同一件事,等於同一判定寫兩遍)。
三處刻意不掛守門(不是漏掛):
| 端點 | 為什麼不掛 |
|---|---|
| GET 連線狀態 | 附載性查詢——專案清單頁、專案規劃頁一載入就被動探一下狀態以決定要不要顯示 Drive 區塊,呼叫者是一般專案成員。掛 read 會讓那兩頁對非管理員整個壞掉,而「有沒有連線」本身不敏感 |
| Per-project 初始化資料夾 / 驗證修復 | 觸發者是專案 manager 而非租戶管理員,掛了會把專案 manager 擋掉(這正是當初移除 is_admin gate 的原因) |
| OAuth callback / webhook 接收 | 本來就沒有 JWT(是 Google 打進來的) |
能力點無需新增 migration:
cloud_integration.{read,create,update,delete}四個能力點 2026-04-27 即已存在(is_platform=false,且早已綁好cloud-integrations選單路由)。DEV 實查七個角色全部已持有,比smtp-config的持有面更廣;新租戶 provisioning 排除清單不含它、出貨基線scripts/init/04-seed-core.sql也已含。本次改動無 migration,三環境都不需套。⚠️ 能力點名稱是底線版
cloud_integration.*,不是 hyphen 版——hyphen 版在 DB 查無。新增相關守門時勿寫成cloud-integration.*,否則會變成同一件事有兩套能力點名。授權模組判定:
cloud_integration不在INFRASTRUCTURE_MODULES豁免清單(維持只有smtp-config/ldap-config/log-forwarding/security-policy四項)——這是販售模組,與 log 轉發性質不同,沒有照就是不能用。六支端點的@require_license("cloud_integration")全部保留。
4. 狀態機與前置條件¶
tenant_drive_integrations.status 四態(非稽核輪次狀態機,是本頁專屬的連線狀態):
| 狀態 | 意義 | 觸發來源 |
|---|---|---|
DISCONNECTED |
尚未連線,或已中斷 | 初始值;中斷連線後的終態 |
CONNECTED |
正常連線中 | OAuth callback 成功寫入 |
REVOKED |
Token 已被 Google 端撤銷(使用者在 Google 帳號設定移除授權) | 背景同步偵測到 401 時回寫(不在本頁鏈路,見 §11) |
EXPIRED |
Access token 過期且 refresh 失敗 | 同上 |
| 條件 | 效果 | 出處 |
|---|---|---|
status === 'DISCONNECTED' |
顯示「連接」按鈕與警語 | GoogleDriveIntegrationCard.vue:286-295 |
status IN ('REVOKED','EXPIRED') |
紅色 Message + 「重新授權」按鈕(走同一支 connect()) |
同檔 :190-191, 297-308 |
status === 'CONNECTED' |
顯示帳號資訊 dl 列表 + 「⋯」選單 + 「中斷連線」按鈕 | 同檔 :311-359 |
root_folder_id 為 null |
不顯示「根資料夾」欄位(Phase 2 才會有資料夾樹) | 同檔 :332 |
last_sync_at 為 null |
不顯示「最後同步」欄位(Phase 3 才會有同步紀錄) | 同檔 :344 |
無 cloud_integration.update 能力 |
「⋯」選單不出現「重建整個租戶資料夾」項目;能力點過濾後若一個選項都不剩,整顆「⋯」按鈕不出現(否則點開是空白面板) | GoogleDriveIntegrationCard.vue(CM-1412 改用 hasCap) |
| BE 寫入操作 | 無任何 phase / 狀態前置檢查——僅角色檢查(見 §3),無論目前 status 為何皆可呼叫 connect/disconnect/rebuild | app service 層程式碼確認 |
5. UI 設計¶
📸 實際畫面截圖待補(一律亮色模式拍攝):以 playwright 對 STG 擷取,預計補以下四張到
assets/img/: ① 未連線態卡片cloud-integrations-disconnected.png② 已連線態卡片(含帳號資訊、根資料夾連結、⋯選單展開)cloud-integrations-connected.png③ 同步 job 歷史 dialog(含 FAILED 列的重試按鈕)cloud-integrations-sync-jobs.png④ 重建租戶資料夾確認 dialog(type-to-confirm 輸入框)cloud-integrations-rebuild-dialog.png
版面骨架與區塊職責(單卡片頁,無左樹/無 Tab/無分頁列表;彩色區塊可點跳本頁小節錨點,API 僅標代表性):
狀態呈現:讀取中用 ProgressBar indeterminate 模式(非全頁 spinner);連接/中斷/重建動作各自獨立 loading flag(connecting / disconnecting / rebuildLoading),按鈕本身顯示 spinner 並 disable 防雙擊;無 socket,狀態更新靠 OAuth popup 完成後的 postMessage 觸發 refresh() 重打狀態 API,其餘操作靠呼叫後手動 refresh() / fetchJobs()。多租戶切換時(watch(tenantId),位於 ProjectListView.vue,非本頁)會觸發連線狀態重查,避免殘留前一租戶的 badge。
6. API 規格¶
Envelope:成功 {"status": true, "data": …}(部分 endpoint 直接回傳物件不包 data 鍵,見各段落)、失敗 {"error_code": "…", "msg": "…"}(HTTP 4xx/5xx)。
6.1 總清單(本模組全部 endpoint;本頁 UI 只用其中 6 支,其餘見備註)¶
| 分類 | Method + Path | 說明 | 完整規格 |
|---|---|---|---|
| 連線 | GET /integrations/google-drive |
連線狀態(本頁載入用) | §6.2 |
| 連線 | POST /integrations/google-drive/auth-url |
產生 OAuth 授權 URL(admin only) | §6.2 |
| 連線 | GET /integrations/google-drive/callback |
OAuth callback(Google 導回,回 HTML 非 JSON) | §6.2 |
| 連線 | DELETE /integrations/google-drive |
中斷連線(admin/super admin only) | §6.2 |
| 同步 job | GET /integrations/google-drive/sync-jobs |
Job 歷史分頁列表(admin only) | §6.3 |
| 同步 job | POST /integrations/google-drive/sync-jobs/{uid}/retry |
手動重試 FAILED job(admin only) | §6.3 |
| 租戶維運 | POST /integrations/google-drive/tenant/rebuild-all |
重建整個租戶資料夾(admin only,destructive) | §6.3 |
| per-project(不在本頁) | POST /integrations/google-drive/projects/{project_uid}/init-folders |
手動初始化單一專案資料夾;FE 呼叫方在 ProjectListView.vue |
§12 坑 1 |
| per-project(不在本頁) | POST /integrations/google-drive/projects/{project_uid}/verify-and-repair-folders |
單一專案驗證與修復;FE 呼叫方在 ProjectCloudIntegrationsPanel.vue(manager-only) |
§12 坑 1 |
| 維運(無 FE 入口) | POST /integrations/google-drive/webhook/register |
手動補註冊 webhook + backfill 未同步專案(admin only) | §6.3 |
| 系統(非本頁呼叫) | POST /webhooks/google-drive/{tenant_id} |
Google Drive 變更通知接收端;無 JWT,靠 channel token 驗證 | §11 |
6.2 連線類¶
[GET] /integrations/google-drive¶
Response data:
| 欄位 | 型別 | 說明 |
|---|---|---|
| uid | string|null | 連線記錄 uid;未連線(無 row)時整包欄位皆為 null,status 固定回 DISCONNECTED |
| status | string | CONNECTED / EXPIRED / REVOKED / DISCONNECTED |
| google_account_email | string|null | 已連線帳號 |
| root_folder_id | string|null | Drive 根資料夾 ID(Phase 2 才有值) |
| connected_by_user_id / connected_by_user_name | int|null / string|null | 操作人 id / nickname(service 層查 UserDomainService 轉換) |
| connected_at | datetime|null | ISO 8601 |
| last_sync_at | datetime|null | 最後一次背景同步時間(Phase 3 才有值) |
| webhook_expires_at | datetime|null | Push channel 到期時間 |
| last_sync_error | string|null | 最近一次同步錯誤訊息 |
出處:api/cloud_integration/serializers/google_drive_integration.py:9-19(GoogleDriveStatusResponse);組裝 app/cloud_integration/service/google_drive_integration_service.py:170-176(get_status)+ app/cloud_integration/dto/google_drive_integration_dto.py:24-54(GoogleDriveIntegrationStatusDTO.from_entity,無連線列時回傳全 null 的 DISCONNECTED DTO)。
[POST] /integrations/google-drive/auth-url¶
無 request body。Response:{auth_url: string, state: string}。state 為一次性隨機 token,寫入 Redis(key drive_oauth_state:<state>,TTL 600 秒,value 為 <tenant_id>:<user_id>)供 callback 驗證身分;auth_url 帶 REQUIRED_SCOPES(drive + userinfo.email)。出處:app/cloud_integration/service/google_drive_integration_service.py:64-72。
[GET] /integrations/google-drive/callback¶
Request(query string):
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
| code | string | 否 | Google 授權碼 |
| state | string | 否 | 比對 Redis |
| error | string | 否 | 使用者拒絕授權時 Google 帶入 |
回傳 HTML 而非 JSON——內嵌 <script> 對 window.opener 執行 postMessage({source:'guidant-drive-oauth', status, error}) 後 800ms 自動關閉視窗;error 值一律經 json.dumps 逸出避免 reflected XSS。出處:api/cloud_integration/routes/google_drive_integration_route.py:69-123。
[DELETE] /integrations/google-drive¶
無 request body。Response:{disconnected: bool}(該租戶原本無連線列時回 false)。出處:app/cloud_integration/service/google_drive_integration_service.py:187-221。
6.3 同步 job 與租戶維運類¶
[GET] /integrations/google-drive/sync-jobs¶
Request(query string):
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
| status | string|null | 否 | PENDING/IN_PROGRESS/SUCCESS/FAILED/SKIPPED;省略 = 不篩選 |
| page | int | 否 | 預設 1 |
| page_size | int | 否 | 預設 20(FE dialog 固定帶 50) |
出處:api/cloud_integration/serializers/drive_sync_job.py:19-22(DriveSyncJobListQuerySchema)。
Response(頂層即含 data / meta,不再包一層 status——與 §1 envelope 描述的分頁慣例一致):data 為陣列,每項欄位:
| 欄位 | 型別 | 說明 |
|---|---|---|
| uid | uuid | Job 識別 |
| job_type | string | INIT_PROJECT_FOLDERS / CREATE_FOLDER / RENAME_FOLDER / PROCESS_DRIVE_CHANGES / IMPORT_DRIVE_FILE / SOFT_DELETE_EVIDENCE / RECONCILE_TASK_FOLDER / ARCHIVE_DRIVE_FILE |
| status | string | 見上表 5 態 |
| priority / retry_count / max_retries | int | 預設 100 / 0 / 5 |
| next_run_at | datetime|null | Worker 排程輪詢依據 |
| last_error | string|null | 最近一次失敗訊息 |
| started_at / finished_at / created_at | datetime|null | — |
| payload | object | Job 執行上下文(依 job_type 內容不同,如 project_uid / scope_uid) |
meta 欄位:
| 欄位 | 型別 | 說明 |
|---|---|---|
| paging | bool | 固定 true |
| page | int | 目前頁碼 |
| page_size | int | 每頁筆數 |
| total | int | 總筆數 |
| total_pages | int | 總頁數 |
| has_next | bool | 是否有下一頁 |
| has_prev | bool | 是否有上一頁 |
出處:api/cloud_integration/serializers/drive_sync_job.py:4-16(DriveSyncJobResponse);組裝 api/cloud_integration/routes/google_drive_sync_route.py:38-75、app/cloud_integration/service/drive_sync_admin_service.py:82-105(list_jobs)。
[POST] /integrations/google-drive/sync-jobs/{uid}/retry¶
Path 參數:uid(job uid)。無 request body。Response:{retried: bool}。行為:僅重置 FAILED job 為 PENDING(等下一輪 worker 輪詢);不做即時重跑。出處:app/cloud_integration/service/drive_sync_admin_service.py:108-112。
[POST] /integrations/google-drive/tenant/rebuild-all¶
無 request body。Response:{wiped: int, enqueued_projects: int}(wiped = 被清空的資料夾對應筆數;enqueued_projects = 重新 enqueue 初始化 job 的專案數)。Destructive:清空後舊 Drive 帳號裡的資料夾不會被刪除(變成孤兒),只是系統端不再追蹤,重建後長出全新資料夾樹。出處:app/cloud_integration/service/drive_sync_admin_service.py:139-178。
[POST] /integrations/google-drive/webhook/register¶
無 request body。Response:{registered: bool, backfilled_projects: int}。Idempotent:對已有 live channel 的租戶呼叫會註冊一個新 channel(不會報錯),並略過已有 PROJECT scope 資料夾對應的專案。出處:api/cloud_integration/routes/google_drive_sync_route.py:178-216。
7. 前端檔案地圖(compliance-manager-fe/)¶
| 檔案 | 角色 |
|---|---|
src/views/integrations/CloudIntegrationsView.vue |
頁面殼(19 行):僅標題 + <GoogleDriveIntegrationCard />,無自身邏輯 |
src/components/integrations/GoogleDriveIntegrationCard.vue |
本頁核心元件(403 行):四態卡片、OAuth popup 流程、中斷連線、「⋯」選單、重建 dialog 皆在此 |
src/components/integrations/SyncJobHistoryDialog.vue |
同步 job 歷史 dialog:狀態篩選 Dropdown、DataTable、FAILED 列重試按鈕 |
src/config/router/index.js:356-364 |
route cloud-integrations,path /settings/cloud-integrations,僅 requiresAuth: true,無 capability meta |
src/config/api/api.js:379-387 |
INTEGRATION_GDRIVE* 常數群 |
src/service/CloudIntegrationService.js |
API 包裝:getGoogleDriveStatus / getGoogleDriveAuthUrl / disconnectGoogleDrive / listSyncJobs / retrySyncJob / initProjectFolders / verifyAndRepairProjectFolders / rebuildAllDriveFolders / driveFolderUrl(純字串拼接,無 API 呼叫) |
src/components/grc/project/ProjectCloudIntegrationsPanel.vue |
另一頁面(專案規劃頁 Tab5,見 專案雲端整合):per-project 驗證與修復,manager-only,非本頁但共用 CloudIntegrationService |
src/views/project/ProjectListView.vue:113-124 |
per-project「同步至 Drive」按鈕(handleSyncToDrive),呼叫 initProjectFolders;非本頁但共用 service |
src/config/locales/i18n/{zh-tw,en}/cloud-integration.json |
頁面 i18n(namespace lang.cloud_integration.*) |
src/config/locales/i18n/{zh-tw,en}/menu.json:128,247 |
選單顯示名稱與說明 |
8. 後端檔案地圖(本模組核心鏈路)¶
| 鏈路 | Route | App Service | 底層 |
|---|---|---|---|
| 連線狀態/OAuth | api/cloud_integration/routes/google_drive_integration_route.py(GoogleDriveIntegrationRoute GET/DELETE、GoogleDriveAuthUrlRoute、GoogleDriveCallbackRoute) |
app/cloud_integration/service/google_drive_integration_service.py::GoogleDriveIntegrationService(get_status / build_auth_url / handle_callback / disconnect) |
TenantDriveIntegrationDomainService → TenantDriveIntegrationRepoImpl(infra/cloud_integration/repository/);OAuth 交換走 infra/cloud_integration/google_drive/google_oauth_client.py;token 加密走 infra/cloud_integration/crypto/fernet_crypto.py |
| 同步 job 管理/租戶重建 | api/cloud_integration/routes/google_drive_sync_route.py(DriveSyncJobsRoute / DriveSyncJobRetryRoute / DriveRebuildAllRoute / DriveWebhookRegisterRoute) |
app/cloud_integration/service/drive_sync_admin_service.py::DriveSyncAdminService |
DriveSyncJobDomainService / DriveFolderMappingDomainService / TenantDriveIntegrationDomainService → 對應 infra/cloud_integration/repository/*_repo_impl.py |
| Per-project 初始化(不在本頁 UI) | 同 sync_route(DriveSyncProjectInitFoldersRoute) |
DriveSyncAdminService::trigger_init_project_folders(僅轉呼叫 orchestration,無角色檢查——見 §3) |
DriveSyncOrchestrationService::try_enqueue_init_project_folders |
| Per-project 驗證修復(不在本頁 UI) | 同 sync_route(DriveProjectVerifyAndRepairRoute) |
app/cloud_integration/service/drive_project_verify_service.py::DriveProjectVerifyService(manager 檢查、Drive API 呼叫刻意移出 @transaction 避免長時間佔用 session,見檔頭 docstring) |
ProjectTreeLoader(走 jedi_oscal_v2 SSP 控制樹)+ GoogleDriveApiClient |
| Webhook 接收(背景,非本頁) | api/cloud_integration/routes/google_drive_webhook_route.py::GoogleDriveWebhookRoute |
app/cloud_integration/service/google_drive_webhook_service.py::GoogleDriveWebhookService |
驗證 channel token → 寫入 drive_sync_jobs(PROCESS_DRIVE_CHANGES) |
DDD 提醒:本模組層級劃分清楚(route 不碰 DB,app service 皆 @transaction,domain service 抽象 repository interface),新增本模組 API 可直接比照現有寫法。唯一值得留意的既有簡化:trigger_init_project_folders 刻意不加角色檢查(docstring 說明是為了避免誤擋非 tenant-admin 的專案 manager),新增類似「轉呼叫 orchestration」的方法時,若涉及寫入需重新評估是否也要這樣簡化,不要無條件比照。
9. DB¶
9.1 資料表總清單(本模組讀寫的全部表)¶
| 表 | 讀/寫 | 說明 | 欄位詳述 |
|---|---|---|---|
| public.tenant_drive_integrations | 寫 | 租戶 OAuth 連線主檔(1 租戶 1 列,tenant_id UNIQUE) |
§9.3 |
| public.drive_folder_mappings | 寫 | Drive 資料夾 ↔ 系統實體對應(ROOT/PROJECT/AP/CONTROL_GROUP/CONTROL/AO/TASK/ARCHIVE/EVIDENCES 九種 scope) | §9.3 |
| public.drive_sync_jobs | 寫 | 背景同步任務佇列(本頁只讀列表 + 重試,寫入多由 orchestration / webhook 觸發) | §9.3 |
| compliance.projects | 讀 | Per-project 操作定位專案(init-folders / verify-and-repair,不在本頁 UI) |
見 project-planning §9.3 |
| compliance.project_participants | 讀 | Per-project 驗證修復的 manager 角色檢查來源 | 見 project-planning §9.3 |
9.2 ER 圖¶
9.3 核心表欄位(取自 scripts/deliverables/out/db_schema.json;DB 未設 comment,欄位語意依程式碼實際用途註記)¶
public.tenant_drive_integrations — 租戶 Drive 連線主檔¶
| 欄位 | 型別 | Nullable | 說明 |
|---|---|---|---|
| id | integer | 否 | PK,nextval 序列 |
| uid | uuid | 否 | 對外識別碼,gen_random_uuid() |
| tenant_id | integer | 否 | UNIQUE 索引(tenant_drive_integrations_tenant_id_key)——確保 1 租戶最多 1 列 |
| provider | varchar(20) | 否 | 預設 'google_drive'(欄位存在為未來多雲端來源預留,目前僅此一種) |
| google_account_email | varchar(255) | 是 | 已連線 Google 帳號 |
| refresh_token_encrypted | text | 是 | Fernet 加密(domain/cloud_integration/service/token_crypto_service.py),絕不明文存放 |
| access_token_cache | text | 是 | Fernet 加密的短期 access token 快取 |
| access_token_expires_at | timestamptz | 是 | access token 到期時間 |
| root_folder_id | varchar(100) | 是 | Drive 上 GuidantAI/ 根資料夾 ID |
| drive_change_cursor | varchar(255) | 是 | Drive Changes API 游標(背景同步用,見 §11) |
| webhook_channel_id / webhook_resource_id / webhook_token / webhook_expires_at | varchar(100) / varchar(100) / varchar(100) / timestamptz | 是 | Drive Push Notification channel 資訊;webhook_token 是 webhook 接收端驗證用的隨機密鑰 |
| status | varchar(20) | 否 | 預設 'DISCONNECTED';四態見 §4 |
| connected_by_user_id / connected_at | integer / timestamptz | 是 | 操作人與連線時間 |
| last_sync_at / last_sync_error | timestamptz / text | 是 | 最近一次背景同步結果 |
| created_user / updated_user | varchar(100) | 是 | 審計欄(login_name) |
索引:idx_tenant_drive_integrations_tenant_id、idx_tenant_drive_integrations_status。
public.drive_folder_mappings — 資料夾對應¶
| 欄位 | 型別 | Nullable | 說明 |
|---|---|---|---|
| id / uid | integer / uuid | 否 | PK / 對外識別 |
| tenant_id | integer | 否 | soft ref(無 FK,避免跨 schema CASCADE) |
| scope_type | varchar(20) | 否 | ROOT/PROJECT/AP/CONTROL_GROUP/CONTROL/AO/TASK/ARCHIVE/EVIDENCES(domain/cloud_integration/enums/scope_type.py) |
| scope_uid | uuid | 是 | 依 scope_type 指向對應實體的 uid(ROOT 無 scope_uid) |
| parent_drive_folder_id | varchar(100) | 是 | 父資料夾 Drive ID(重建父子關係用) |
| drive_folder_id | varchar(100) | 否 | UNIQUE(drive_folder_mappings_drive_folder_id_key) |
| display_name_snapshot | varchar(255) | 否 | 建立當下的資料夾顯示名稱快照 |
| is_unlinked | boolean | 否 | 預設 false;verify-and-repair 發現 Drive 端已刪除/無權限時標記為 true |
索引:uq_drive_folder_mappings_root(tenant_id, scope_type WHERE scope_type='ROOT',確保每租戶僅 1 個根節點)、uq_drive_folder_mappings_scope(tenant_id, scope_type, scope_uid WHERE scope_uid IS NOT NULL,同一實體同一 scope 僅 1 個對應)、idx_drive_folder_mappings_parent。
public.drive_sync_jobs — 背景同步任務佇列¶
| 欄位 | 型別 | Nullable | 說明 |
|---|---|---|---|
| id / uid | bigint / uuid | 否 | PK / 對外識別 |
| tenant_id | integer | 否 | soft ref |
| job_type | varchar(40) | 否 | 8 種(見 §6.3 sync-jobs 回應欄位表) |
| payload | jsonb | 否 | 預設 '{}';handler 執行所需上下文(project_uid / scope_uid 等,依 job_type 而異) |
| status | varchar(20) | 否 | 預設 'PENDING';5 態見 §6.3 |
| priority | integer | 否 | 預設 100,數字愈小優先權愈高(worker 排程依據) |
| retry_count / max_retries | integer | 否 | 預設 0 / 5 |
| next_run_at | timestamptz | 否 | 預設 now();worker 輪詢的排程時間點 |
| last_error | text | 是 | 最近一次失敗訊息 |
| started_at / finished_at | timestamptz | 是 | 執行區間 |
索引:idx_drive_sync_jobs_pending(tenant_id, status, next_run_at WHERE status='PENDING',worker 輪詢用)、idx_drive_sync_jobs_status(status, created_at DESC,本頁列表查詢用)。
10. 頁面邏輯與資料對應¶
載入時序:頁面掛載 → onMounted(refresh) → GET /integrations/google-drive → 依 status 切換卡片內容;「⋯」選單的同步紀錄 dialog 用 watch(dialogVisible) 觸發,開啟才打 GET /sync-jobs(非預先載入)。
關鍵欄位對應(API ↔ 畫面):
| 畫面元素 | FE state / computed | API 欄位 |
|---|---|---|
| 卡片顯示分支 | isConnected / isError(status.status) |
status |
| 帳號 email | 直接綁定 | google_account_email |
| 連線時間+操作人 | new Date(...).toLocaleString() + by {name} |
connected_at / connected_by_user_name |
| 根資料夾連結 | driveRootUrl(computed,root_folder_id 為 null 時不顯示整列) |
root_folder_id |
| 最後同步時間 | 條件渲染(last_sync_at 為 null 不顯示) |
last_sync_at |
| 「⋯」選單「重建」項目可見性 | hasCap('cloud_integration.update')(CM-1412 改;原為 userInfo.value.is_admin,會把租戶管理員判成非管理員) |
— |
| 同步 job 表格列 | jobs(fetchJobs() 容錯兩種 shape:陣列 or {data:[...]}) |
sync-jobs response data[] |
| Job 重試按鈕可見性 | data.status === 'FAILED' |
status |
| 重建 dialog 確認鈕啟用 | rebuildConfirmEnabled(輸入框內容 trim 後等於 'REBUILD') |
— |
儲存流程:連接/中斷/重建皆是「呼叫 API → 等回應 → toast → refresh() 重打狀態」的一次性動作,無表單暫存或草稿概念。OAuth 連接特殊之處在於用 popup + postMessage 而非直接導頁,避免使用者離開設定頁上下文。
錯誤對應:中斷/重建/重試失敗走 toast 顯示 e?.response?.data?.msg ?? e?.message;狀態讀取失敗(401/404,未連線的正常情形)刻意只 console.debug,不顯示 toast 干擾(GoogleDriveIntegrationCard.vue:58-63 註解說明)。常見碼:GRC_NOT_ADMIN(403,非 admin 操作寫入)、GRC_DRIVE_OAUTH_STATE_INVALID(400,OAuth state 過期或重放)、GRC_DRIVE_OAUTH_EXCHANGE_FAILED / GRC_DRIVE_USERINFO_FETCH_FAILED(400,OAuth 交換失敗)、GRC_DRIVE_OAUTH_NO_REFRESH_TOKEN(400,Google 未回 refresh token,常見於未強制 prompt=consent 的重複授權)。
11. 背景行為與外部依賴¶
| 類型 | 內容 |
|---|---|
| 背景 worker | app/cloud_integration/service/drive_sync_worker.py::DriveSyncWorker——由 APScheduler 週期呼叫 run_once(),消費 drive_sync_jobs 佇列(PENDING → 依 job_type 分派給對應 handler,如 create_folder_handler / process_drive_changes_handler 等 8 種)。完整排程與 handler 細節屬背景服務 spec 範疇(本專案目前尚無獨立 background-services spec,暫以此檔案路徑為權威出處)本頁只負責顯示 job 記錄與手動重試 |
| Webhook | Google Drive Push Notification → POST /webhooks/google-drive/{tenant_id}(無 JWT,X-Goog-Channel-Token 驗證)→ enqueue PROCESS_DRIVE_CHANGES job;channel 由「連接」流程或「Webhook 手動註冊」API 建立,7 天到期需重新註冊 |
| jedi-* 套件 | jedi_common(@transaction、get_user_context);per-project 驗證修復另依賴 jedi_oscal_v2(透過 ProjectTreeLoader)與 jedi_project(ProjectDomainService) |
| 加密 | infra/cloud_integration/crypto/fernet_crypto.py(FernetCrypto)——refresh token / access token 存 DB 前一律加密,key 來自 config.DRIVE_TOKEN_ENCRYPTION_KEY(見 .env / 部署文件) |
| Redis | OAuth state 一次性 token(drive_oauth_state:<state>,TTL 600 秒)——非本頁常駐依賴,僅連接流程短暫使用 |
| 系統參數 | GOOGLE_DRIVE_OAUTH_CLIENT_ID / _CLIENT_SECRET / _REDIRECT_URI、DRIVE_WEBHOOK_PUBLIC_BASE_URL、DRIVE_TOKEN_ENCRYPTION_KEY、DRIVE_FILE_SIZE_LIMIT_MB(皆見 di_containers/cloud_integration/cloud_integration_containers.py:99-151,實值見部署文件) |
| Socket | 無 |
12. 邊界情況與已知坑¶
- 本頁 UI 只涵蓋租戶層操作,per-project 操作分散在別處:
init-folders(ProjectListView.vue「同步至 Drive」按鈕)與verify-and-repair-folders(ProjectCloudIntegrationsPanel.vue,掛在專案規劃頁 Tab5)都是同一api/cloud_integration模組的 endpoint,但 FE 入口不在本頁——查文件或抓 bug 時注意「Drive 相關功能」不會全部集中在/settings/cloud-integrations - OAuth callback 無角色檢查:
GET /callback沒有@jwt_required()(Google 重定向瀏覽器過來時沒有帶 JWT),身分驗證完全依賴 Redis 一次性statetoken(TTL 600 秒,用畢即刪);state內編碼了tenant_id:user_id,若 state 外洩理論上可被重放但視窗僅 10 分鐘且一次性 - ~~
disconnect允許 super admin 繞過 tenant admin 檢查~~(CM-1412 已移除):該_is_super_admin旁路與傳入的is_admin判的是同一件事(同一判定寫兩遍),已隨守門改能力點一併拆掉。現在 disconnect 與其他寫入操作一致,只認is_admin trigger_init_project_folders刻意不做角色檢查:docstring 明載是為了避免誤擋「非租戶管理員但是專案 manager」的正常操作者;純靠@jwt_required()+ FE 隱藏按鈕(CM-1412 改能力點時刻意維持現狀不掛,見 §3)。新增類似「觸發 orchestration」的方法不要無條件比照,需重新評估- 重建租戶資料夾是不可逆操作:清空
drive_folder_mappings後,舊 Drive 帳號裡實際的資料夾檔案不會被刪除,只是系統端拿掉追蹤關係,變成孤兒資料夾;FE 用 type-to-confirm(需打字輸入REBUILD)降低誤觸風險,但沒有復原機制 - 帳號切換(reconnect 不同 Google 帳號)會清空全租戶資料夾對應:
handle_callback偵測到google_account_email與既有連線不同時,直接wipe_all_for_tenant(因為 Drive 權限是 per-account,舊資料夾新帳號存取不到);同帳號重新授權(token 過期後 re-auth)則保留root_folder_id,不會誤清空 GET /sync-jobsresponse 沒有外層status鍵,只有data/meta:與本頁其他 endpoint(皆為{"status": true, "data": ...})不同,是沿用return_response對分頁 meta 的特殊處理(同docs/specs/v1.8.0/project-management/_overview.md提到的頂層 meta key 慣例);FEfetchJobs()為此寫了雙 shape 容錯(陣列 or{data:[...]})- Webhook register 無 FE 入口:
POST /webhook/register只存在於 API 層,目前僅供維運人員手動呼叫(例如舊租戶在 Phase 3 上線前就已連線、或啟動一個 Drive 斷線期間建立的專案);文件與 code 都沒有 FE 按鈕,未來若要曝光給 UI 需另外設計操作入口與風險提示
13. 開發與驗證¶
- 跑起來:BE
python main_socketio.py(port 8000,log 在log/app.log);FE 在compliance-manager-fe/起 Vite dev server。BE 改 service code 後必須重啟(無 hot reload) - 測試帳號:dev 環境 tenant admin 角色帳號(密碼見
.env/ 部署文件);Google OAuth 需設定GOOGLE_DRIVE_OAUTH_CLIENT_ID等環境變數才能實際跑通連接流程 - 導航路徑:登入 → 側邊選單「雲端空間整合」(
cloud-integrations) - 前置資料:租戶需有 admin 角色使用者;若要測試 per-project 相關功能需另外進專案規劃頁 Tab5 或專案清單頁
- E2E:
compliance-manager-test/repo(Cucumber + Playwright);本頁相關 feature 檔以雲端整合/cloud-integration/google-drive搜尋 - 相關文件:FR-016(
docs/features/FR-016-2604-google-drive-sync/,含三個 phase 的 implementation plan:OAuth → 資料夾 → 同步);背景 worker 排程細節目前僅見程式碼(drive_sync_worker.py),尚無獨立 spec;DB / API 全量見 GAI-SD-02/03