手冊首頁 / 系統管理 / 雲端整合

雲端整合(Cloud Integrations)

功能群:證據管理|儲存後端與證據來源共用概念先讀 功能群總覽

事實基準:2026-07-05 從 FE / BE code 掃出(FR-016 Google Drive sync 三階段功能;API schema 逐條對照 marshmallow serializer 與 service 組裝碼)

1. 功能描述

雲端整合頁是租戶層級(非單一專案)的 Google Drive 連線管理頁:tenant admin 在此完成 OAuth 授權、檢視連線狀態、查看背景同步 job 歷史,以及在資料夾結構損壞時觸發全租戶重建。連線建立後,Drive 成為專案資料夾結構與證據檔案的雲端來源(見 _overview §4);per-project 的資料夾初始化 / 修復操作則分散在專案清單頁與專案規劃頁 Tab5(見 §1.1 註記),不在本頁。

主要使用者:tenant admin(is_admin 角色)。一般使用者可讀取連線狀態(頁面可進入),但寫入操作(連接/中斷/重建)皆鎖 admin。

角色速覽(完整定義見 _overview §5;本頁的角色基調與其餘系統/租戶層頁面一致):

角色 一句話
tenant admin(is_admin 租戶管理者 — 本頁全部寫入操作(連接/中斷/重建)的唯一授權角色
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;本頁的角色基調與其餘系統/租戶層頁面一致):

