操作日誌(User Log)
功能群:系統管理|三層 RBAC 權限模型與角色權限基調先讀 功能群總覽
事實基準:2026-07-05 從 FE / BE code 掃出(BE 核心邏輯位於外部套件
jedi-api-log,主專案為 route + 全域 middleware 薄殼);API schema 逐條對照 marshmallow serializer 與 service 組裝碼
1. 功能描述
操作日誌頁是系統管理群「稽核與個人」分類下的唯讀查詢頁:檢視
public.api_logs 這張「每一次 API
請求」的稽核軌跡(誰、何時、打了哪個 URL、帶什麼
payload、回應耗時等)。本頁不寫入任何資料——api_logs
的寫入完全由全域 Flask
middleware(common/middleware/app_mw.py)在每個請求的
before_request/after_request/teardown_request
自動完成,與本頁的使用者操作無關。頁面本身只有一張 DataTable + 全域搜尋
+ 匯出,無左側樹、無 Tab、無新增/編輯/刪除。
主要使用者:系統管理層使用者(頁面可見性由 RBAC 選單控管,見 §3)。
角色速覽:
| 角色 | 一句話 |
|---|---|
| 系統管理層使用者 | 本頁唯一操作者——選單可見性由 RBAC 控管(見 _overview §2),BE 查詢端點僅檢查 JWT 登入;匯出端點連 JWT 都未檢查(見 §3) |
⚠️ 本頁只查
public.api_logs,不查public.system_logs:FEUserLogs.vue唯一呼叫的資料來源是POST /log/api-logs;同目錄下UserLogService.js(fetch('/demo/data/logs.json'))是死碼,未被UserLogs.vue或任何其他檔案 import。system_logs是另一條完全獨立的寫入管道(jedi-common的DBLogHandler,掛在 Python logging 層,於 WARNING 以上事件寫入event_code),本頁沒有對應的查詢 UI,見 §9。
1.1 功能總覽(本頁全部功能)
| # | 功能 | 說明 | 位置 | 詳述 |
|---|---|---|---|---|
| 1 | 日誌清單(分頁/排序) | 9 欄 DataTable,lazy 分頁(25/50/100)+ 多欄排序;per-user
查詢狀態持久化(useTableQuery scope) |
主表格 | UC-UL-01 |
| 2 | 全域關鍵字搜尋 | 單一搜尋框,同一關鍵字套用到 7
個欄位(user_name/level/request/source_ip/server_ip/method/url) |
Header 搜尋框 | UC-UL-01 / §12 坑 1 |
| 3 | 重置查詢 | 清空搜尋/排序/分頁回到第一頁 | Header「重置查詢」按鈕 | §10 |
| 4 | Payload 密碼遮罩(顯示層) | 若 request 欄位可解析為 JSON 且含 password
key,顯示前於 FE 再次遮罩為 *** |
清單「Payload」欄 | §10 |
| 5 | 匯出日誌(zip/xlsx) | 匯出目前條件未套用的全量 api_logs 為
xlsx,包成 zip 下載 |
Header「匯出」按鈕 | UC-UL-02 / §12 坑 1 |
UC(§2)展開 1(查詢/篩選/排序)、2(匯出)兩條核心操作路徑;其餘(重置查詢、密碼遮罩顯示)屬單點行為,不另展開。
2. Use Case
角色速覽:
| 角色 | 一句話 |
|---|---|
| 系統管理層使用者 | 本頁唯一操作者——選單可見性由 RBAC 控管(見 _overview §2),BE 查詢端點僅檢查 JWT 登入;匯出端點連 JWT 都未檢查(見 §3) |
流程圖視覺慣例:菱形 = 判斷、橘底 = 例外/擋下、紅框 = 錯誤(標 error code)、綠框 = 成功終點、虛線 = 可選路徑。
UC-UL-01 查詢/篩選/排序日誌
| 項目 | 內容 |
|---|---|
| 角色 | 系統管理層使用者(頁面可見性由 RBAC 選單控管) |
| 前置條件 | 已登入;user-log 選單可見 |
| 產出 / 後置條件 | 無資料異動(唯讀);DataTable / pager.total 更新 |
UC-UL-02 匯出日誌
| 項目 | 內容 |
|---|---|
| 角色 | 名義上系統管理層使用者;實際上任何人(含未登入者)皆可呼叫(見下) |
| 前置條件 | 無(端點未強制登入) |
| 產出 / 後置條件 | 下載 zip 檔(內含 xlsx);無資料異動 |
3. 權限矩陣
頁面可見性依 RBAC 三層模型基調,見 _overview §2。本頁端點層的實際檢查點:
| 操作 | FE 判定 | BE 強制 | BE 檢查位置 |
|---|---|---|---|
| 選單 / 頁面可見性 | RBAC 選單資料決定(route meta 無 capability 欄位,純靠
ui_routes/capabilities 回傳結果) |
不適用(選單層問題,非本頁端點) | jedi-auth web-menu(見 _overview
§2.1) |
查詢清單 POST /log/api-logs |
無額外前端判斷 | ⚠️ 僅
@jwt_required(),無角色/租戶檢查;api_logs
表本身無 tenant_id/org_unit_id
欄位,任何租戶的登入使用者皆可查到全租戶的 API
呼叫紀錄 |
api/log/routes/api_log_route.py:24(ApiLogsRoute.post) |
匯出 GET /log/api-logs/export |
匯出按鈕對所有可見本頁的使用者皆顯示 | 🔴 完全無認證檢查——@jwt_required()
該行被註解掉(api/log/routes/api_log_route.py:51),任何人(含未登入、無
JWT)直接呼叫此 URL 即可下載全部 api_logs |
api/log/routes/api_log_route.py:48-51(ExportApiLogsRoute.get) |
🔴 已知安全漏洞(SEC-001,已有 regression test 記錄,見 §12 坑 1):
tests/test_security.py明確記錄ExportApiLogsRoute的@jwt_required()已被註解掉,並以pytest.xfail標記此為「預期會失敗」的已知 bug(test_SEC_001_no_jwt)。本頁 spec 僅如實記錄,不在此 session 修復(修正需另開 bug/issue session,走docs/issues/流程)。⚠️ 跨租戶可見性:即使是查詢端點(有
@jwt_required()),api_logs/system_logs兩表皆無tenant_id欄位、亦無 RLS 隔離(session_scope()注入的app.allowed_tenant_paths對這兩張表不生效,因為它們不受 RLS policy 涵蓋)。任何登入使用者查詢本頁,看到的是全系統、跨所有租戶的 API 呼叫紀錄,包含其他租戶使用者的操作內容與 request payload。這與_overview §3「RLS 做租戶隔離」的一般假設不同,是本頁特有的資料可見性坑。
4. 狀態機與前置條件
本頁無狀態機、無輪次/階段概念(日誌是唯讀稽核紀錄,非生命週期資源)。無欄位層級前置條件——查詢與匯出皆無條件可用(除登入外,且匯出連登入都不需要,見 §3)。
| 條件 | 效果 | 出處 |
|---|---|---|
| FE 未帶排序 | BE 強制補 created_at desc(避免預設 asc
排序把當日資料排到最後幾頁) |
api/log/routes/api_log_route.py:36-39 |
| 搜尋關鍵字為空 | buildPayload() 不帶
filters,等同查全部 |
useTableQuery.js:119-125(UserLogs.vue
呼叫端) |
| 匯出 | 不套用畫面上目前的搜尋/排序/分頁條件,永遠匯出全表(見 §12 坑 2) | api/log/routes/api_log_route.py:53-54(export_api_log_file()
無參數) |
5. UI 設計
📸 實際畫面截圖待補(一律亮色模式拍攝):以 playwright 對 STG 擷取,預計補以下兩張到
assets/img/: ① 整頁(搜尋框 + DataTable 9 欄 + 匯出按鈕)userlog-overview.png② Payload 欄 tooltip 特寫(顯示 password 遮罩效果)userlog-payload-tooltip.png
版面骨架(單欄:Header 搜尋/動作 + DataTable,無左側樹、無 Tab、無 Dialog):
狀態呈現:DataTable loading 遮罩載入中;匯出按鈕獨立
downloading loading 狀態,不阻塞整頁查詢;url
欄超長時 tooltip 顯示全文(無截斷樣式,欄寬用 max-w-*rem
class 限制);user_agent/request 欄以
text-overflow-ellipsis 截斷 + tooltip 顯示全文;無
socket,操作後皆重打 getUserLogs()
重新整理。查詢狀態(搜尋關鍵字/排序/分頁)以 useTableQuery
依 route scope + 使用者 uid 持久化於
queryStore,換使用者或重新登入會自動重新查詢(UserLogs.vue:51-55)。
6. API 規格
Envelope:成功
{"status": true, "data": [...], "meta": {...}}(return_response
組裝,見 common/util/response_util.py:4-16);失敗為
4xx/5xx 標準錯誤 envelope。
6.1 總清單(本頁涉及的全部 endpoint)
| 分類 | Method + Path | 說明 | 完整規格 |
|---|---|---|---|
| 查詢 | POST /log/api-logs |
分頁查詢(本頁唯一資料來源,payload={pager, sort, filters}) |
§6.2 |
| 匯出 | GET /log/api-logs/export |
匯出全量 api_logs 為 zip(內含 xlsx);🔴 無認證 |
§6.3 |
本頁僅 2 個 endpoint,皆非常規 CRUD,故 §6.2~6.3 展開完整規格(無 GAI-SD-02 常規 CRUD 段落可省略)。
6.2 查詢類
[POST] /log/api-logs
Request
payload(ApiLogPageQueryRequest):
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
| pager | object | 否 | {page, page_size, with_total}(PagerSchema,page
預設 1、page_size 預設 25) |
| sort | array | 否 | [{field, order}];省略時 BE 強制補
[{field: created_at, order: desc}] |
| filters | object | 否 | ApiLogQueryEntity
子集:user_name/level/request/source_ip/server_ip/method/url(FE
全域搜尋對這 7 欄各自帶同一關鍵字) |
Response data[](每項一筆 API 呼叫紀錄):
| 欄位 | 型別 | 說明 |
|---|---|---|
| id | int | 內部序號(複合 PK 之一,另一是 act_time) |
| act_time | datetime | 操作時間,YYYY-MM-DD HH:MM:SS(請求進入時的時間戳,非資料庫
created_at) |
| user_uid | string | 命名易混淆:實際存的是使用者
login_name(非 UUID),由 after_request
回填 |
| user_name | string | 使用者 nickname(after_request 從
get_user_context() 回填),未登入請求為空字串 |
| type | string | 日誌類型,固定為
"1"(LogTypeCode.API_LOG),本頁未使用此欄位篩選 |
| level | string | 日誌級別(INFO 等),寫入時固定
'INFO',update_api_log 不會改動此欄 |
| source_ip | string | 來源
IP(request.remote_addr);敏感欄位,本文件不列任何實際值 |
| server_ip | string | 伺服器 IP(request.host,含
port);敏感欄位,本文件不列任何實際值 |
| url | string | 完整請求 URL(含 querystring) |
| method | string | HTTP method |
| params | string | Query string(不含路徑) |
| request | string | Request body 原文(before_request 寫入時已用
mark_password() 遮罩 password key) |
| user_agent | string | User-Agent header |
| created_at | datetime | DB 列建立時間(分區鍵之一) |
| message | string | 一般請求為 request
path;發生例外時(teardown_request)覆寫為例外訊息字串 |
| response | string | Response body 原文(after_request
回填,未做遮罩——回應內容若含敏感欄位不會被 mask,見 §12 坑 3) |
| duration | float | 請求耗時(秒),after_request 計算
now() - g.start_time |
出處:api/log/serializers/api_log.py:20-40(ApiLogResponse
/ ApiLogPageQueryResponse);寫入端
common/middleware/app_mw.py:34-137。
6.3 匯出類
[GET] /log/api-logs/export
無 query 參數、無 body(不吃畫面上目前的搜尋/排序/分頁條件,永遠對全表匯出,見 §12 坑 2)。
Response:Content-Type: application/zip,Content-Disposition: attachment,檔名
user_log_<YYYYMMDDHHMMSS>.zip(內含同名
.xlsx,無 header 列,欄位順序同 §6.2
Response,但實際輸出欄位由 ApiLogExportDTO 決定)。
出處:api/log/routes/api_log_route.py:48-68(ExportApiLogsRoute.get)、jedi_api_log/app/service/api_log_service.py:53-76(export_api_log_file,get_all_by_fields
→ pandas → ExcelUtil.clean_invalid_characters → zip)。
7. 前端檔案地圖(compliance-manager-fe/)
| 檔案 | 角色 |
|---|---|
src/views/action-log/UserLogs.vue |
唯一檢視檔(約 200 行;DataTable + 全域搜尋 + 匯出,無子元件) |
src/config/router/index.js:236-243 |
route user-log,path
/log/user-log;meta 無 capability
欄位,可見性由 RBAC 選單資料決定 |
src/config/api/api.js:90-91 |
API_LOGS(/log/api-logs)/
EXPORT_API_LOGS(/log/api-logs/export) |
src/service/BaseService.js |
本頁查詢用
baseService.post(API.API_LOGS, payload);匯出改用裸
axios 直接打(因需
responseType: 'blob',BaseService
泛型不支援) |
src/service/UserLogService.js |
⚠️ 死碼:fetch('/demo/data/logs.json')
假資料服務,未被 UserLogs.vue 或任何其他檔案 import,見
§1 |
src/composables/useTableQuery.js |
共用分頁/排序/搜尋 state(per-scope + per-user
持久化);buildPayload()
是全域搜尋「同關鍵字套多欄」邏輯的來源 |
src/stores/queryStore.js |
useTableQuery 背後的 Pinia
store(resetCurrent() 供「重置查詢」使用) |
src/utils/userUtil.js |
useUserUtil() 取
userInfo(供使用者變更時觸發重新查詢) |
src/config/locales/i18n/{zh-tw,en}/pages.json |
頁面 i18n,namespace lang.user_logs.*(第 226-239
行) |
8. 後端檔案地圖(本頁核心鏈路)
| 鏈路 | Route | App Service | 底層 |
|---|---|---|---|
| 查詢清單 | api/log/routes/api_log_route.py::ApiLogsRoute.post |
jedi_api_log.app.service.api_log_service.ApiLogService::get_devices_and_pager(@transaction,方法名沿用自其他模組樣板、未改名) |
ApiLogDomainService.get_api_logs_and_pager →
ApiLogRepoImpl(繼承 BaseRepositoryImpl)→
_get_pager_list_query(OR-across-string-fields ILIKE,見
§12 坑 2) |
| 匯出 | api/log/routes/api_log_route.py::ExportApiLogsRoute.get |
ApiLogService::export_api_log_file(@transaction) |
ApiLogDomainService.get_all_by_fields →
ApiLogRepoImpl.get_all_by_fields(走
BaseRepositoryImpl._gen_filters,AND-only、多數欄位
==,見 §12 坑 2)→ pandas/openpyxl |
| 寫入(全域,非本頁觸發) | 無獨立 route;掛在 Flask app 生命週期鉤子 | common/middleware/app_mw.py::init_app_interceptor(before_request/after_request/teardown_request,三段式寫入) |
ApiLogService.add_api_log / get_api_log /
update_api_log → ApiLogRepoImpl.add /
.update(自訂 SQL,非 BaseRepositoryImpl
泛用版) |
DDD 提醒:本模組是「主專案薄殼 + 外部套件扛全部
domain/infra」的典型範例——domain/log/、infra/log/
在主專案不存在,全數在 jedi-api-log(pip
套件名,對應原始碼倉庫
jedi-log,~/Projects/Jedicogy/module/jedi-python-package/jedi-log/)。主專案僅有
route(HTTP 邊界)+ 全域 middleware(app_mw.py,寫入
api_logs
的唯一入口)。改日誌核心行為(欄位、過濾邏輯、匯出格式)要去
jedi-log 套件改,改動需遵守
CLAUDE.md「外部套件異動規範」。route 層無 DB 存取(守規範);但
common/middleware/app_mw.py
是全域基礎設施而非本頁專屬 app service,其
@transaction 語意由套件內 ApiLogService
方法自帶。
9. DB
9.1 資料表總清單(本頁讀寫的全部表)
| 表 | 讀/寫 | 說明 | 欄位詳述 |
|---|---|---|---|
| public.api_logs | 讀(本頁);寫(全域 middleware,非本頁操作觸發) | 每次 HTTP API
請求的稽核軌跡:時間/使用者/URL/method/request-response/耗時;月分區(RANGE
on act_time)+ BRIN 索引;retention 90
天 |
§9.3 |
| public.system_logs | 不讀不寫(本頁未查詢此表,見 §1) | event_code 事件日誌:由 jedi-common 的
DBLogHandler(Python logging Handler,WARNING
以上等級)寫入,與本頁 API 呼叫軌跡是完全獨立的管道;月分區 +
BRIN;retention 180 天 |
§9.3 |
api_logs vs system_logs 的本質區分:
api_logs是「每一次 HTTP 請求」的軌跡(method/url/request/response/duration),由 Flask 生命週期鉤子(before_request/after_request/teardown_request)逐請求寫入;system_logs是「系統事件」(event_code語意化事件,如稽核進度、階段推進),由應用程式碼呼叫logger.warning(...)等經DBLogHandler落地。兩者欄位形狀不同(system_logs多event_code/path/func_name/line_no,少source_ip/server_ip/request/response/duration),本頁只涵蓋前者。
9.2 ER 圖(本頁讀寫範圍)
9.3 核心表欄位(取自 DEV DB dump)
public.api_logs — API 日誌表(本頁資料來源)
DB 無表層 comment(db_schema.json 該表
comment 欄為 null;套件 ORM model 端有
comment「API日誌表:記錄API請求與回應的詳細資訊」但未同步灌回
DB)。已分區(partitioned: true,RANGE(act_time),月分區
+ DEFAULT 分區防溢出,見
scripts/sql/2026-06-03-log-tables-partitioning.sql)。複合主鍵
(id, act_time)(partition key 必入 PK,PostgreSQL
分區表限制)。
| 欄位 | 型別 | 說明 |
|---|---|---|
| id | integer | 序號(複合 PK 之一,api_logs_id_seq) |
| act_time | timestamp(NOT NULL,複合 PK 之一) | 操作時間,同時是分區鍵;有 BRIN 索引
ix_api_logs_act_time_brin |
| user_uid | varchar(36)|null | 命名為 uid 實存 login_name(見 §6.2) |
| user_name | varchar(500)|null | nickname |
| type | varchar(10)(NOT NULL) | 日誌類型碼(固定 "1" = API_LOG) |
| level | varchar(10)(NOT NULL) | 日誌級別 |
| source_ip | varchar(255)(NOT NULL) | 來源 IP;本文件僅描述欄位存在,不列任何實際值 |
| server_ip | varchar(255)(NOT NULL) | 伺服器 IP(含 port);本文件僅描述欄位存在,不列任何實際值 |
| url | varchar(500)(NOT NULL) | 完整請求 URL |
| method | varchar(10)(NOT NULL) | HTTP method |
| params | text|null | Query string |
| request | text|null | Request body(寫入時已 mask password) |
| response | text|null | Response body(未 mask,見 §12 坑 3) |
| message | text|null | 一般為 request path;例外時為錯誤訊息 |
| user_agent | varchar(5000)|null | User-Agent |
| duration | numeric|null | 請求耗時(秒),預設 0 |
| created_at | timestamp(NOT NULL) | DB 列建立時間 |
無
tenant_id/org_unit_id欄位、無 FK——本表刻意不綁租戶(見 §3 跨租戶可見性說明)。
public.system_logs — 系統事件日誌表(本頁不涉及,供對照)
同為已分區(月分區 + BRIN),複合 PK
(id, act_time),retention 180
天。核心欄位:event_code(varchar(10),事件碼)、path/func_name/line_no(呼叫點座標,DBLogHandler
從 LogRecord
取得)、message(事件訊息);沒有
source_ip/server_ip/request/response/duration(因為它記錄的是應用程式事件而非
HTTP 請求)。
10. 頁面邏輯與資料對應
載入時序:onMounted 呼叫
getUserLogs() → buildPayload() 組
{pager, sort, filters}(filters
僅在搜尋框有值時才附帶)→ POST /log/api-logs → 回填
logs.value 與
pager.value(Object.assign(pager.value, meta))。使用者
uid 變化(如延遲取得或重新登入)觸發 watch
重新查詢(UserLogs.vue:51-55)。
關鍵欄位對應(API ↔︎ 畫面):
| 畫面元素 | FE state | API 欄位 |
|---|---|---|
| DataTable 各欄 | logs(array) |
data[]
逐項對應(act_time/level/user_name/url/method/source_ip/server_ip/user_agent/request) |
| 全域搜尋框 | filterTable.global.value |
payload.filters.{user_name,level,request,source_ip,server_ip,method,url}(同一關鍵字套
7 欄,OR 連接,見 §12 坑 1) |
| Payload 欄顯示 | markPassword(data.request) computed(v-dompurify-html
渲染) |
data.request(BE 側已遮罩一次,FE 端對可解析 JSON 的
password key 再遮罩一次,雙重防護) |
| 分頁/排序 | pager/sort(useTableQuery
提供) |
request payload.{pager,sort};response
meta.{page,page_size,total} |
儲存流程:本頁無任何寫入操作(唯讀查詢頁);使用者的操作(搜尋/排序/翻頁/重置)皆觸發同一
getUserLogs() 重新查詢,不做樂觀更新。
錯誤對應:BE error envelope → toast 顯示(若有
BaseService 統一攔截)。本頁無專屬 error
code(common/code/
各檔搜尋日誌相關前綴皆無結果,jedi_api_log.common.enum.error_code
亦未見被主專案引用捕捉特定碼);查詢失敗多半是
401(未登入)或未捕捉例外的 500。
11. 背景行為與外部依賴
| 類型 | 內容 |
|---|---|
| 通知 | 無——本頁不觸發、也不消費任何通知 |
| Socket | 無 socket / 輪詢;操作後皆重打 getUserLogs() |
| 全域寫入管線 | common/middleware/app_mw.py 掛在
每一個 Flask 請求的
before_request(建立日誌列,失敗時 try/except
吞掉不影響業務 API)/after_request(回填
response/耗時/使用者資訊)/teardown_request(例外路徑回填
message);本頁完全是這條全域管線的唯讀消費端 |
| 分區維護 | public.maintain_log_partitions() DB function(建未來 3
個月分區 + DROP 超過 retention 的月分區,api_logs=90
天/system_logs=180 天);⚠️
目前無排程呼叫此函式(scripts/sql/2026-06-03-log-tables-partitioning.sql
註解明載「排程未接(pg_cron 未裝)」,主專案程式碼亦未見任何
APScheduler/cron job 呼叫
maintain_log_partitions——需人工執行或後續補排程,否則分區只會累積不會清理,retention
政策實質未生效) |
| jedi-* 套件 | jedi-api-log(原始碼倉庫名 jedi-log,日誌
domain/infra
核心)、jedi-common(BaseRepositoryImpl/transaction/DBLogHandler/分頁
spec)、jedi-auth(get_user_context() 供
after_request 取當前使用者) |
| 系統參數 | 無 feature flag |
12. 邊界情況與已知坑
- 🔴 匯出端點無任何認證檢查(SEC-001,已有 regression
test):
GET /log/api-logs/export的@jwt_required()被註解在api/log/routes/api_log_route.py:51,任何人(含未登入者)只要知道 URL 即可下載全部api_logs(含其他使用者的 request/response 內容、IP 等)。tests/test_security.py的test_SEC_001_no_jwt已用pytest.xfail記錄此已知漏洞;即使補上@jwt_required(),test_SEC_002_normal_user也指出任何登入者(無角色檢查)仍可匯出全部日誌。本頁 spec 僅記錄不修,修正需另開 bug session - 查詢與匯出走兩套不同的過濾邏輯,行為不對稱:畫面查詢(
POST /log/api-logs)走BaseRepositoryImpl._get_pager_list_query——字串欄位皆ILIKE '%value%'且跨欄位以 OR 連接(符合 FE「同關鍵字套 7 欄」的搜尋直覺);但匯出(GET /log/api-logs/export)走ApiLogRepoImpl.get_all_by_fields→_gen_filters(模組自訂、非BaseRepositoryImpl泛用版)——多數欄位是==精確比對、僅user_name/level/request/source_ip/server_ip/method用.like()(且url不在此清單內,若帶urlfilter 會變成精確比對而非模糊)、且條件間是 AND。實務上匯出端點目前呼叫時未帶任何 filter 參數(§6.3 無 query/body),所以這套不對稱邏輯暫時不影響現況行為,但屬「兩套真相並存」的技術債,日後若要讓匯出吃畫面篩選條件,務必留意這條差異 - Response body
未做密碼遮罩:
before_request寫入request欄位時有呼叫mark_password()遮罩 JSON body 內的passwordkey,但after_request回填response欄位時沒有同等遮罩處理(common/middleware/app_mw.py:93:response.data.decode(...)直接存)。若某支 API 的回應內容意外帶出密碼或 token 類敏感欄位,會原樣寫入api_logs.response,且此表無 RLS、任何登入使用者皆可查詢(見坑 4),敏感度較高 - 跨租戶可見性:
api_logs/system_logs皆無tenant_id欄位、不受 RLS 涵蓋,任何登入使用者查詢本頁即可看到全系統所有租戶的 API 呼叫紀錄與 request/response 內容,與其他頁面「RLS 自動做租戶隔離」的預設假設不同(見 _overview §3) user_uid欄位命名誤導:ApiLogResponse.user_uid實際存的是使用者login_name字串(after_request用curr_user_info.login_name回填),不是 UUID/不是users.uid。維護時勿假設可拿此欄位去 joinusers.uid- 分區 retention
政策未實際生效:
maintain_log_partitions()函式已建立(90/180 天 DROP 邏輯完整),但沒有任何排程機制呼叫它(pg_cron 未裝、主專案亦無對應 APScheduler job),目前分區只會持續新建、不會自動清除到期分區——若未來磁碟空間或效能出現異常,先確認是否為此函式從未真正跑過 UserLogService.js是死碼:src/service/UserLogService.js(讀/demo/data/logs.json假資料)未被UserLogs.vue或任何其他檔案引用,屬歷史殘留,日後清理死碼時可考慮移除(本頁 spec 僅記錄,不擅自刪除)level/type欄位形同虛設:level寫入時固定'INFO'(update_api_log不會改動),type固定"1"(LogTypeCode.API_LOG)——雖然 DataTable 有「日誌等級」欄且可搜尋/排序,但目前系統實際上只會產生單一 level 值,欄位的區分能力未被實際使用- 匯出檔名含時間戳但無條件摘要:匯出檔名
user_log_<YYYYMMDDHHMMSS>.zip僅反映匯出時間,配合坑 2(匯出不吃畫面篩選),使用者若在特定條件下匯出、事後容易搞混是哪次查詢的結果(因為匯出其實永遠是全表,不是「當前篩選結果」)
13. 開發與驗證
- 跑起來:BE
python main_socketio.py(port 8000,log 在log/app.log);FE 在compliance-manager-fe/起 Vite dev server。BE 改 service code 後必須重啟(無 hot reload);改jedi-api-log(jedi-log)套件本身需依 poetry path dependency 流程(見 CLAUDE.md「外部套件異動規範」) - 測試帳號:dev 環境系統管理層角色帳號(密碼見
.env/ 部署文件) - 導航路徑:登入 →
系統設定選單「操作日誌」(
user-log,path/log/user-log) - 前置資料:無強制前置資料——只要系統有任何 API 呼叫紀錄(幾乎必然存在,因為全域 middleware 對所有請求都寫)即可看到資料
- E2E:
compliance-manager-test/repo(Cucumber + Playwright);本頁相關 feature 檔以user-log/操作日誌/api-log搜尋 - 安全測試:
tests/test_security.py(TestExportApiLogsSecurityVulnerability)已涵蓋匯出端點的 SEC-001 已知漏洞回歸測試 - 相關文件:DB / API 全量見 GAI-SD-02/03;分區與
retention 設計見
scripts/sql/2026-06-03-log-tables-partitioning.sql