# 审计记录：文件管理行点击与自动选服

## 审计信息

- **日期**: 2026-06-01
- **审计人**: Claude (AI)
- **触发原因**: Bug 修复 — `#/files` 无法进入子目录、未选服务器时空列表
- **关联 changelog**: `docs/changelog/2026-06-01-files-page-row-click.md`

## 审计范围（实际改动文件清单）

| 文件 | 行数 | 状态 |
|------|------|------|
| `frontend/src/pages/FilesPage.vue` | 1429 | ☑ 全文已审 |
| `frontend/src/composables/useServerList.ts` | 37 | ☑ 全文已审 |
| `docs/changelog/2026-06-01-files-page-row-click.md` | 25 | ☑ 已审 |

## 变更摘要（审计对象）

1. **Vuetify 4 兼容**：移除无效的 `@click:row` / `@dblclick:row` / `@contextmenu:row`，改用 `:row-props="fileRowProps"` 绑定 `onClick` / `onDblclick` / `onContextmenu`。
2. **自动选服**：`onMounted` 在 `loadServers()` 后调用 `resolveInitialServerId()`（URL → sessionStorage → 第一台），并 `browse()`。
3. **记忆服务器**：`onServerChange` 写入 `sessionStorage` 键 `nexus:files:last_server_id`。
4. **列表加载**：`useServerList` 改用 `http.getList`，兼容数组与 `{ items, total }`。

---

## 审计 8 步结果

### Step 1: 登记 ✅

已登记 3 个文件；变更动机与 changelog 一致。

### Step 2: 全文 Read ✅

- `FilesPage.vue`：template（工具栏、表格、对话框、FileEditorWorkbench）+ script setup 全量通读。
- `useServerList.ts`：全量 37 行。
- 交叉引用：`remotePath.ts`、`fileBrowse.ts`、`api/index.ts`（`getList`）未改，仅核对调用契约。

### Step 3: 规则扫描 H

| ID | 规则 | 扫描位置 | 命中 |
|----|------|----------|------|
| H1 | XSS（未消毒用户输入进 DOM） | `FilesPage.vue` template | 否 — 文件名来自 API，Vue 默认转义 |
| H2 | 凭据泄露 | sessionStorage、路由 query | 否 — 仅存 `server_id` 数字 |
| H3 | IDOR（越权 server_id） | `browse()`、`resolveInitialServerId` | 否 — 后端 JWT + `get_by_id`；前端校验 ID 在列表内（初始化路径） |
| H4 | 路径穿越（`../`） | `openEntry` 符号链接绝对路径 | 否（既有）— `normalizeRemotePath`；穿越依赖 SSH 返回的 link_target，属管理员可信面 |
| H5 | 静默吞错 | `useServerList` catch | 既有模式 — `servers=[]`，页面有空状态提示 |
| H6 | 事件委托冲突（checkbox/按钮误触发行导航） | `fileRowProps` | 已缓解 — `isRowActionTarget` + `closest` |
| H7 | Vuetify API 废弃 | `@click:row` | 已修复 — 改用 `row-props` |
| H8 | 未选服务器调用 API | `browse()` | 否 — 入口 `if (!selectedServer.value) return` |
| H9 | 双击/单击竞态 | 目录行 click + dblclick | 否 — `onRowDblClick` 仅处理 `file` 类型 |
| H10 | sessionStorage 污染 | `FILES_LAST_SERVER_KEY` | 否 — 写入前 `onServerChange` 有值；读取时 `serverList.some` 校验 |
| H11 | 类型安全 | `fileRowProps(data)` | 可接受 — `data.item` 与 `FileEntry` 一致 |
| H12 | 常量声明顺序 | `FILES_LAST_SERVER_KEY` 在 `onServerChange` 之后 | 见 Closure — 运行时无 TDZ 问题，可读性欠佳 |

### Step 4: Closure 表

| H | 判定 | 依据 |
|---|------|------|
| H1 | 通过 | 无 `v-html`；`{{ item.name }}` 等文本插值 |
| H2 | 通过 | `sessionStorage` 仅 `String(serverId)` |
| H3 | 通过 | `resolveInitialServerId` L857-868：`Number.isFinite` + `serverList.some`；API 仍由后端鉴权 |
| H4 | 通过（既有） | 符号链接绝对路径 L751-752 走 `normalizeRemotePath`；后端 `sync_v2.browse_directory` 亦有规范化 |
| H5 | 通过（既有） | `useServerList` L29-30 与多页一致；Files 页 `onMounted` L1363-1365 有 snackbar |
| H6 | 通过 | `isRowActionTarget` L891-894 排除 `.v-selection-control, .v-btn, …` |
| H7 | 通过 | L187 `:row-props="fileRowProps"`；L896-914 实现与 Vuetify 4 `RowPropsFunction` 一致 |
| H8 | 通过 | `browse` L800-801；`navigateToPath` L759-762 |
| H9 | 通过 | `onRowDblClick` L923-926 仅 `isRegularFile` |
| H10 | 通过 | L861-866、L844-845 |
| H11 | 通过 | TypeScript `type-check` 已通过（本地构建前） |
| H12 | 建议 | `FILES_LAST_SERVER_KEY` 宜上移至 `// ── State ──` 区，非功能缺陷 |