角色 一句話
tenant admin(is_admin 租戶管理者 — 本頁全部寫入操作(連接/中斷/重建)的唯一授權角色
super admin 系統層級管理者 — disconnect 額外放行的第二種身分(見 §3)
一般使用者 可進頁、可讀連線狀態,無任何寫入按鈕會出現作用(BE 仍會擋)

流程圖視覺慣例:菱形 = 判斷、橘底 = 例外/擋下、紅框 = 錯誤(標 error code)、綠框 = 成功終點、虛線 = 可選路徑。

UC-CI-01 連接/重新授權 Google Drive

項目 內容
角色 tenant admin
前置條件 已登入;is_admin=true
產出 / 後置條件 tenant_drive_integrations 新增或更新一列(status=CONNECTED,token 加密存放);同 transaction 內註冊 Drive Push channel;best-effort 為斷線期間新建的專案補建資料夾
UcConnect start 使用者點「連接 Google Drive」 is_admin BE 檢查:使用者是 tenant admin? start->is_admin err_403 403 GRC_NOT_ADMIN is_admin->err_403 auth_url POST /auth-url 產生 state(Redis TTL 600s)+ Google 授權 URL is_admin->auth_url popup FE 開 popup 導向 Google 同意畫面 auth_url->popup user_grant 使用者同意?(取消/逾時走例外分支) popup->user_grant cancel popup 關閉或 5 分鐘逾時 → FE 顯示取消/逾時提示 user_grant->cancel callback GET /callback:驗證 state → 換 token → 查 userinfo user_grant->callback callback_fail state 失效/token 交換失敗/ 無 refresh_token?(三種 400 錯誤共用此分支) callback->callback_fail err_callback 400 GRC_DRIVE_OAUTH_STATE_INVALID / _EXCHANGE_FAILED / _NO_REFRESH_TOKEN callback_fail->err_callback account_swap 與既有連線 email 不同?(換帳號) callback_fail->account_swap wipe 清空該 tenant 全部 drive_folder_mappings account_swap->wipe save 同一 transaction:加密寫入 tenant_drive_integrations(status=CONNECTED) → 註冊 Drive Push channel → best-effort 補 enqueue 未同步 project account_swap->save wipe->save html 回傳 HTML:postMessage(opener)+自動關閉 popup save->html fe_toast FE 驗證 origin/source → toast → refresh() 重打狀態 API html->fe_toast
UC-CI-01 流程:admin 檢查→OAuth popup→callback 換 token→(換帳號才清空舊資料夾對應)→寫入+註冊 webhook+backfill→postMessage 通知 FE

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。

UcDisconnect start 使用者點「中斷連線」 is_admin BE 檢查:tenant admin 或 super admin? start->is_admin err_403 403 無權限 is_admin->err_403 s1 ① webhook_channel_manager.stop_for_tenant 停 Drive Push channel is_admin->s1 s2 ② HTTP 向 Google 撤銷 refresh token s1->s2 best-effort:失敗只 log,不擋下一步 s3 ③ _domain.disconnect(tenant_id) 清空 DB 欄位 → status=DISCONNECTED (實際生效的一步) s2->s3 best-effort:失敗只 log,不擋下一步 done FE toast → 重打狀態 API s3->done
UC-CI-02 流程:檢權→① 停 Drive Push channel→② 撤銷 Google refresh token(前兩步 best-effort,失敗只 log 不擋)→③ 清空 DB 回 DISCONNECTED(實際生效)

UC-CI-03 重建整個租戶 Drive 資料夾結構(destructive)

項目 內容
角色 tenant admin
前置條件 已登入;is_admin=true;FE 要求輸入關鍵字 REBUILD 才能按下確認鈕
產出 / 後置條件 該租戶全部 drive_folder_mappings 被清空;tenant_drive_integrations.root_folder_id 重置為 null;每個現存專案重新 enqueue INIT_PROJECT_FOLDERS job
CloudIntegrationsUiLayout page 雲端整合頁(/settings/cloud-integrations) 頁首:標題「雲端空間整合」 GoogleDriveIntegrationCard Header:Google icon+標題/說明+狀態 Tag+「⋯」更多選單 未連線態:警語+「連接 Google Drive」按鈕 連線異常態(REVOKED/EXPIRED):紅色 Message+「重新授權」按鈕 已連線態:帳號 email/連線時間+操作人/根資料夾連結/最後同步時間(dl/dt/dd) 已連線態右下角:「中斷連線」按鈕(danger,唯一常駐次要操作) more_menu 「⋯」更多選單(Menu popup) 查看同步紀錄/重新授權/ 重建整個租戶資料夾(限 admin) page->more_menu 開啟 sync_dialog SyncJobHistoryDialog (同步 job 歷史表格+篩選+重試) more_menu->sync_dialog 查看同步紀錄 rebuild_dialog Rebuild-all 確認 Dialog (type-to-confirm:輸入 REBUILD) more_menu->rebuild_dialog 重建租戶資料夾(admin)
UC-CI-03 流程:admin 檢查→FE type-to-confirm→BE 清空 mapping+重置 root_folder_id→為全部專案重新 enqueue init job

上圖沿用版面骨架圖示意「⋯」選單 → 重建 dialog 的觸發路徑;重建本身無分支例外(僅 is_admin 一道門),故不另畫獨立流程圖。

UC-CI-04 查看同步紀錄並手動重試 FAILED job

項目 內容
角色 tenant admin(is_admin
前置條件 已登入;is_admin=true;欲重試者須為 FAILED 狀態的 job
產出 / 後置條件 FAILED job 重置為 PENDING(等下一輪背景 worker 輪詢重跑,非即時);不改動其他 job
UcSyncRetry start admin 開「⋯」→ 同步紀錄 dialog list GET /sync-jobs?status=&page=&page_size= 分頁列出 drive_sync_jobs(可依狀態篩選) start->list is_admin BE 檢查:is_admin? list->is_admin err_403 403 GRC_NOT_ADMIN is_admin->err_403 show DataTable 顯示 job 列 (FAILED 列才出現「重試」按鈕) is_admin->show click 點 FAILED job 的「重試」 show->click retry_admin BE 檢查:is_admin? click->retry_admin retry_admin->err_403 reset POST /sync-jobs/{uid}/retry 將該 FAILED job 重置為 PENDING (等下一輪 worker 輪詢,不即時重跑) retry_admin->reset done 回 {retried: true} → FE 重載列表 reset->done
UC-CI-04 流程:admin 開同步紀錄 dialog→GET /sync-jobs 分頁列表(可依狀態篩選)→僅 FAILED 列顯示重試→POST /sync-jobs/{uid}/retry 重置為 PENDING 等 worker 輪詢

UC-CI-05 手動補註冊 Webhook 並 backfill(維運操作,無 FE 入口)

項目 內容
角色 tenant admin(is_admin);route 層直接檢查
前置條件 已登入;is_admin=true;典型情境為 Phase 3 上線前就已連線、或斷線期間新建專案的舊租戶
產出 / 後置條件 註冊一個新 Drive Push channel(idempotent,已有 live channel 也不報錯);為未同步專案 backfill 補 enqueue init(略過已有 PROJECT scope 對應的專案)
UcWebhookRegister start 維運人員呼叫 API(無 FE 入口) call POST /webhook/register start->call is_admin route 層檢查:is_admin? call->is_admin err_403 403 GRC_NOT_ADMIN is_admin->err_403 register 註冊一個新 Drive Push channel (idempotent:已有 live channel 也不報錯) is_admin->register backfill backfill:為未同步專案補 enqueue init (略過已有 PROJECT scope 對應的專案) register->backfill done 回 {registered: true, backfilled_projects: n} backfill->done
UC-CI-05 流程:維運呼叫 POST /webhook/register→route 層檢 is_admin→註冊新 Push channel(idempotent)→backfill 未同步專案 init→回註冊與 backfill 筆數

3. 權限矩陣

操作 FE 判定 BE 強制 BE 檢查位置
讀連線狀態(進頁/卡片顯示) 無(requiresAuth: true 僅要求登入) @jwt_required(),無角色檢查 api/cloud_integration/routes/google_drive_integration_route.py:26-36GET
產生 OAuth 授權 URL 無額外判定(按鈕永遠顯示於未連線態) is_admin → 否則 GRC_NOT_ADMIN(403) app/cloud_integration/service/google_drive_integration_service.py:64-67
中斷連線 無額外判定(按鈕永遠顯示於已連線態) is_admin is_super_admin(兩者其一即可) 同檔 :187-190
查看同步 job 歷史 無額外判定 is_admin → 否則 GRC_NOT_ADMIN app/cloud_integration/service/drive_sync_admin_service.py:98-99
手動重試 sync job 無額外判定 is_admin → 否則 GRC_NOT_ADMIN 同檔 :110-111
重建整個租戶資料夾 「⋯」選單項目 v-if="isAdmin" 才出現(userInfo.is_admin is_admin → 否則 GRC_NOT_ADMIN 同檔 :157-158
Per-project 初始化資料夾 無角色判定(ProjectListView.vue 無角色檢查(僅 @jwt_required(),刻意設計避免誤擋非 admin 的專案 manager) app/cloud_integration/service/drive_sync_admin_service.py:126-129(docstring 明載設計理由)
Per-project 驗證與修復 canEdit(manager 才可點) 專案 manager(project_participant.role == "manager")→ 否則 GRC_NOT_MANAGER app/cloud_integration/service/drive_project_verify_service.py:122-130
Webhook 手動註冊+backfill 無 FE 入口 is_admin → 否則 GRC_NOT_ADMIN api/cloud_integration/routes/google_drive_sync_route.py:205-206(route 層直接檢查,非 service 層)
OAuth callback(Google 導回) 無角色檢查(state 一次性 Redis token 即身分綁定,見 §12 坑 2) api/cloud_integration/routes/google_drive_integration_route.py:69-96
Webhook 事件接收 無 JWT;以 X-Goog-Channel-Token 比對取代 api/cloud_integration/routes/google_drive_webhook_route.py

權限模型與其他系統層頁面一致:本頁是「租戶層級」而非「專案層級」,因此用的是 UserContextDTO.is_admin(tenant admin JWT claim)而非專案 project_participants.role;唯一例外是 per-project 驗證與修復(§1.1 #9)沿用專案層 manager 檢查,因為那是掛在專案規劃頁 Tab5 下的操作。

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
userInfo.is_admin 為 false 「⋯」選單不出現「重建整個租戶資料夾」項目 同檔 :208-214
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 僅標代表性):

頁首標題:雲端整合(租戶層,單卡片頁,無左樹/無 Tab/無分頁列表) 跳至 §2 Use Case(UC-CI-01 連接/重新授權、UC-CI-02 中斷連線) GoogleDriveIntegrationCard — 四態切換 DISCONNECTED(未連線) 「連接 Google Drive」→ OAuth popup CONNECTED(已連線) 帳號/連線時間/根資料夾/最後同步 REVOKED(已撤銷) 醒目 Message:重新授權 EXPIRED(已過期) 醒目 Message:重新授權 GET /integrations/google-drive ・ POST …/auth-url DELETE /integrations/google-drive(中斷,admin/super admin) → 點我跳 §2 UC-CI-01/02 卡片右上「⋯」選單 重新授權/查看同步紀錄/重建租戶資料夾(admin) 跳至 §2 Use Case(UC-CI-04 同步 job 歷史列表與 FAILED 手動重試) SyncJobHistoryDialog drive_sync_jobs 分頁列表+狀態篩選+重試 GET …/sync-jobs POST …/sync-jobs/{uid}/retry → §2 UC-CI-04 跳至 §2 Use Case(UC-CI-03 重建整個租戶資料夾,type-to-confirm) 重建租戶資料夾確認 dialog Destructive;type-to-confirm 輸入 REBUILD POST …/tenant/rebuild-all (限 admin) → §2 UC-CI-03 彩色=可點跳本頁小節錨點(框內標錨點+代表性 API)|灰=本頁描述|虛線框=「⋯」選單開出的 dialog 層 API 僅列代表性 2~3 條,完整規格見 §6
雲端整合頁版面 wireframe:頁首標題、GoogleDriveIntegrationCard(四態切換)、「⋯」選單開出的同步紀錄 dialog 與重建租戶資料夾 dialog

狀態呈現:讀取中用 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-19GoogleDriveStatusResponse);組裝 app/cloud_integration/service/google_drive_integration_service.py:170-176get_status)+ app/cloud_integration/dto/google_drive_integration_dto.py:24-54GoogleDriveIntegrationStatusDTO.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_urlREQUIRED_SCOPESdrive + 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-22DriveSyncJobListQuerySchema)。

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-16DriveSyncJobResponse);組裝 api/cloud_integration/routes/google_drive_sync_route.py:38-75app/cloud_integration/service/drive_sync_admin_service.py:82-105list_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.pyGoogleDriveIntegrationRoute GET/DELETE、GoogleDriveAuthUrlRouteGoogleDriveCallbackRoute app/cloud_integration/service/google_drive_integration_service.py::GoogleDriveIntegrationServiceget_status / build_auth_url / handle_callback / disconnect TenantDriveIntegrationDomainServiceTenantDriveIntegrationRepoImplinfra/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.pyDriveSyncJobsRoute / 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_jobsPROCESS_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 圖

CloudEr tenant_drive_integrations public.tenant_drive_integrations (1 租戶 1 列,tenant_id 唯一) id / uid PK tenant_id UNIQUE provider 預設 google_drive google_account_email refresh_token_encrypted Fernet 加密 access_token_cache / _expires_at Fernet 加密 root_folder_id GuidantAI/ 根資料夾 drive_change_cursor Drive Changes API 游標 webhook_channel_id / _resource_id / _token / _expires_at Push 頻道 status CONNECTED/EXPIRED/REVOKED/DISCONNECTED connected_by_user_id / connected_at last_sync_at / last_sync_error drive_folder_mappings public.drive_folder_mappings id / uid PK tenant_id soft ref scope_type ROOT/PROJECT/AP/CONTROL_GROUP/\lCONTROL/AO/TASK/ARCHIVE/EVIDENCES scope_uid soft ref,依 scope_type 指向的實體 uid parent_drive_folder_id / drive_folder_id UNIQUE(drive_folder_id) display_name_snapshot 建立當下名稱快照 is_unlinked verify 發現失聯時標記 tenant_drive_integrations->drive_folder_mappings soft ref(tenant_id) 1 租戶 : N 資料夾對應 drive_sync_jobs public.drive_sync_jobs id / uid PK tenant_id soft ref job_type INIT_PROJECT_FOLDERS 等 8 種 payload jsonb,handler 執行上下文 status PENDING/IN_PROGRESS/SUCCESS/\lFAILED/SKIPPED priority / retry_count / max_retries 預設 100 / 0 / 5 next_run_at worker 排程輪詢依據 last_error / started_at / finished_at tenant_drive_integrations->drive_sync_jobs soft ref(tenant_id) 1 租戶 : N 同步 job drive_folder_mappings->drive_sync_jobs payload 內以 scope_uid 交叉參照 (非 FK,執行期解析)
Drive 整合三表關係:tenant_drive_integrations 為租戶連線主檔(1:1 tenant);drive_folder_mappings 與 drive_sync_jobs 皆以 tenant_id soft ref 掛靠,彼此透過 payload/scope_uid 執行期交叉參照,非 FK

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_ididx_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/EVIDENCESdomain/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) UNIQUEdrive_folder_mappings_drive_folder_id_key
display_name_snapshot varchar(255) 建立當下的資料夾顯示名稱快照
is_unlinked boolean 預設 false;verify-and-repair 發現 Drive 端已刪除/無權限時標記為 true