**附带观察（非本次引入，不记 FINDING）**：

- `watch(route.query.server_id)` L1378-1390 未校验 `serverList`，非法 `?server_id=abc` 可能得到 `NaN`（原逻辑已存在）。建议在后续小改中与 `resolveInitialServerId` 对齐。

### Step 5: 入口表

| 入口类型 | 位置 | 说明 |
|----------|------|------|
| 用户点击 | `fileRowProps.onClick/onDblclick/onContextmenu` | 行级导航/编辑/菜单 |
| 用户选择 | `v-select` `selectedServer` | 切换服务器 → `onServerChange` |
| 路由 query | `server_id`, `path` | `onMounted` / `watch` / `syncRouteQuery` |
| sessionStorage | `nexus:files:last_server_id` | 恢复上次服务器 |
| HTTP GET | `/servers/` | `useServerList.loadServers` |
| HTTP POST | `/sync/browse` 等 | 既有文件操作（未改契约） |

### Step 6: 输入 → Sink

```
[URL server_id / sessionStorage / 默认第一台]
  → resolveInitialServerId (校验 ∈ serverList)
  → selectedServer
  → browse() → POST /sync/browse { server_id, path }
  → 后端 JWT + ServerRepository + SSH ls

[行点击 item.name]
  → openEntry → joinRemotePath + normalizeRemotePath
  → browse()（同上）

[行点击 非目录]
  → onRowClick 无操作（仅目录/符号链接）

[双击文件]
  → editFile → readRemoteFileContent → FileEditorWorkbench
```

**控制措施**：路径片段经 `joinRemotePath`/`validatePathSegment`；服务器 ID 初始化路径白名单校验；API 鉴权在后端。

### Step 7: 归类

```
FilesPage.vue           1429行  12H  0 FINDING
useServerList.ts            37行   1H  0 FINDING
changelog                    25行   0H  0 FINDING
────────────────────────────────────────────
总计                      13H   0 FINDING
```

### Step 8: DoD ✅

| 维度 | 项 | 结果 |
|------|-----|------|
| 安全 | 无新增 XSS/IDOR/凭据泄露 | ✅ |
| 逻辑 | 目录单击可进入；无 server 时自动选服并 browse | ✅ |
| 逻辑 | 复选框/操作列不误触发目录进入 | ✅ |
| 性能 | `row-props` 每行创建闭包（与 Vuetify 常规用法一致） | ✅ |
| 质量 | TS type-check | ✅（实现时已通过） |
| 质量 | `FILES_LAST_SERVER_KEY` 声明位置 | ⚠️ 建议上移（不阻断） |
| 文档 | changelog ≥10 行 | ✅ |
| 部署 | 生产 tar 需在 WSL/Git Bash 重跑；prune 路径为仓库根 `deploy/prune_frontend_assets.py` | ⚠️ 运维注意 |

---

## FINDING 列表

无（0 FINDING）。

---

## 审查 4 维度（摘要）

### 安全（12 项抽样）

- JWT 请求经既有 `http` 客户端 ✅
- 不在前端存储 SSH 密码 ✅
- `server_id` 仅数字、列表内校验（初始化） ✅
- 行事件不执行 `eval`/动态 HTML ✅

### 逻辑（8 项）

- 根因：Vuetify 4 无 `click:row` → 已用 `row-props` 修复 ✅
- 根因：无 query 不 browse → 已自动选服 ✅
- 空服务器列表提示 ✅
- 符号链接相对路径不盲目 navigate ✅

### 性能（5 项）

- `displayedFiles` 仍为 computed 过滤，未增加额外请求 ✅
- `preloadDirectoryFiles` 行为未改 ✅

### 质量（5 项）

- 注释说明 Vuetify 4 变更原因 ✅
- `useServerList` 与 `SchedulesPage`/`RetriesPage` 列表解析策略统一 ✅
- 常量位置可优化（H12 建议）⚠️

---

## 结论

☑ **审计通过，0 FINDING，可合并/可部署**

**部署前提醒**（非代码缺陷）：

1. Windows `tar` 对 `-C web/app` 可能失败，请用 WSL 或 `deploy/deploy-frontend.sh` 打包上传。
2. 生产 prune：`cd /www/wwwroot/api.synaglobal.vip && python3 deploy/prune_frontend_assets.py`
3. 浏览器验证 `#/files` 后建议 **Ctrl+F5** 强刷。

**可选后续小改**（不阻断）：

- 将 `FILES_LAST_SERVER_KEY` 移至 state 区顶部。
- `watch(route.query.server_id)` 增加与 `resolveInitialServerId` 相同的列表校验。