索引:uq_drive_folder_mappings_roottenant_id, scope_type WHERE scope_type='ROOT',確保每租戶僅 1 個根節點)、uq_drive_folder_mappings_scopetenant_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_pendingtenant_id, status, next_run_at WHERE status='PENDING',worker 輪詢用)、idx_drive_sync_jobs_statusstatus, created_at DESC,本頁列表查詢用)。

10. 頁面邏輯與資料對應

載入時序:頁面掛載 → onMounted(refresh)GET /integrations/google-drive → 依 status 切換卡片內容;「⋯」選單的同步紀錄 dialog 用 watch(dialogVisible) 觸發,開啟才打 GET /sync-jobs(非預先載入)。

關鍵欄位對應(API ↔︎ 畫面):

畫面元素 FE state / computed API 欄位
卡片顯示分支 isConnected / isErrorstatus.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
「⋯」選單「重建」項目可見性 isAdminuserInfo.value.is_admin,非本頁 API 回傳,來自登入時的使用者資訊)
同步 job 表格列 jobsfetchJobs() 容錯兩種 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@transactionget_user_context);per-project 驗證修復另依賴 jedi_oscal_v2(透過 ProjectTreeLoader)與 jedi_projectProjectDomainService
加密 infra/cloud_integration/crypto/fernet_crypto.pyFernetCrypto)——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_URIDRIVE_WEBHOOK_PUBLIC_BASE_URLDRIVE_TOKEN_ENCRYPTION_KEYDRIVE_FILE_SIZE_LIMIT_MB(皆見 di_containers/cloud_integration/cloud_integration_containers.py:99-151,實值見部署文件)
Socket

12. 邊界情況與已知坑

  1. 本頁 UI 只涵蓋租戶層操作,per-project 操作分散在別處init-foldersProjectListView.vue「同步至 Drive」按鈕)與 verify-and-repair-foldersProjectCloudIntegrationsPanel.vue,掛在專案規劃頁 Tab5)都是同一 api/cloud_integration 模組的 endpoint,但 FE 入口不在本頁——查文件或抓 bug 時注意「Drive 相關功能」不會全部集中在 /settings/cloud-integrations
  2. OAuth callback 無角色檢查GET /callback 沒有 @jwt_required()(Google 重定向瀏覽器過來時沒有帶 JWT),身分驗證完全依賴 Redis 一次性 state token(TTL 600 秒,用畢即刪);state 內編碼了 tenant_id:user_id,若 state 外洩理論上可被重放但視窗僅 10 分鐘且一次性
  3. disconnect 允許 super admin 繞過 tenant admin 檢查is_admin 為 false 時還會額外查 user.is_super_admin(系統層級),這是本模組唯一有「雙重身分」判定的操作,其餘寫入 API(connect / rebuild / retry)僅認 is_admin
  4. trigger_init_project_folders 刻意不做角色檢查:docstring 明載是為了避免用 is_admin 誤擋「非 tenant-admin 但是專案 manager」的正常操作者;純靠 @jwt_required() + FE 隱藏按鈕。新增類似「觸發 orchestration」的方法不要無條件比照,需重新評估
  5. 重建租戶資料夾是不可逆操作:清空 drive_folder_mappings 後,舊 Drive 帳號裡實際的資料夾檔案不會被刪除,只是系統端拿掉追蹤關係,變成孤兒資料夾;FE 用 type-to-confirm(需打字輸入 REBUILD)降低誤觸風險,但沒有復原機制
  6. 帳號切換(reconnect 不同 Google 帳號)會清空全租戶資料夾對應handle_callback 偵測到 google_account_email 與既有連線不同時,直接 wipe_all_for_tenant(因為 Drive 權限是 per-account,舊資料夾新帳號存取不到);同帳號重新授權(token 過期後 re-auth)則保留 root_folder_id,不會誤清空
  7. GET /sync-jobs response 沒有外層 status 鍵,只有 data/meta:與本頁其他 endpoint(皆為 {"status": true, "data": ...})不同,是沿用 return_response 對分頁 meta 的特殊處理(同 docs/specs/v1.8.0/project-management/_overview.md 提到的頂層 meta key 慣例);FE fetchJobs() 為此寫了雙 shape 容錯(陣列 or {data:[...]}
  8. 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 或專案清單頁
  • E2Ecompliance-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