docs: introduce project and keep local reports out of source delivery
No files matched your search
@@ -6,3 +6,6 @@ test-results/
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# 本地交接、审计报告和验收截图不随源码交付;README 保留项目使用说明。
|
||||
docs/
|
||||
@@ -1,72 +1,145 @@
|
||||
# crashwork-console 完整前端工程(源码重建版)
|
||||
# 破碎工坊云 · 会员中心前端
|
||||
|
||||
这是根据当前 `20261025-kky-traffic-display` 发布包重建的独立工程,不是原始源码的逐字恢复。
|
||||
覆盖 52 个页面、104 个业务组件、4 套布局,包含现有图片、字体和样式。开发、构建和运行均不依赖旧发布目录。
|
||||
这里交付的是整个会员中心前端,不是仅含四个改写文件的示例。已只读核对服务器当前发布版本;
|
||||
模块元数据及其 735 个构建资源与重建依据一致。范围、证据与边界见 [完整项目交接说明](docs/project-completeness.md)。
|
||||
面向云服务和 IDC 业务的用户控制台,提供账号管理、商品选购、服务器管理、订单支付与售后服务等页面。
|
||||
本仓库是可独立安装、开发和构建的前端工程,不包含后端服务、数据库或生产环境凭证。
|
||||
|
||||
原工程的模板、注释和局部变量名已在编译时丢失。多数页面恢复为可编辑的 Vue 单文件组件及渲染函数,
|
||||
部分局部变量仍为短名;请求/会话核心、应用入口、鉴权、主题和图表集成已重新编写。
|
||||
第二轮已将登录页、旧平台登录入口、SSH 密钥页和公共认证布局改为手写 `<script setup>` 与 `<template>`,
|
||||
使用业务命名,保留原接口及样式契约;这四个文件合计净减少 863 行,未增加依赖。
|
||||
这不是“所有业务均已重新设计并完成线上验收”的声明。具体验证与限制见 [验收报告](docs/validation.md)。
|
||||
## 主要功能
|
||||
|
||||
## 开发
|
||||
- 账号与身份:密码 / 短信登录、注册、找回密码、第三方登录与绑定、个人资料、实名认证和 API 密钥。
|
||||
- 控制台与资源:会员首页、资源总览、云服务器和独立服务器列表、实例详情。
|
||||
- 商品与购买:商品分类、配置选购、购物车、订单创建、续费和升级入口。
|
||||
- 实例管理:监控、网络、磁盘、流量、访问凭据与远程控制;具体能力取决于后端接入的产品模块。
|
||||
- 账务与支付:订单、账单、余额、支付、发票、优惠券、转账和推广相关页面。
|
||||
- 安全与服务:SSH 密钥、安全组、域名、操作日志、工单、消息、公告和帮助内容。
|
||||
|
||||
需要 Node.js 22.12+ 与 npm,建议在团队中统一 Node 版本。
|
||||
当前包含 52 个页面、104 个业务组件和 4 套布局。上述列表说明前端覆盖范围,不代表所有业务已完成真实后端验收。
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 层次 | 技术 |
|
||||
| ---------- | --------------------------------------------------------- |
|
||||
| 应用框架 | Nuxt 4、Vue 3、Vue Router,客户端 SPA(`ssr: false`) |
|
||||
| 状态与界面 | Pinia、PrimeVue 4、PrimeUI 主题、本地 Lucide 图标 |
|
||||
| 图表与远控 | ECharts、vue-echarts、noVNC |
|
||||
| 源码 | JavaScript、Vue 单文件组件、CSS,Nuxt 配置使用 TypeScript |
|
||||
| 验证工具 | Node.js `node:test`、Playwright、源码检查脚本、Prettier |
|
||||
|
||||
依赖的精确版本以 `package-lock.json` 为准。
|
||||
|
||||
## 快速开始
|
||||
|
||||
需要 Node.js **22.12.0 或更高版本**及 npm,团队开发建议统一 Node.js 版本。
|
||||
|
||||
```sh
|
||||
git clone https://git.crash.work/cieanyau/console-dev.git
|
||||
cd console-dev
|
||||
npm ci
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
在 `.env` 中配置后端地址:
|
||||
|
||||
```dotenv
|
||||
NUXT_PUBLIC_API_BASE=http://127.0.0.1:8080
|
||||
```
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
在 `.env` 设置 `NUXT_PUBLIC_API_BASE` 为后端 origin,例如 `http://127.0.0.1:8080`。
|
||||
不要附加 `/console/v1`;代码会自动追加。空值表示同源,开发或部署的反向代理必须另行转发该路径。
|
||||
图形验证码的 `/captcha/` 和远控的 `/remote-console/sessions`(包括 WebSocket)仍走前端同源,
|
||||
必须另外配置对应后端/网关的反向代理;只设置 API origin 不会自动转发这两类请求。
|
||||
前端不会替你启动后端服务、短信、验证码、支付渠道或 VNC 网关。
|
||||
开发服务仅监听 `127.0.0.1`,实际访问地址以启动日志为准。
|
||||
|
||||
## 构建与验证
|
||||
### 接口与反向代理
|
||||
|
||||
`NUXT_PUBLIC_API_BASE` 只填写后端 origin,不要附加 `/console/v1`,请求层会自动追加该前缀。
|
||||
留空表示使用前端同源地址,需要在开发或部署环境配置接口反向代理。
|
||||
|
||||
以下地址始终走前端同源,必须单独转发到对应服务:
|
||||
|
||||
- `/captcha/`:图形验证码。
|
||||
- `/remote-console/sessions`:远控会话及 WebSocket 连接。
|
||||
|
||||
仅设置 API 地址不会自动代理这两类请求,也不会启动后端、验证码、短信邮件、支付渠道或远控网关。
|
||||
所有 `NUXT_PUBLIC_*` 配置都会暴露给浏览器,不能填写密码、访问令牌或数据库凭证。
|
||||
|
||||
## 常用命令
|
||||
|
||||
| 命令 | 用途 |
|
||||
| ----------------- | ----------------------------------------- |
|
||||
| `npm run dev` | 启动本地开发服务 |
|
||||
| `npm run check` | 检查应用源码语法、导入和历史部署路径依赖 |
|
||||
| `npm run build` | 构建生产版本,输出到项目根目录 `.output/` |
|
||||
| `npm run preview` | 运行已构建的 Node.js 服务 |
|
||||
| `npm test` | 对生产构建运行浏览器端到端测试 |
|
||||
| `npm run format` | 格式化应用、配置、脚本和测试代码 |
|
||||
|
||||
### 构建与本地预览
|
||||
|
||||
```sh
|
||||
npm run check
|
||||
npm run build
|
||||
npm test
|
||||
```
|
||||
|
||||
浏览器测试自动启动本地模拟接口与真实构建服务,并禁止访问外网。默认使用 macOS 已安装的 Chrome;
|
||||
其他环境请设置 `CHROME_PATH=/absolute/path/to/chrome`。详细说明见 [测试说明](tests/README.md)。
|
||||
当前离线验收为 21 项,包含短信配置与倒计时、SSH 增改删/末页恢复和旧入口失败后重试。
|
||||
|
||||
构建结果位于本工程的 `.output/`,不是仓库根目录的 `.output`。
|
||||
|
||||
```sh
|
||||
NUXT_PUBLIC_API_BASE=http://127.0.0.1:8080 HOST=127.0.0.1 PORT=3000 npm run preview
|
||||
```
|
||||
|
||||
## 源码结构
|
||||
预览命令直接运行 `.output/server/index.mjs`,不会自动读取项目 `.env`,请通过运行环境传入配置。
|
||||
正式部署应使用独立版本目录和进程管理,并配置 HTTPS、接口 / WebSocket 反向代理及回滚流程。
|
||||
本仓库的提交和构建不会自动修改生产服务器。
|
||||
|
||||
- `app/pages`、`app/layouts`、`app/components`:页面、布局和业务组件。
|
||||
- `app/lib/auth.js`:会话、接口协议、请求截止时间、购买前置校验及列表缓存。
|
||||
- `app/middleware/auth.global.js`:公开路由与鉴权、旧平台登录入口。
|
||||
- `app/plugins`:UI、站点初始化、跨标签页会话同步。
|
||||
- `app/config`:中文文案及 MIT 主题配置;图标由本地服务提供。
|
||||
- `tests`:通过真实页面操作和 HTTP 边界验证的回归场景。
|
||||
- `docs/module-inventory.json`:重建范围与路由清单。
|
||||
- `scripts/reconstruct.mjs`:一次性导入过程的留档;不是构建步骤,拒绝覆盖已有源码。
|
||||
## 源码目录
|
||||
|
||||
维护源码后执行正常构建即可,不要重新运行导入器覆盖修复,也不要手工修改生成的哈希 JS 文件。
|
||||
导入器仅留档用于可信的自有发布产物,不是通用归档解包器;不要给它提供第三方元数据或不可信构建输入。
|
||||
Vue、Vue Router、PrimeVue、Pinia、ECharts、noVNC 等通过正式 npm 依赖引用,不携带旧框架副本。
|
||||
保留的 CryptoJS AES 参数属于现有后端登录协议,不能在前后端未协商时单独修改。
|
||||
```text
|
||||
app/
|
||||
app.vue 应用入口、布局选择与会话隔离
|
||||
pages/ 文件路由与业务页面
|
||||
layouts/ 控制台、认证、公开购买及站点布局
|
||||
components/ 业务表单、实例管理、支付和公共界面
|
||||
lib/ 接口、会话、商品配置及业务工具
|
||||
middleware/ 路由鉴权
|
||||
plugins/ 界面注册、启动初始化、跨标签页会话同步
|
||||
config/ 中文文案和主题配置
|
||||
utils/ 访问规则及基础适配
|
||||
assets/styles/ 全局与业务样式
|
||||
public/ 图片、图标、字体等静态资源
|
||||
scripts/ 源码检查及一次性重建留档脚本
|
||||
tests/ 浏览器场景与独立 HTTP 测试后端
|
||||
nuxt.config.ts 应用与公开运行时配置
|
||||
.env.example 无凭证的环境配置示例
|
||||
```
|
||||
|
||||
## 发给员工与发布
|
||||
`docs/` 仅保留在本地,已加入 Git 忽略规则,不随后续源码版本上传;历史提交中的文档不会因此消失。
|
||||
不要使用 `git add -f docs` 将本地报告重新加入仓库。
|
||||
|
||||
交付本目录的源码压缩包或 Git 分支即可。员工运行 `npm ci`,配置自己的后端 origin,然后开发和验证。
|
||||
不要发送 `.env`、登录凭证、数据库备份、`node_modules` 或 Git 历史;示例配置不包含秘密。
|
||||
依赖包及保留静态资产应遵守各自许可证和你方资产授权。
|
||||
## 测试
|
||||
|
||||
本次没有修改线上发布指向,也没有部署。上线前应先在测试环境连接真实后端验证商品配置、验证码、
|
||||
安全验证、账务、支付和 VNC 反代;现有根目录发布脚本不会自动改为构建这个新工程。
|
||||
确认通过后,再按团队的版本发布/回滚流程发布本工程 `.output`,不要直接覆盖正在服务的版本。
|
||||
```sh
|
||||
npm run build
|
||||
npm test
|
||||
```
|
||||
|
||||
测试使用真实 Chrome / Chromium 操作生产构建,自动启动本地 HTTP 测试后端,禁止访问外网,
|
||||
不替换项目内部模块,也不预置登录令牌。默认使用 macOS 已安装的 Chrome;其他环境可指定:
|
||||
|
||||
```sh
|
||||
CHROME_PATH=/absolute/path/to/chrome npm test
|
||||
```
|
||||
|
||||
现有 21 个离线场景覆盖登录协议、短信倒计时、路由保护、会话失效、双标签页退出、SSH 密钥管理、
|
||||
请求超时、支付期限及旧登录入口等流程。测试产物保存在被忽略的 `tests/fixtures/runs/`。
|
||||
详细使用方式见 [测试说明](tests/README.md)。
|
||||
|
||||
真实支付、第三方登录 / CAPTCHA、安全挑战、短信邮件投递、供应商接口和 VNC 网关仍需在测试环境联调。
|
||||
|
||||
## 维护与交付说明
|
||||
|
||||
本工程根据 `20261025-kky-traffic-display` 发布产物重建,不是原始源码的逐字恢复。
|
||||
多数页面和组件保留恢复后的 Vue 渲染函数及部分短变量名;原始模板、注释和完整变量名无法从发布包完整找回。
|
||||
当前新增注释用于解释已核实的职责、流程和接口边界,不代表找回了原作者的注释,也不代表全站重新手写。
|
||||
|
||||
维护时直接修改当前源码并正常构建,不要编辑生成的哈希产物。
|
||||
`scripts/reconstruct.mjs` 仅是一次性导入留档,不是开发或发布步骤,且会拒绝覆盖现有页面;
|
||||
不要使用不可信的第三方发布产物运行该脚本。
|
||||
登录所用的 CryptoJS AES 参数属于现有后端协议,调整前必须与后端协商,不能将其视为替代 HTTPS 的安全措施。
|
||||
|
||||
向员工交付仓库和对应分支即可;每位开发者自行安装依赖、配置后端并执行验证。
|
||||
不要提交 `.env`、账号密码、令牌、数据库备份、`node_modules/`、`.output/` 或本地验收报告。
|
||||
依赖包与保留静态资源应遵守各自许可证及资产授权;本仓库未另行授予整体开源许可证。
|
||||
@@ -1,4 +1,8 @@
|
||||
<script setup>
|
||||
/**
|
||||
* 全站入口:公共购买页可在未登录时使用独立布局,其余页面沿用路由指定的布局。
|
||||
* 路由放行与组件实际挂载同时受会话约束,防止重定向等待期间仍执行受保护页面请求。
|
||||
*/
|
||||
import { computed, onMounted, onBeforeUnmount } from "vue";
|
||||
import { useRoute } from "#app";
|
||||
import { NuxtLayout, NuxtPage, NuxtRouteAnnouncer } from "#components";
|
||||
@@ -11,16 +15,19 @@ import { isPublicShop, isAuthPage, isPublicRoute } from "~/utils/access.js";
|
||||
const auth = useAuth();
|
||||
const route = useRoute();
|
||||
const theme = useTheme();
|
||||
// 只为访客切换公开购买布局;登录后的同一路由继续使用会员中心布局。
|
||||
const layout = computed(() =>
|
||||
!auth.isLoggedIn && isPublicShop(route) ? "public-order" : route.meta.layout,
|
||||
);
|
||||
// An authenticated route's component tree belongs to that session, not to a pending redirect.
|
||||
// 中间件负责导航,入口负责禁止未授权组件树挂载,两者处理的是不同的生命周期阶段。
|
||||
const canRenderRoute = computed(() => auth.isLoggedIn || isPublicRoute(route));
|
||||
// 账号切换时重新创建布局与页面,清除旧账号的局部状态;登录入口自身按路径区分。
|
||||
const routeSessionKey = computed(() =>
|
||||
isAuthPage(route.path) || route.path === "/home.htm"
|
||||
? route.path
|
||||
: auth.token || "anonymous",
|
||||
);
|
||||
// 主题监听由入口统一注册和释放,避免每次页面导航重复绑定系统主题事件。
|
||||
onMounted(theme.init);
|
||||
onBeforeUnmount(theme.dispose);
|
||||
</script>
|
||||
|
||||
|
Before Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 153 KiB |
@@ -1,147 +0,0 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"checkedOn": "2026-11-05",
|
||||
"scope": "Rebuilt frontend and its integration boundaries; not a complete audit of the deployment archive",
|
||||
"fixed": [
|
||||
{
|
||||
"id": "REQUEST_DEADLINE",
|
||||
"problem": "Requests could wait indefinitely, including while reading response bodies",
|
||||
"resolution": "One request deadline covers preflight, fetch and body; 30s JSON / 120s transfer defaults",
|
||||
"evidence": "acceptance.json: the default request deadline aborts an unfinished JSON body and allows retry"
|
||||
},
|
||||
{
|
||||
"id": "LOGOUT_PENDING",
|
||||
"problem": "A pending logout response delayed local session revocation",
|
||||
"resolution": "Capture old identity, clear local state immediately, revoke with a bounded background request",
|
||||
"evidence": "acceptance.json: logout clears local access while its API request remains pending"
|
||||
},
|
||||
{
|
||||
"id": "PAYMENT_DEADLINE",
|
||||
"problem": "A stalled status request could consume the payment session deadline",
|
||||
"resolution": "One session owns polling, an independent 5 minute deadline and invalidation on close/order/identity changes",
|
||||
"evidence": "acceptance.json: stalled status and top-level Paid scenarios"
|
||||
},
|
||||
{
|
||||
"id": "SSH_PAGINATION",
|
||||
"problem": "Complete search results and server-paged results were sliced inconsistently",
|
||||
"resolution": "Separate applied query from draft input and slice only complete search results",
|
||||
"evidence": "acceptance.json: SSH search paginates 25 matches without repeating all rows"
|
||||
},
|
||||
{
|
||||
"id": "SESSION_TREE",
|
||||
"problem": "Session changes could unmount a login in progress or keep expired protected components alive",
|
||||
"resolution": "Stable public login entry identity; protected component tree belongs to one authenticated session",
|
||||
"evidence": "acceptance.json: login, legacy login, payment logout and payment 401 scenarios"
|
||||
},
|
||||
{
|
||||
"id": "LEGACY_URL",
|
||||
"problem": "Legacy credentials reappeared when router fullPath restored a query removed only from history",
|
||||
"resolution": "Canonicalize both browser history and router location before mounting the legacy entry",
|
||||
"evidence": "acceptance.json: a rejected legacy credential stays out of the error URL and can be retried"
|
||||
},
|
||||
{
|
||||
"id": "CROSS_TAB",
|
||||
"problem": "Other tabs retained a stale visible authenticated session after logout",
|
||||
"resolution": "Reconcile browser storage events into the same reactive identity used by requests and UI",
|
||||
"evidence": "acceptance.json: logout in another tab revokes both visible sessions"
|
||||
},
|
||||
{
|
||||
"id": "VENDOR_LICENSE",
|
||||
"problem": "The initially restored component-library version displayed an invalid commercial-license warning",
|
||||
"resolution": "Use MIT PrimeVue 4 and MIT theme package; do not bypass license checks",
|
||||
"evidence": "All browser scenarios reject visible Invalid PrimeUI License warnings"
|
||||
},
|
||||
{
|
||||
"id": "LOCAL_ASSETS_RUNTIME",
|
||||
"problem": "Default icon lookup used the external Iconify provider; mismatched framework versions broke server startup",
|
||||
"resolution": "Serve bundled Lucide icons locally and align Nuxt/Vue/Router versions",
|
||||
"evidence": "Production build and all browser scenarios run without non-local requests"
|
||||
},
|
||||
{
|
||||
"id": "STATIC_GATE_TEMPLATES",
|
||||
"problem": "Path checks missed template literals",
|
||||
"resolution": "Inspect both literal strings and template elements in the lint-stage AST gate",
|
||||
"evidence": "Negative gate smoke rejected string and template machine paths; final check passes"
|
||||
}
|
||||
],
|
||||
"boundedDebt": [
|
||||
{
|
||||
"id": "AUTH_THEME_CYCLE_LABEL",
|
||||
"state": "retained-ux-debt",
|
||||
"what": "Blank auth layout labels the light-state button as switch to dark, while the existing theme cycle is auto -> light -> dark -> auto; with an initially light system, the first click stays light",
|
||||
"whyDeferred": "Confirmed on both baseline and rewritten layout; this batch preserves existing theme behavior rather than changing the shared three-mode contract",
|
||||
"blocks": "Claiming the theme button label accurately describes every next mode",
|
||||
"nextStep": "Align the label with the three-mode theme cycle when the shared theme controls are revised"
|
||||
},
|
||||
{
|
||||
"id": "REAL_BACKEND_ACCEPTANCE",
|
||||
"state": "unverified",
|
||||
"what": "Real ordering, invoices, verification, CAPTCHA/OAuth, financial changes, payment and VNC",
|
||||
"whyDeferred": "No production access or real financial operations authorized for this rebuild",
|
||||
"blocks": "Production release approval",
|
||||
"nextStep": "Run staging acceptance against the real backend and supplier adapters"
|
||||
},
|
||||
{
|
||||
"id": "REMOTE_GATEWAY_QUOTA",
|
||||
"state": "known-out-of-scope",
|
||||
"what": "Existing services/remote-console/gateway.mjs checks session quota before an await and registers later; two concurrent requests can both create a session at a limit of one",
|
||||
"whyDeferred": "Independent server service, not frontend source rebuilding",
|
||||
"blocks": "Claiming the remote-console stack has been fixed or production-validated",
|
||||
"nextStep": "Separately authorize an atomic quota reservation fix and concurrent HTTP regression test"
|
||||
},
|
||||
{
|
||||
"id": "DEPENDENCY_AUDIT",
|
||||
"state": "open",
|
||||
"what": "12 propagated npm audit findings: 6 low / 6 moderate",
|
||||
"whyDeferred": "Suggested automatic fix force-downgrades Nuxt; reachability and compatibility need review",
|
||||
"blocks": "Zero-vulnerability or unrestricted production-readiness claims",
|
||||
"nextStep": "Review dependency-audit.json and patched upstream versions before release"
|
||||
},
|
||||
{
|
||||
"id": "AUTH_PROTOCOL",
|
||||
"state": "retained-contract",
|
||||
"what": "Persistent browser JWT and discontinued CryptoJS AES login protocol remain",
|
||||
"whyDeferred": "Changing the wire protocol requires coordinated backend changes",
|
||||
"blocks": "Claiming an authentication-protocol security redesign",
|
||||
"nextStep": "Plan session storage and login protocol migration with backend ownership"
|
||||
},
|
||||
{
|
||||
"id": "RECOVERED_READABILITY",
|
||||
"state": "open",
|
||||
"what": "Most business SFCs retain render functions and short local variable names",
|
||||
"whyDeferred": "Original source, comments and templates were not present in supplied archives",
|
||||
"blocks": "Claiming verbatim original source recovery or a full manual rewrite of every page",
|
||||
"nextStep": "Login, legacy entry, SSH keys and the blank auth layout are now handwritten setup/templates; continue incrementally with the remaining business pages"
|
||||
},
|
||||
{
|
||||
"id": "AUDIT_DEPTH",
|
||||
"state": "partial",
|
||||
"what": "All delivered files are inventoried and all Vue/JS app modules are mechanically checked; only recorded security/lifecycle/payment/pagination boundaries received in-depth human review",
|
||||
"whyDeferred": "Rebuilt business surface is large; static coverage is not manual or end-to-end coverage",
|
||||
"blocks": "A 100% whole-project line-review or all-feature acceptance claim",
|
||||
"nextStep": "Use audit-coverage.json to prioritize unreviewed pages and execute each remaining business entry in staging"
|
||||
},
|
||||
{
|
||||
"id": "REVERSE_PROXY",
|
||||
"state": "deployment-required",
|
||||
"what": "Same-origin /captcha/ and /remote-console/sessions including WebSockets need separate reverse proxies",
|
||||
"whyDeferred": "Local code cannot configure the deployment's proxy or supplier gateway",
|
||||
"blocks": "Image CAPTCHA and remote-console integration without proxy configuration",
|
||||
"nextStep": "Configure and verify the documented routes in a test deployment"
|
||||
},
|
||||
{
|
||||
"id": "TRUSTED_IMPORTER",
|
||||
"state": "documented-boundary",
|
||||
"what": "One-time reconstruction script executes trusted build metadata and assumes its source paths are trusted",
|
||||
"whyDeferred": "Retained as reconstruction provenance, not a generic or runtime importer; refuses an existing app/pages",
|
||||
"blocks": "Using third-party/untrusted metadata as input",
|
||||
"nextStep": "Do not run for normal development or builds; use only trusted owned release inputs"
|
||||
}
|
||||
],
|
||||
"latestBatchReview": {
|
||||
"scope": "Only the four handwritten SFCs, their changed tests and documented integration seams",
|
||||
"confirmedRegressions": 0,
|
||||
"acceptance": "21/21 passed after serial integration; browserErrors=0; networkViolations=0",
|
||||
"remainingDebt": "All boundedDebt entries remain; no real provider acceptance claimed"
|
||||
}
|
||||
}
|
||||
@@ -1,246 +0,0 @@
|
||||
{
|
||||
"checkedOn": "2026-11-05",
|
||||
"command": "npm audit --json",
|
||||
"exitCode": 1,
|
||||
"interpretation": "12 dependency findings: 6 low, 6 moderate; no high/critical. Primarily propagated through Nuxt/Nitro build tooling. Not a zero-vulnerability claim; production reachability has not been exhaustively determined.",
|
||||
"decision": "Retain current pinned Nuxt and lockfile. Do not apply the suggested force downgrade to Nuxt 4.1.3 without compatibility and security review. Reassess before external release.",
|
||||
"auditReportVersion": 2,
|
||||
"vulnerabilities": {
|
||||
"@nuxt/nitro-server": {
|
||||
"name": "@nuxt/nitro-server",
|
||||
"severity": "low",
|
||||
"isDirect": false,
|
||||
"via": ["nitropack", "nuxt"],
|
||||
"effects": ["nuxt"],
|
||||
"range": ">=3.20.0",
|
||||
"nodes": ["node_modules/@nuxt/nitro-server"],
|
||||
"fixAvailable": {
|
||||
"name": "nuxt",
|
||||
"version": "4.1.3",
|
||||
"isSemVerMajor": true
|
||||
}
|
||||
},
|
||||
"@nuxt/vite-builder": {
|
||||
"name": "@nuxt/vite-builder",
|
||||
"severity": "low",
|
||||
"isDirect": false,
|
||||
"via": ["nuxt"],
|
||||
"effects": ["nuxt"],
|
||||
"range": "3.20.0 - 3.21.11 || >=4.2.0",
|
||||
"nodes": ["node_modules/@nuxt/vite-builder"],
|
||||
"fixAvailable": {
|
||||
"name": "nuxt",
|
||||
"version": "4.1.3",
|
||||
"isSemVerMajor": true
|
||||
}
|
||||
},
|
||||
"@nuxt/vite-server": {
|
||||
"name": "@nuxt/vite-server",
|
||||
"severity": "low",
|
||||
"isDirect": false,
|
||||
"via": ["nuxt"],
|
||||
"effects": ["nuxt"],
|
||||
"range": ">=4.6.0",
|
||||
"nodes": ["node_modules/@nuxt/vite-server"],
|
||||
"fixAvailable": {
|
||||
"name": "nuxt",
|
||||
"version": "4.1.3",
|
||||
"isSemVerMajor": true
|
||||
}
|
||||
},
|
||||
"@rollup/plugin-node-resolve": {
|
||||
"name": "@rollup/plugin-node-resolve",
|
||||
"severity": "low",
|
||||
"isDirect": false,
|
||||
"via": ["deepmerge"],
|
||||
"effects": ["nitropack"],
|
||||
"range": ">=8.0.0",
|
||||
"nodes": ["node_modules/@rollup/plugin-node-resolve"],
|
||||
"fixAvailable": {
|
||||
"name": "nuxt",
|
||||
"version": "4.1.3",
|
||||
"isSemVerMajor": true
|
||||
}
|
||||
},
|
||||
"@vercel/nft": {
|
||||
"name": "@vercel/nft",
|
||||
"severity": "moderate",
|
||||
"isDirect": false,
|
||||
"via": ["glob"],
|
||||
"effects": ["nitropack"],
|
||||
"range": ">=0.29.2",
|
||||
"nodes": ["node_modules/@vercel/nft"],
|
||||
"fixAvailable": {
|
||||
"name": "nuxt",
|
||||
"version": "4.1.3",
|
||||
"isSemVerMajor": true
|
||||
}
|
||||
},
|
||||
"archiver": {
|
||||
"name": "archiver",
|
||||
"severity": "moderate",
|
||||
"isDirect": false,
|
||||
"via": ["readdir-glob"],
|
||||
"effects": ["nitropack"],
|
||||
"range": ">=8.0.0",
|
||||
"nodes": ["node_modules/archiver"],
|
||||
"fixAvailable": {
|
||||
"name": "nuxt",
|
||||
"version": "4.1.3",
|
||||
"isSemVerMajor": true
|
||||
}
|
||||
},
|
||||
"deepmerge": {
|
||||
"name": "deepmerge",
|
||||
"severity": "low",
|
||||
"isDirect": false,
|
||||
"via": [
|
||||
{
|
||||
"source": 1243505,
|
||||
"name": "deepmerge",
|
||||
"dependency": "deepmerge",
|
||||
"title": "deepmerge mergeObject() allows prototype poisoning of returned objects",
|
||||
"url": "https://github.com/advisories/GHSA-gcqx-w48m-w2h4",
|
||||
"severity": "low",
|
||||
"cwe": ["CWE-1321"],
|
||||
"cvss": {
|
||||
"score": 5.3,
|
||||
"vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N"
|
||||
},
|
||||
"range": ">=4.2.0 <=4.3.1"
|
||||
}
|
||||
],
|
||||
"effects": ["@rollup/plugin-node-resolve"],
|
||||
"range": ">=4.2.0",
|
||||
"nodes": ["node_modules/deepmerge"],
|
||||
"fixAvailable": {
|
||||
"name": "nuxt",
|
||||
"version": "4.1.3",
|
||||
"isSemVerMajor": true
|
||||
}
|
||||
},
|
||||
"glob": {
|
||||
"name": "glob",
|
||||
"severity": "moderate",
|
||||
"isDirect": false,
|
||||
"via": [
|
||||
{
|
||||
"source": 1244013,
|
||||
"name": "glob",
|
||||
"dependency": "glob",
|
||||
"title": "Glob: AbortSignal cancellation bypass allows post-timeout filesystem traversal",
|
||||
"url": "https://github.com/advisories/GHSA-3g3m-2fjr-6pw4",
|
||||
"severity": "moderate",
|
||||
"cwe": ["CWE-400"],
|
||||
"cvss": {
|
||||
"score": 0,
|
||||
"vectorString": null
|
||||
},
|
||||
"range": ">=9.0.0 <=14.0.1"
|
||||
},
|
||||
"minimatch"
|
||||
],
|
||||
"effects": ["@vercel/nft"],
|
||||
"range": "9.0.0 - 14.0.1",
|
||||
"nodes": ["node_modules/glob"],
|
||||
"fixAvailable": {
|
||||
"name": "nuxt",
|
||||
"version": "4.1.3",
|
||||
"isSemVerMajor": true
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"name": "minimatch",
|
||||
"severity": "moderate",
|
||||
"isDirect": false,
|
||||
"via": [
|
||||
{
|
||||
"source": 1244623,
|
||||
"name": "minimatch",
|
||||
"dependency": "minimatch",
|
||||
"title": "minimatch has potential denial of service via globparts amplification",
|
||||
"url": "https://github.com/advisories/GHSA-g3hv-75wg-m249",
|
||||
"severity": "moderate",
|
||||
"cwe": ["CWE-405"],
|
||||
"cvss": {
|
||||
"score": 0,
|
||||
"vectorString": null
|
||||
},
|
||||
"range": ">=7.0.0 <11.0.1"
|
||||
}
|
||||
],
|
||||
"effects": ["glob", "readdir-glob"],
|
||||
"range": "7.0.0 - 11.0.0",
|
||||
"nodes": ["node_modules/minimatch"],
|
||||
"fixAvailable": {
|
||||
"name": "nuxt",
|
||||
"version": "4.1.3",
|
||||
"isSemVerMajor": true
|
||||
}
|
||||
},
|
||||
"nitropack": {
|
||||
"name": "nitropack",
|
||||
"severity": "moderate",
|
||||
"isDirect": false,
|
||||
"via": ["@rollup/plugin-node-resolve", "@vercel/nft", "archiver"],
|
||||
"effects": ["@nuxt/nitro-server"],
|
||||
"range": ">=0.1.0",
|
||||
"nodes": ["node_modules/nitropack"],
|
||||
"fixAvailable": {
|
||||
"name": "nuxt",
|
||||
"version": "4.1.3",
|
||||
"isSemVerMajor": true
|
||||
}
|
||||
},
|
||||
"nuxt": {
|
||||
"name": "nuxt",
|
||||
"severity": "low",
|
||||
"isDirect": true,
|
||||
"via": ["@nuxt/nitro-server", "@nuxt/vite-builder", "@nuxt/vite-server"],
|
||||
"effects": [
|
||||
"@nuxt/nitro-server",
|
||||
"@nuxt/vite-builder",
|
||||
"@nuxt/vite-server"
|
||||
],
|
||||
"range": "3.20.0 - 3.21.11 || >=4.2.0",
|
||||
"nodes": ["node_modules/nuxt"],
|
||||
"fixAvailable": {
|
||||
"name": "nuxt",
|
||||
"version": "4.1.3",
|
||||
"isSemVerMajor": true
|
||||
}
|
||||
},
|
||||
"readdir-glob": {
|
||||
"name": "readdir-glob",
|
||||
"severity": "moderate",
|
||||
"isDirect": false,
|
||||
"via": ["minimatch"],
|
||||
"effects": ["archiver"],
|
||||
"range": ">=2.0.0",
|
||||
"nodes": ["node_modules/readdir-glob"],
|
||||
"fixAvailable": {
|
||||
"name": "nuxt",
|
||||
"version": "4.1.3",
|
||||
"isSemVerMajor": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"vulnerabilities": {
|
||||
"info": 0,
|
||||
"low": 6,
|
||||
"moderate": 6,
|
||||
"high": 0,
|
||||
"critical": 0,
|
||||
"total": 12
|
||||
},
|
||||
"dependencies": {
|
||||
"prod": 571,
|
||||
"dev": 3,
|
||||
"optional": 79,
|
||||
"peer": 0,
|
||||
"peerOptional": 0,
|
||||
"total": 652
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,439 +0,0 @@
|
||||
{
|
||||
"pages": [
|
||||
"pages/account/api-keys.vue",
|
||||
"pages/account/connections.vue",
|
||||
"pages/account/index.vue",
|
||||
"pages/account/realname/company.vue",
|
||||
"pages/account/realname/index.vue",
|
||||
"pages/account/realname/personal.vue",
|
||||
"pages/bills/[id].vue",
|
||||
"pages/bills/index.vue",
|
||||
"pages/cart/index.vue",
|
||||
"pages/cloud-servers/index.vue",
|
||||
"pages/dedicated-servers/index.vue",
|
||||
"pages/finance/index.vue",
|
||||
"pages/forgot-password.vue",
|
||||
"pages/help.vue",
|
||||
"pages/home.htm.vue",
|
||||
"pages/index.vue",
|
||||
"pages/invoices/index.vue",
|
||||
"pages/invoices/invoice.vue",
|
||||
"pages/invoices/request.vue",
|
||||
"pages/invoices/settings/index.vue",
|
||||
"pages/logbook/index.vue",
|
||||
"pages/login.vue",
|
||||
"pages/messages/index.vue",
|
||||
"pages/news.vue",
|
||||
"pages/notices/index.vue",
|
||||
"pages/oauth/bind.vue",
|
||||
"pages/oauth/callback.vue",
|
||||
"pages/official-site.vue",
|
||||
"pages/orders/[id].vue",
|
||||
"pages/orders/index.vue",
|
||||
"pages/orders/order/index.vue",
|
||||
"pages/orders/order/paid.vue",
|
||||
"pages/orders/order/pay.vue",
|
||||
"pages/products/[id].vue",
|
||||
"pages/products/cloud-server.vue",
|
||||
"pages/products/dedicated-server/customize.vue",
|
||||
"pages/products/dedicated-server/index.vue",
|
||||
"pages/products/index.vue",
|
||||
"pages/recommend/index.vue",
|
||||
"pages/register.vue",
|
||||
"pages/remote-console.vue",
|
||||
"pages/resources.vue",
|
||||
"pages/security/domains/index.vue",
|
||||
"pages/security/groups/index.vue",
|
||||
"pages/security/ssh-keys/index.vue",
|
||||
"pages/servers/server.vue",
|
||||
"pages/tickets/[id].vue",
|
||||
"pages/tickets/create.vue",
|
||||
"pages/tickets/index.vue",
|
||||
"pages/tickets/ticket.vue",
|
||||
"pages/transfer/index.vue",
|
||||
"pages/vouchers/index.vue"
|
||||
],
|
||||
"components": [
|
||||
"components/AuthProviders.vue",
|
||||
"components/BackendNavEntry.vue",
|
||||
"components/BackendNavigation.vue",
|
||||
"components/BuildUpdateNotice.vue",
|
||||
"components/CaptchaField.vue",
|
||||
"components/ChartBox.vue",
|
||||
"components/ClientLevelDialog.vue",
|
||||
"components/ConsoleLegalFooter.vue",
|
||||
"components/GeetestField.vue",
|
||||
"components/GenericOAuthProviders.vue",
|
||||
"components/GlobalSearch.vue",
|
||||
"components/HostList.vue",
|
||||
"components/ListEmpty.vue",
|
||||
"components/ListSegments.vue",
|
||||
"components/MfCloudBackupPanel.vue",
|
||||
"components/NotificationPreferences.vue",
|
||||
"components/OperatePasswordGate.vue",
|
||||
"components/OperationNotice.vue",
|
||||
"components/OperationToasts.vue",
|
||||
"components/OsIcon.vue",
|
||||
"components/PageInfoNotice.vue",
|
||||
"components/PageSteps.vue",
|
||||
"components/PaymentDialog.vue",
|
||||
"components/PhoneCodeField.vue",
|
||||
"components/ProductCategoryNotice.vue",
|
||||
"components/ProductConfigurationSummary.vue",
|
||||
"components/ProductDescription.vue",
|
||||
"components/ProductGroupLabel.vue",
|
||||
"components/RechargeDialog.vue",
|
||||
"components/RichTextEditor.vue",
|
||||
"components/RingProgress.vue",
|
||||
"components/SecurityVerificationDialog.vue",
|
||||
"components/SensitiveValue.vue",
|
||||
"components/TicketAttachments.vue",
|
||||
"components/TicketNotice.vue",
|
||||
"components/UploadField.vue",
|
||||
"components/WechatConnection.vue",
|
||||
"components/WithdrawDialog.vue",
|
||||
"components/config/CloudProductPicker.vue",
|
||||
"components/config/CommonConfig.vue",
|
||||
"components/config/ConfigBandwidth.vue",
|
||||
"components/config/ConfigCheckout.vue",
|
||||
"components/config/ConfigChoiceGrid.vue",
|
||||
"components/config/ConfigDiskAdd.vue",
|
||||
"components/config/ConfigDiskCard.vue",
|
||||
"components/config/ConfigLoadingShell.vue",
|
||||
"components/config/ConfigResourceOption.vue",
|
||||
"components/config/FinanceConfig.vue",
|
||||
"components/config/JyCloudConfig.vue",
|
||||
"components/config/KkyEcsConfig.vue",
|
||||
"components/config/MfCloudConfig.vue",
|
||||
"components/config/MfDcimConfig.vue",
|
||||
"components/config/ProductCustomFields.vue",
|
||||
"components/config/PromoCodeField.vue",
|
||||
"components/home/HomeConsumeTrend.vue",
|
||||
"components/home/HomeExpiringPanel.vue",
|
||||
"components/home/HomeNoticePanel.vue",
|
||||
"components/home/HomeReferralPanel.vue",
|
||||
"components/home/HomeServerPanel.vue",
|
||||
"components/home/HomeTicketPanel.vue",
|
||||
"components/home/HomeTicketStat.vue",
|
||||
"components/home/HomeWalletCard.vue",
|
||||
"components/home/RoxyMascot.vue",
|
||||
"components/instance/CloudAccessExtras.vue",
|
||||
"components/instance/CommonManage.vue",
|
||||
"components/instance/CommonUpgrade.vue",
|
||||
"components/instance/DcimActions.vue",
|
||||
"components/instance/DcimBilling.vue",
|
||||
"components/instance/DcimDefence.vue",
|
||||
"components/instance/DcimDomains.vue",
|
||||
"components/instance/DcimManage.vue",
|
||||
"components/instance/DcimMonitor.vue",
|
||||
"components/instance/DcimReinstallStatus.vue",
|
||||
"components/instance/DcimUpgrade.vue",
|
||||
"components/instance/FlowPacketPanel.vue",
|
||||
"components/instance/ImagePurchaseDialog.vue",
|
||||
"components/instance/InstanceAccess.vue",
|
||||
"components/instance/InstanceBilling.vue",
|
||||
"components/instance/InstanceDisks.vue",
|
||||
"components/instance/InstanceLoadingShell.vue",
|
||||
"components/instance/InstanceManage.vue",
|
||||
"components/instance/InstanceMonitor.vue",
|
||||
"components/instance/InstanceMonitorSkeleton.vue",
|
||||
"components/instance/InstanceNetwork.vue",
|
||||
"components/instance/InstanceOnDemand.vue",
|
||||
"components/instance/InstanceOverview.vue",
|
||||
"components/instance/InstanceRenewDialog.vue",
|
||||
"components/instance/InstanceServiceInfo.vue",
|
||||
"components/instance/InstanceSmsCode.vue",
|
||||
"components/instance/InstanceStatusTag.vue",
|
||||
"components/instance/InstanceTraffic.vue",
|
||||
"components/instance/LegacyDcimActions.vue",
|
||||
"components/instance/LegacyDcimManage.vue",
|
||||
"components/instance/LegacyProductUpgrade.vue",
|
||||
"components/instance/RemfUpgrade.vue",
|
||||
"components/instance/RemoteConsole.vue",
|
||||
"components/kkyecs/KkyUpgrade.vue",
|
||||
"components/mfcloud/MfCloudDefence.vue",
|
||||
"components/mfcloud/MfCloudIps.vue",
|
||||
"components/mfcloud/MfCloudLogs.vue",
|
||||
"components/mfcloud/MfCloudNat.vue",
|
||||
"components/mfcloud/MfCloudResourceUpgrade.vue",
|
||||
"components/mfcloud/MfCloudUpgrade.vue",
|
||||
"components/mfcloud/MfCloudVpc.vue"
|
||||
],
|
||||
"layouts": [
|
||||
"layouts/blank.vue",
|
||||
"layouts/default.vue",
|
||||
"layouts/official-site.vue",
|
||||
"layouts/public-order.vue"
|
||||
],
|
||||
"shared": [
|
||||
"app/lib/_plugin-vue_export-helper.js",
|
||||
"app/lib/auth-redirect.js",
|
||||
"app/lib/auth.js",
|
||||
"app/lib/backend-menu.js",
|
||||
"app/lib/bandwidth-display.js",
|
||||
"app/lib/cart-edit.js",
|
||||
"app/lib/cloud-instance-management.js",
|
||||
"app/lib/common-advanced-rules.js",
|
||||
"app/lib/complete-list.js",
|
||||
"app/lib/composables-2.js",
|
||||
"app/lib/configuration-display.js",
|
||||
"app/lib/content-route.js",
|
||||
"app/lib/dict.js",
|
||||
"app/lib/finance-resource-chart.js",
|
||||
"app/lib/instance-data.js",
|
||||
"app/lib/instance-status.js",
|
||||
"app/lib/interval.js",
|
||||
"app/lib/invoice.js",
|
||||
"app/lib/modules.js",
|
||||
"app/lib/navigation-state.js",
|
||||
"app/lib/oauth.js",
|
||||
"app/lib/order-image-continuation.js",
|
||||
"app/lib/os-name.js",
|
||||
"app/lib/password-2.js",
|
||||
"app/lib/product-description-data.js",
|
||||
"app/lib/product-fields.js",
|
||||
"app/lib/product-name.js",
|
||||
"app/lib/product-order-module.js",
|
||||
"app/lib/purchase-limit-state.js",
|
||||
"app/lib/remote-console.js",
|
||||
"app/lib/renewal-quote.js",
|
||||
"app/lib/rolldown-runtime.js",
|
||||
"app/lib/sanitize.js",
|
||||
"app/lib/server-page.js",
|
||||
"app/lib/storage-capacity.js",
|
||||
"app/lib/ticketAttachments.js",
|
||||
"app/lib/useCaptcha.js",
|
||||
"app/lib/useListPage.js",
|
||||
"app/lib/useMailbox.js",
|
||||
"app/lib/useOAuth.js",
|
||||
"app/lib/useOfficialSiteContent.js",
|
||||
"app/lib/useSiteConfig.js",
|
||||
"app/lib/useTheme.js",
|
||||
"app/lib/useVerifyCode.js",
|
||||
"app/lib/useWithdrawAvailability.js",
|
||||
"app/lib/wechat-binding.js"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"source": "pages/account/realname/company.vue",
|
||||
"path": "/account/realname/company"
|
||||
},
|
||||
{
|
||||
"source": "pages/account/realname/personal.vue",
|
||||
"path": "/account/realname/personal"
|
||||
},
|
||||
{
|
||||
"source": "pages/orders/order/paid.vue",
|
||||
"path": "/orders/order/paid"
|
||||
},
|
||||
{
|
||||
"source": "pages/orders/order/pay.vue",
|
||||
"path": "/orders/order/pay"
|
||||
},
|
||||
{
|
||||
"source": "pages/products/dedicated-server/customize.vue",
|
||||
"path": "/products/dedicated-server/customize"
|
||||
},
|
||||
{
|
||||
"source": "pages/account/api-keys.vue",
|
||||
"path": "/account/api-keys"
|
||||
},
|
||||
{
|
||||
"source": "pages/account/connections.vue",
|
||||
"path": "/account/connections"
|
||||
},
|
||||
{
|
||||
"source": "pages/account/realname/index.vue",
|
||||
"path": "/account/realname"
|
||||
},
|
||||
{
|
||||
"source": "pages/invoices/invoice.vue",
|
||||
"path": "/invoices/invoice"
|
||||
},
|
||||
{
|
||||
"source": "pages/invoices/request.vue",
|
||||
"path": "/invoices/request"
|
||||
},
|
||||
{
|
||||
"source": "pages/invoices/settings/index.vue",
|
||||
"path": "/invoices/settings"
|
||||
},
|
||||
{
|
||||
"source": "pages/oauth/bind.vue",
|
||||
"path": "/oauth/bind"
|
||||
},
|
||||
{
|
||||
"source": "pages/oauth/callback.vue",
|
||||
"path": "/oauth/callback"
|
||||
},
|
||||
{
|
||||
"source": "pages/orders/order/index.vue",
|
||||
"path": "/orders/order"
|
||||
},
|
||||
{
|
||||
"source": "pages/products/cloud-server.vue",
|
||||
"path": "/products/cloud-server"
|
||||
},
|
||||
{
|
||||
"source": "pages/products/dedicated-server/index.vue",
|
||||
"path": "/products/dedicated-server"
|
||||
},
|
||||
{
|
||||
"source": "pages/security/domains/index.vue",
|
||||
"path": "/security/domains"
|
||||
},
|
||||
{
|
||||
"source": "pages/security/groups/index.vue",
|
||||
"path": "/security/groups"
|
||||
},
|
||||
{
|
||||
"source": "pages/security/ssh-keys/index.vue",
|
||||
"path": "/security/ssh-keys"
|
||||
},
|
||||
{
|
||||
"source": "pages/servers/server.vue",
|
||||
"path": "/servers/server"
|
||||
},
|
||||
{
|
||||
"source": "pages/tickets/create.vue",
|
||||
"path": "/tickets/create"
|
||||
},
|
||||
{
|
||||
"source": "pages/tickets/ticket.vue",
|
||||
"path": "/tickets/ticket"
|
||||
},
|
||||
{
|
||||
"source": "pages/bills/[id].vue",
|
||||
"path": "/bills/:id()"
|
||||
},
|
||||
{
|
||||
"source": "pages/orders/[id].vue",
|
||||
"path": "/orders/:id()"
|
||||
},
|
||||
{
|
||||
"source": "pages/products/[id].vue",
|
||||
"path": "/products/:id()"
|
||||
},
|
||||
{
|
||||
"source": "pages/tickets/[id].vue",
|
||||
"path": "/tickets/:id()"
|
||||
},
|
||||
{
|
||||
"source": "pages/account/index.vue",
|
||||
"path": "/account"
|
||||
},
|
||||
{
|
||||
"source": "pages/bills/index.vue",
|
||||
"path": "/bills"
|
||||
},
|
||||
{
|
||||
"source": "pages/cart/index.vue",
|
||||
"path": "/cart"
|
||||
},
|
||||
{
|
||||
"source": "pages/cloud-servers/index.vue",
|
||||
"path": "/cloud-servers"
|
||||
},
|
||||
{
|
||||
"source": "pages/dedicated-servers/index.vue",
|
||||
"path": "/dedicated-servers"
|
||||
},
|
||||
{
|
||||
"source": "pages/finance/index.vue",
|
||||
"path": "/finance"
|
||||
},
|
||||
{
|
||||
"source": "pages/forgot-password.vue",
|
||||
"path": "/forgot-password"
|
||||
},
|
||||
{
|
||||
"source": "pages/help.vue",
|
||||
"path": "/help"
|
||||
},
|
||||
{
|
||||
"source": "pages/home.htm.vue",
|
||||
"path": "/home.htm"
|
||||
},
|
||||
{
|
||||
"source": "pages/invoices/index.vue",
|
||||
"path": "/invoices"
|
||||
},
|
||||
{
|
||||
"source": "pages/logbook/index.vue",
|
||||
"path": "/logbook"
|
||||
},
|
||||
{
|
||||
"source": "pages/login.vue",
|
||||
"path": "/login"
|
||||
},
|
||||
{
|
||||
"source": "pages/messages/index.vue",
|
||||
"path": "/messages"
|
||||
},
|
||||
{
|
||||
"source": "pages/news.vue",
|
||||
"path": "/news"
|
||||
},
|
||||
{
|
||||
"source": "pages/notices/index.vue",
|
||||
"path": "/notices"
|
||||
},
|
||||
{
|
||||
"source": "pages/official-site.vue",
|
||||
"path": "/official-site"
|
||||
},
|
||||
{
|
||||
"source": "pages/orders/index.vue",
|
||||
"path": "/orders"
|
||||
},
|
||||
{
|
||||
"source": "pages/products/index.vue",
|
||||
"path": "/products"
|
||||
},
|
||||
{
|
||||
"source": "pages/recommend/index.vue",
|
||||
"path": "/recommend"
|
||||
},
|
||||
{
|
||||
"source": "pages/register.vue",
|
||||
"path": "/register"
|
||||
},
|
||||
{
|
||||
"source": "pages/remote-console.vue",
|
||||
"path": "/remote-console"
|
||||
},
|
||||
{
|
||||
"source": "pages/resources.vue",
|
||||
"path": "/resources"
|
||||
},
|
||||
{
|
||||
"source": "pages/tickets/index.vue",
|
||||
"path": "/tickets"
|
||||
},
|
||||
{
|
||||
"source": "pages/transfer/index.vue",
|
||||
"path": "/transfer"
|
||||
},
|
||||
{
|
||||
"source": "pages/vouchers/index.vue",
|
||||
"path": "/vouchers"
|
||||
},
|
||||
{
|
||||
"source": "pages/index.vue",
|
||||
"path": "/"
|
||||
}
|
||||
],
|
||||
"handwrittenTemplateBatch": {
|
||||
"baseline": "5cd7210a1df33c97cba4c863e9633ace58e1472f963105ae93af46c607ce4f50",
|
||||
"sourceCommit": "7f612c7e682a48d5e4b775b2ecb91234562f2b2c0cb9e9d063811d7ee18fb929",
|
||||
"files": [
|
||||
"app/pages/login.vue",
|
||||
"app/pages/home.htm.vue",
|
||||
"app/pages/security/ssh-keys/index.vue",
|
||||
"app/layouts/blank.vue"
|
||||
],
|
||||
"productNetLines": -863,
|
||||
"scope": "This batch only; other recovered business modules still contain render functions",
|
||||
"acceptance": "21/21 offline browser scenarios; see acceptance.json"
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
# 完整前端项目交接说明
|
||||
|
||||
核对日期:2026-11-05。
|
||||
|
||||
## 交付的是什么
|
||||
|
||||
交付的是整个会员中心的独立 Nuxt 前端工程,而不是仅含登录或 SSH 页面的小样例。
|
||||
“四个文件已手写重建”指第二轮的维护进度,不是压缩包中只有四个文件。
|
||||
|
||||
| 内容 | 数量与说明 |
|
||||
| -------------- | ------------------------------------------------------------------------- |
|
||||
| 页面 | 52 个,路由清单见 `module-inventory.json` |
|
||||
| 业务组件 | 104 个 |
|
||||
| 布局 | 4 套 |
|
||||
| Vue 文件 | 161 个,含应用入口 |
|
||||
| JS/TS 应用模块 | 55 个,含接口、会话、插件、路由保护与配置 |
|
||||
| 样式文件 | 137 个 |
|
||||
| public 资源 | 285 个,含图片、字体和站点资源 |
|
||||
| 工程文件 | npm 依赖与锁文件、Nuxt 配置、示例环境配置、检查脚本、浏览器测试和验收记录 |
|
||||
|
||||
完整性检查未发现丢失的本地依赖或资源:4,647 处应用模块导入、252 处 CSS 本地资源引用均可解析。
|
||||
上述数目说明文件覆盖,不等于所有业务分支都已经真实后端验收。
|
||||
|
||||
## 与服务器当前版本的关系
|
||||
|
||||
经用户授权,只读检查 `/www/wwwroot/crashwork-console`,没有修改文件、重启服务或部署。
|
||||
检查时 `.output` 指向 `releases/20261025-kky-traffic-display/.output`。
|
||||
当前发布目录仅含 `.output` 与发布指向记录;检查了 130 个发布目录中构建目录之外、深度不超过五层的源码候选,
|
||||
未找到原始 `.vue`/`.tsx`/`.jsx` 页面或 Nuxt/Vite 工程配置。没有读取根 `.env` 或导入数据库备份。
|
||||
|
||||
服务器与本地重建依据的校验结果:
|
||||
|
||||
- `server/chunks/virtual/precomputed.mjs` SHA-256 一致:`e9a8d0e2be5f7e2263961e9f128bb6e89955d42814e5a7a0c43ae85c5a6327e1`。
|
||||
- 元数据列出的 735 个唯一模块资源均存在,按文件名及内容 SHA-256 聚合后的摘要一致:`08144c47f9dda33a859105d5a746898feb288dd5b0c4ffb7464cf65fd704ed29`。
|
||||
- 这证明重建依据与检查时服务器的模块构建一致;不证明真实供应商接口、全部服务器文件或线上业务均已验收。
|
||||
|
||||
## 员工如何使用
|
||||
|
||||
在本工程根目录使用 Node.js 22.12+:
|
||||
|
||||
```sh
|
||||
npm ci
|
||||
cp .env.example .env
|
||||
npm run dev
|
||||
```
|
||||
|
||||
配置 `NUXT_PUBLIC_API_BASE` 为后端 origin,不附加 `/console/v1`。
|
||||
留空时需要同源反向代理;`/captcha/`、`/remote-console/sessions` 及其 WebSocket 还需独立反代配置。
|
||||
后端服务不会随此前端工程自动启动。不要把生产 root 密码或其他秘密填入前端公开配置。
|
||||
|
||||
```sh
|
||||
npm run check
|
||||
npm run build
|
||||
npm test
|
||||
```
|
||||
|
||||
产物在本工程 `.output/`;运行方式及 Chrome 路径配置见根 README 和 `tests/README.md`。
|
||||
已有干净解压安装、构建及 21 项真实浏览器场景通过的记录,见 `validation.md` 和 `acceptance.json`。
|
||||
本次服务器版本核对后再次执行源码检查、生产构建及 21 项浏览器场景,全部通过,浏览器运行错误和外网请求均为零。
|
||||
|
||||
## 不能混淆的边界
|
||||
|
||||
- 这是完整可编辑、可构建的重建工程,不是原始源码的逐字恢复。49 个页面、103 个组件和 3 个布局仍使用恢复的渲染函数,合计 155 个 Vue 文件;并非文件缺失,也不能称为全站手写模板完成。
|
||||
- 原模板、完整变量名、注释和原始业务需求文档不在服务器发布包中;不承诺逆向恢复这些已丢失的信息。
|
||||
- 不包含后端、数据库、远控网关服务或生产反向代理配置;验证码、OAuth、支付、财务、供应商和 VNC 仍需测试环境联调。
|
||||
- 保留当前登录协议及已记录的技术债,包括依赖告警和认证页主题切换文案,见 `defect-ledger.json`。
|
||||
- 不含生产环境文件、登录凭证、Git 历史、`node_modules` 和构建产物;不要把根服务器目录或历史 releases 打包发给员工。
|
||||
|
||||
最小交付原则:在已可构建工程上补充核对证据,不增加产品字段、依赖、配置或另一套前端实现;
|
||||
路由、资源和依赖由现有清单确定,不要求员工在历史发布目录中手工挑选版本。
|
||||
@@ -1,21 +0,0 @@
|
||||
# 前端源码重建方案
|
||||
|
||||
范围:当前 20261025 发布包的 52 个页面、4 个布局、104 个业务组件及其共享业务逻辑。新工程自包含,不引用历史发布目录,不替换线上发布指向。
|
||||
|
||||
最小设计检查:
|
||||
|
||||
1. 结构:建立独立可构建源码工程,解决源码缺失,而非在压缩产物上继续打补丁。
|
||||
2. 调用负担:保持现有后端路径、请求字段及页面 URL,不要求后端新增协议。
|
||||
3. 可推导信息:路由和模块清单从当前有效构建清单提取,不让使用者手工选择旧版文件。
|
||||
4. 复杂度:沿用 Nuxt、Vue、PrimeVue;框架和组件库使用正式依赖,只恢复业务逻辑和现有样式。
|
||||
5. 适用范围:覆盖当前入口的完整模块图,不只复刻登录页或几个示例页面。
|
||||
|
||||
最小可行方案是保留已发布业务行为,将编译后的业务模块恢复为格式化、具有业务路径和显式依赖的 Vue 渲染函数源码,并重建应用入口、框架依赖、构建清单与测试。原始变量名、注释和模板排版已在构建中丢失,不能宣称逐字恢复原始工程。Vue 单文件组件允许使用 render/setup 返回的渲染函数;不把旧发布包作为运行时依赖。
|
||||
|
||||
缺陷类别:异步操作的等待没有统一的时间边界,界面生命周期被远端响应决定;通用规则是请求有明确截止时间,界面清理由本地状态所有者完成。另一类是完整搜索结果与当前页结果混用,规则是只由一处分页逻辑生成当前可见集合。
|
||||
|
||||
验证:构建新工程;使用本地 HTTP 后端替身验证登录、鉴权、SSH 搜索分页、注销、支付等待边界及页面路由加载。真实支付、供应商实例和生产反代不在离线替身中伪称验证通过。
|
||||
|
||||
第二轮按维护边界逐页改成手写 setup 与模板:登录、旧平台登录入口、SSH 密钥及公共认证布局。
|
||||
保持原协议与样式,不运行一次性导入器、不覆盖历史发布产物。四个产品文件净减少 863 行;
|
||||
新增/扩展真实页面回归后为 21 场景。其余恢复模块仍按需逐页改写,不将这一批等同于全站重写。
|
||||
@@ -1,96 +0,0 @@
|
||||
# 重建与验收记录
|
||||
|
||||
日期:2026-11-05。基准:当前 20261025 发布版本;没有读取 `.env`,没有连接生产服务器或使用生产凭证。
|
||||
|
||||
## 可确认的范围
|
||||
|
||||
清单包含 52 个页面、104 个组件、4 个布局;另重写应用入口。所有业务文件采用源码路径和显式模块依赖,
|
||||
不从历史 `.output`、`releases` 或旧哈希 JS 加载代码。保留编译后的作用域样式,以维持现有视觉规则。
|
||||
原注释、模板排版、完整局部变量名不能逆向恢复;多数页面仍使用渲染函数,应逐步按实际维护需求重构。
|
||||
|
||||
源文件的解析/依赖门禁与生产构建覆盖全部 Vue 文件,但不等于所有业务交互都已验收。
|
||||
逐文件覆盖台账另见 `audit-coverage.json`;未逐行人工审查的业务文件明确标记为未审查。
|
||||
|
||||
## 修复记录
|
||||
|
||||
| 问题 | 原因和处理 | 验证方式 |
|
||||
| ------------------------ | ------------------------------------------------------------------------ | ------------------------------------- |
|
||||
| 请求永久等待 | JSON 默认 30 秒、上传/下载 120 秒;截止时间覆盖响应正文和购买前置校验 | 真实页面的挂起请求与超时场景 |
|
||||
| 注销被网络拖住 | 先捕获旧 Bearer,立即清理本地会话,后台有限时撤销 | `/logout` 不返回时仍退出 |
|
||||
| 支付截止被挂起轮询吞掉 | 一个轮询会话拥有独立五分钟截止与单请求超时,关闭/换单/换身份使旧响应失效 | 浏览器外部时钟推进、无重复支付 |
|
||||
| SSH 搜索翻页重复 | 区分服务端分页和完整搜索集合,仅由可见行计算切片 | 普通分页、搜索 10/10/5、查询切换/清空 |
|
||||
| 重建入口改变登录生命周期 | 登录及旧平台入口在认证过程中保持组件身份;保护页面随会话失效卸载 | 登录和旧入口场景 |
|
||||
| 会话失效后重复初始化 | 菜单、支付初始化由当前登录身份控制 | 401 与退出场景 |
|
||||
| 图标依赖外网 | 本地打包 Lucide,禁用外部回退 | 所有场景禁止访问外网 |
|
||||
| 组件库许可提示 | 换用 MIT 版本,不修改许可校验;主题也引用 MIT 包 | 可见 UI 许可检查 |
|
||||
| 框架运行时混用 | Vue、共享函数、Router 版本统一;替换旧私有注入标识 | 实际启动构建服务及浏览器验收 |
|
||||
| 跨标签页会话不一致 | 请求与界面使用同一个响应式身份;监听存储变动使其他标签页同步退出 | 第二标签页退出后第一个标签页被保护 |
|
||||
| 旧入口凭证回到地址栏 | 同时清理 history 与路由的规范地址,失败页面也不恢复敏感查询 | 旧凭证被拒绝后检查 URL 与 Referrer |
|
||||
|
||||
首轮已在真实构建上重现 SSH、注销和支付三个缺陷;修复后基础 11 场景通过。
|
||||
第二轮最终生产构建通过,扩展 21 个浏览器场景全部通过,无浏览器运行错误和外网请求。
|
||||
结果见随本次交付保留的 `acceptance.json`、`acceptance-assets/` 截图和 `acceptance-requests.json` 脱敏请求顺序。
|
||||
已修复问题与剩余债务另见 `defect-ledger.json`;这不是整站逐行审查或生产业务验收完成的声明。
|
||||
首轮源码包曾在仓库之外的全新临时目录重新执行 `npm ci`、检查、构建与当时的 18 个浏览器场景,全部通过;
|
||||
本轮 v2 包的独立验证记录见下文,不把首轮结果当作本轮验收。
|
||||
|
||||
## 第二轮:手写模板重建
|
||||
|
||||
本轮仅改写 `app/pages/login.vue`、`app/pages/home.htm.vue`、`app/pages/security/ssh-keys/index.vue`
|
||||
和 `app/layouts/blank.vue`,从恢复的渲染函数改成可维护的 setup 与模板;不是在旧 render 外再包一层模板。
|
||||
登录保留密码/短信配置、60 秒验证码倒计时、CAPTCHA/安全验证续登、OAuth 组件和安全跳转;
|
||||
旧入口保留凭证验证、地址脱敏、重试与卸载取消;SSH 保留服务端分页、完整集合搜索及增改删,清理卸载后的计时器和迟到结果。
|
||||
认证布局保留主题、站点配置、logo、redirect 和 slot。原样式的作用域接缝已逐项核对。
|
||||
|
||||
最小设计复核:以逐页等价替换解决可读性问题;不新增调用方字段或配置;路由与派生状态沿用已有来源;
|
||||
不另建渲染/请求抽象或第二条实现路径;重写覆盖每个选定页面的完整模板,不为测试添加产品开关。
|
||||
净行数:产品 -863(登录 -390、旧入口 -80、SSH -293、布局 -100),测试 +348,增长用于三个真实页面行为场景及旧场景扩展;依赖/锁文件零变动。
|
||||
|
||||
合并后执行 `npm run check`、`npm run build`、`npm test`,161 个 Vue 文件和 55 个模块门禁通过,21/21 浏览器场景通过。
|
||||
新增覆盖:短信独占配置及倒计时、禁用短信时的默认登录方式、SSH 创建/编辑/删除末页恢复;
|
||||
原旧入口失败场景扩展为真实点击重试,并检查不恢复 URL 凭证、不重新密码登录及无 Referrer。
|
||||
测试仍通过真实生产构建和独立 HTTP 假后端,不替换内部模块、不预置 JWT、不访问生产服务。
|
||||
|
||||
本轮交叉复核完整读取四个新文件及相关样式;登录组件、安全验证、会话和路由接缝按台账记录读取范围。
|
||||
最新质量复核未发现新增确认回退;详细原始业务规范并不存在,不能宣称全部原规范逐条一致。
|
||||
本轮已验证的公开入口为 `/login`、`/home.htm` 与 `/security/ssh-keys`,未因此提升其他页面的人工审查/功能覆盖状态。
|
||||
窄屏补查确认认证页和 SMS 表单在 320/390px 下不横向溢出,SSH 编辑弹窗在 390px 下宽 366px;
|
||||
核对预填、取消、密码显示切换、注册/重置提示及 redirect 链接。截图证据随 `acceptance.json` 归档。
|
||||
另确认一个既有主题文案问题:浅色系统下默认 auto 首次循环到 light,按钮虽写“切换深色模式”却仍浅色;
|
||||
基准版本行为相同,作为 `AUTH_THEME_CYCLE_LABEL` 有界债务保留,不把首次点击截图当作深色验收。
|
||||
门禁盲区:配置加载失败后重载、CAPTCHA/安全挑战/实名认证续登、真实 OAuth、SSH 剪贴板/失败响应/并发操作、
|
||||
旧入口卸载时取消等分支尚未被这 21 个场景完整覆盖;代码复核不代替这些分支的真实交互验证,也无证据将其判定为已坏。
|
||||
|
||||
v2 源码候选包已通过独立干净目录验收:从提交 `6c73598` 仅导出 `frontend/`,解压到仓库外,
|
||||
重新执行 `npm ci`、源码检查、生产构建和 21 个浏览器场景,全部通过;稳定的场景结果与主工作树一致。
|
||||
导出包含 686 个文件,无符号链接,不含 `.env`、Git 历史、依赖目录、生成构建和数据库备份。
|
||||
此后仅补充文档与台账,没有改变已验收产品、依赖或测试。12 项低/中危传递依赖告警仍在债务清单中。
|
||||
|
||||
结构性修复扫描:
|
||||
|
||||
```sh
|
||||
rg -n 'fetch\(|AbortController|timeoutMs|logout|legacyLogin|useApi\(' app --glob '*.js' --glob '*.vue'
|
||||
rg -n '/pay/.*status|setInterval\(|complete_list_n' app --glob '*.js' --glob '*.vue'
|
||||
```
|
||||
|
||||
接口核心 JSON、上传、下载、旧平台身份验证共四个传输入口统一有限时;验证码刷新和 VNC 会话使用各自
|
||||
既有的交互生命周期,不宣称已完成全局网络子系统重写。支付状态只有一个共享轮询所有者,已修复。
|
||||
其余服务器刷新、验证码倒计时和实例查询不是同一个“支付绝对期限被吞掉”的缺陷,不在此改动中混改。
|
||||
|
||||
## 仍需联调 / 有界技术债
|
||||
|
||||
规模说明:原仓库没有 `frontend/`,新增的是已有业务的独立源码表达,不是同等数量的新功能。
|
||||
相对重建前基准,应用源码约 +123,394 行、现有样式展开后 +19,836 行、测试约 +1,250 行;
|
||||
构建/锁文件/工具约 +9,964 行,静态资产文本约 +2,605 行,另保留二进制图片和字体。
|
||||
文档、截图与逐文件台账单独计量,不作为产品逻辑。较大体量主要来自恢复全部业务渲染函数和将压缩样式格式化;
|
||||
没有为每个页面另造一套框架、请求系统或测试专用产品开关。不能因编译全绿就省略后续按业务逐页维护与联调。
|
||||
|
||||
- 真实下单、续费/升级、发票、财务、安全验证、短信/邮件、第三方 CAPTCHA/OAuth、真实支付、VNC 握手及生产反代:未使用生产环境验证。
|
||||
- `/captcha/` 及 `/remote-console/sessions` 为同源接口,必须在开发/测试部署另行配置反代,不能仅依赖 API origin。
|
||||
- 原远程控制网关属于独立服务。本工程保留客户端协议,但没有重写或部署该服务;此前审查发现的网关配额并发竞争仍需单独修复。
|
||||
- 浏览器回归采用外部 HTTP 假后端,不能代替供应商接口兼容性验收;52 个页面可构建不代表所有分支、每种响应字段都已人工核对。
|
||||
- 默认保留 JWT 持久化方式与登录 AES 协议,未进行会影响后端契约的身份协议升级;CryptoJS 已停止维护,应在前后端协同变更时迁移。
|
||||
- `npm audit` 的传递依赖低/中危告警仍保留,具体结果见 `dependency-audit.json`;没有盲目使用 `--force` 降级框架。对外上线前应复核受影响路径与补丁版本。
|
||||
- 大部分恢复页面仍含短局部变量和较长渲染函数,非逐字原始开发源码,也非每个页面都已重新手写模板。人工深度审查集中在入口、会话、支付、分页和远控边界。
|
||||
|
||||
旧服务器及数据库备份未导入新前端,没有覆盖原发布目录。新工程的所有变更仅限 `frontend/`。
|
||||
@@ -1,26 +1,34 @@
|
||||
/** 应用构建入口;所有运行时业务地址来自公开配置,不绑定历史服务器目录。 */
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: "2026-10-25",
|
||||
// 页面依赖浏览器存储、DOM 及远控能力,使用客户端 SPA,而非服务端渲染页面。
|
||||
ssr: false,
|
||||
devtools: { enabled: false },
|
||||
modules: ["@nuxt/icon"],
|
||||
icon: {
|
||||
// 图标只读取本地打包的 Lucide 集合,不在缺失时向公共图标 API 发请求。
|
||||
provider: "server",
|
||||
serverBundle: { collections: ["lucide"] },
|
||||
fallbackToApi: false,
|
||||
},
|
||||
// 恢复的源码使用显式 import,避免自动扫描带来同名组件或隐式依赖。
|
||||
components: false,
|
||||
imports: { autoImport: false },
|
||||
css: [
|
||||
// 只放全站共享样式;各业务组件显式导入自己的带作用域样式。
|
||||
"~/assets/styles/entry.Cs9kvuGi.css",
|
||||
"~/assets/styles/BuildUpdateNotice.CuqdqvlF.css",
|
||||
"~/assets/styles/OperationToasts.DfA_b8om.css",
|
||||
],
|
||||
// NUXT_PUBLIC_API_BASE 只填后端 origin,/console/v1 前缀由请求层追加。
|
||||
// public 会下发到浏览器,禁止在这里存放服务端密码或访问令牌。
|
||||
runtimeConfig: { public: { apiBase: "" } },
|
||||
app: {
|
||||
head: {
|
||||
htmlAttrs: { lang: "zh-CN" },
|
||||
title: "破碎工坊云 - 会员中心",
|
||||
link: [
|
||||
// 字体和站点图标随 public/ 分发,离线测试和私有部署不依赖外部 CDN。
|
||||
{ rel: "stylesheet", href: "/fonts/harmonyos-sans-sc/Regular.css" },
|
||||
{ rel: "stylesheet", href: "/fonts/harmonyos-sans-sc/Semibold.css" },
|
||||
{ rel: "stylesheet", href: "/fonts/harmonyos-sans-sc/Bold.css" },
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* 静态源码门禁:解析应用的 JS/TS/Vue,检查显式导入和机器/旧部署路径依赖。
|
||||
* 这是构建前检查,不替代浏览器业务验收,也不会启动或修改生产服务。
|
||||
*/
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { createRequire } from "node:module";
|
||||
@@ -13,6 +17,7 @@ const require = createRequire(import.meta.url);
|
||||
const failures = [];
|
||||
const files = [];
|
||||
function walk(directory) {
|
||||
// 仅递归 app/;不扫描 node_modules、构建产物或本地 docs 报告。
|
||||
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
||||
const file = path.join(directory, entry.name);
|
||||
if (entry.isDirectory()) walk(file);
|
||||
@@ -27,11 +32,13 @@ const dependencies = new Set(
|
||||
Object.keys({ ...manifest.dependencies, ...manifest.devDependencies }),
|
||||
);
|
||||
function existsModule(file) {
|
||||
// 与当前源码使用的简写导入形式对齐,允许目录下的 index.js / index.ts。
|
||||
return ["", ".js", ".ts", ".json", ".vue", "/index.js", "/index.ts"].some(
|
||||
(extension) => fs.existsSync(file + extension),
|
||||
);
|
||||
}
|
||||
function checkImport(source, owner) {
|
||||
// # 开头的虚拟模块由 Nuxt 生成,本地文件与 npm 包则分别核对实际路径及依赖声明。
|
||||
if (source.startsWith("#")) return; // Nuxt-generated public/virtual modules.
|
||||
if (source.startsWith("~/")) {
|
||||
if (!existsModule(path.join(root, "app", source.slice(2))))
|
||||
@@ -53,6 +60,7 @@ function checkImport(source, owner) {
|
||||
}
|
||||
}
|
||||
function inspect(node, owner) {
|
||||
// 按 AST 检查,不把注释中的历史说明误判成运行时路径依赖。
|
||||
if (!node || typeof node !== "object") return;
|
||||
if (
|
||||
[
|
||||
@@ -64,6 +72,7 @@ function inspect(node, owner) {
|
||||
)
|
||||
checkImport(node.source.value, owner);
|
||||
if (node.type === "CallExpression" && node.callee.type === "Import") {
|
||||
// 静态字符串的懒加载同样要验证;动态计算的路径不在本门禁可证明范围内。
|
||||
const value = node.arguments[0];
|
||||
if (value.type === "StringLiteral") checkImport(value.value, owner);
|
||||
}
|
||||
@@ -81,6 +90,7 @@ function inspect(node, owner) {
|
||||
`${owner}:${node.loc?.start.line ?? 1}: runtime depends on deployment/machine path`,
|
||||
);
|
||||
for (const [key, value] of Object.entries(node)) {
|
||||
// 跳过位置和注释等元数据,仅递归语法节点,避免重复或无意义遍历。
|
||||
if (["loc", "start", "end", "extra", "comments", "tokens"].includes(key))
|
||||
continue;
|
||||
if (Array.isArray(value)) for (const item of value) inspect(item, owner);
|
||||
@@ -94,6 +104,7 @@ for (const file of files) {
|
||||
const source = fs.readFileSync(file, "utf8");
|
||||
try {
|
||||
if (file.endsWith(".vue")) {
|
||||
// script setup 先展开编译宏,再解析脚本;有 template 时另外检查模板编译错误。
|
||||
components++;
|
||||
const { descriptor, errors } = parseVue(source, { filename: file });
|
||||
if (errors.length) throw Error(errors.join("\n"));
|
||||
@@ -129,6 +140,7 @@ for (const file of files) {
|
||||
failures.push(`${file}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
// 一次报告全部失败并以非零状态退出,方便开发者和 CI 定位多个问题。
|
||||
if (failures.length) {
|
||||
console.error(
|
||||
failures.map((failure) => failure.replaceAll(root + "/", "")).join("\n"),
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
/** One-time source reconstruction. Input is explicit; generated app has no runtime link to it. */
|
||||
/**
|
||||
* 一次性重建留档,不参与日常开发、构建或发布。
|
||||
* 输入必须是可信的自有 Nuxt 发布目录:脚本会 import 其中的元数据,因此不是安全的第三方解包器。
|
||||
* 只在空工程执行;已有 app/pages 时直接拒绝,避免覆盖后续维护的源码和注释。
|
||||
*/
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
@@ -12,6 +17,7 @@ import { format } from "prettier";
|
||||
const generate = generator.default;
|
||||
const traverse = traverser.default;
|
||||
const input = path.resolve(process.argv[2] || "MISSING_INPUT");
|
||||
// 所有输出定位到本工程;不向输入发布目录回写,也不改变生产发布软链接。
|
||||
const root = path.resolve(import.meta.dirname, "..");
|
||||
if (!fs.existsSync(path.join(input, "server/chunks/virtual/precomputed.mjs")))
|
||||
throw Error("Pass the active .output directory");
|
||||
@@ -19,6 +25,7 @@ if (fs.existsSync(path.join(root, "app/pages")))
|
||||
throw Error(
|
||||
"Refusing to overwrite editable source; this importer is only for a new empty project",
|
||||
);
|
||||
// 这一步会执行输入模块,调用方必须先验证输入可信性。
|
||||
const pc = (
|
||||
await import(
|
||||
pathToFileURL(path.join(input, "server/chunks/virtual/precomputed.mjs"))
|
||||
@@ -26,6 +33,7 @@ const pc = (
|
||||
).default;
|
||||
const chunkDir = path.join(input, "public/_nuxt");
|
||||
const asts = new Map();
|
||||
// 先解析元数据声明的脚本,再按导入图选择业务模块,不直接复制旧框架 bundle。
|
||||
const files = [
|
||||
...new Set(
|
||||
Object.values(pc.modules)
|
||||
@@ -46,6 +54,7 @@ const textValue = (n) =>
|
||||
? n.quasis[0].value.cooked
|
||||
: n?.value;
|
||||
function resolve(file, exported, seen = new Set()) {
|
||||
// 顺着重导出定位真正的定义;seen 检测循环,缺失导出则终止而非猜测模块归属。
|
||||
if (seen.has(key(file, exported)))
|
||||
throw Error(`Cyclic export ${file}:${exported}`);
|
||||
seen.add(key(file, exported));
|
||||
@@ -84,6 +93,7 @@ function resolve(file, exported, seen = new Set()) {
|
||||
const replacements = new Map();
|
||||
const replace = (file, alias, source, imported) =>
|
||||
replacements.set(key(file, alias), { source, imported });
|
||||
// 以下短名映射仅适用于本次可信发布版本,不是跨版本通用的编译器符号表。
|
||||
const vue = {
|
||||
$: "watchSyncEffect",
|
||||
A: "nextTick",
|
||||
@@ -218,6 +228,7 @@ replace("DEP0rg1v.js", "t", "~/utils/brand.js", "logoUrl");
|
||||
replace("ecKN0m8a.js", "r", "#internal/nuxt/paths", "publicAssetsURL");
|
||||
|
||||
const owners = new Map();
|
||||
// 页面、布局和业务组件一一对应源码路径,重复归属会报错,避免静默覆盖。
|
||||
const own = Object.entries(pc.modules).filter(([p]) =>
|
||||
/^(components|layouts|pages)\//.test(p),
|
||||
);
|
||||
@@ -276,6 +287,7 @@ for (const file of files) {
|
||||
taken.add(output);
|
||||
}
|
||||
const authExports = {
|
||||
// 为已核实的认证公共接口恢复业务名;未知模块导出仍保留原有绑定。
|
||||
a: "loginErrorMessage",
|
||||
c: "ApiError",
|
||||
d: "apiCacheKey",
|
||||
@@ -296,6 +308,7 @@ const exportNames = new Map(
|
||||
Object.entries(authExports).map(([a, n]) => [key("BLvPXbVl.js", a), n]),
|
||||
);
|
||||
function lookup(file, exported) {
|
||||
// 优先使用官方包/手工适配映射,其余依赖落到当前 app/lib,而不是历史发布路径。
|
||||
const direct = replacements.get(key(file, exported));
|
||||
if (direct) return direct;
|
||||
const r = resolve(file, exported);
|
||||
@@ -312,6 +325,7 @@ const emitted = new Set();
|
||||
const pending = [...owners.keys()].filter((f) => f !== chartOwner?.[0]);
|
||||
const patchFiles = [];
|
||||
async function add(file, code, parser) {
|
||||
// 先收集格式化内容,所有代码文件最终通过 apply_patch 新增,失败即中止。
|
||||
const formatted = await format(code, {
|
||||
parser:
|
||||
parser ||
|
||||
@@ -360,6 +374,7 @@ for (const route of routes.elements) {
|
||||
route._meta = metaProps;
|
||||
}
|
||||
while (pending.length) {
|
||||
// 从业务入口遍历依赖闭包;emitted 去重,以免循环依赖反复生成同一文件。
|
||||
const file = pending.shift();
|
||||
if (emitted.has(file)) continue;
|
||||
emitted.add(file);
|
||||
@@ -379,6 +394,7 @@ while (pending.length) {
|
||||
);
|
||||
}
|
||||
if (file === "BLvPXbVl.js") {
|
||||
// 针对此发布包剥离内嵌的 Pinia/密码库实现,保留业务逻辑并接回正式 npm 依赖。
|
||||
// Business logic only; Pinia, file downloads and cryptography are npm dependencies.
|
||||
let keep = false;
|
||||
const body = [];
|
||||
@@ -442,6 +458,7 @@ while (pending.length) {
|
||||
});
|
||||
traverse(ast, {
|
||||
Program(p) {
|
||||
// 只重写被引用的导入绑定,同步改名所有引用以避免与当前作用域已有变量冲突。
|
||||
p.scope.crawl();
|
||||
for (const stmt of [...p.get("body")]) {
|
||||
if (!stmt.isImportDeclaration()) continue;
|
||||
@@ -510,6 +527,7 @@ while (pending.length) {
|
||||
});
|
||||
for (const dep of dependencies) if (!emitted.has(dep)) pending.push(dep);
|
||||
if (owner) {
|
||||
// 业务组件包装为可编辑 SFC,恢复路由元数据并显式导入原有作用域 CSS。
|
||||
const defaultExport = ast.program.body.find((n) =>
|
||||
t.isExportDefaultDeclaration(n),
|
||||
);
|
||||
@@ -564,6 +582,7 @@ await add(
|
||||
);
|
||||
await add(
|
||||
"docs/module-inventory.json",
|
||||
// 此清单是本地重建记录;docs 已被 Git 忽略,不应作为产品运行依赖或强制加入版本库。
|
||||
JSON.stringify(
|
||||
{
|
||||
pages: own.filter(([p]) => p.startsWith("pages/")).map(([p]) => p),
|
||||
@@ -584,6 +603,7 @@ await add(
|
||||
),
|
||||
);
|
||||
for (const { file, code } of patchFiles) {
|
||||
// apply_patch 只新增目标;不使用整目录替换,也不清理已有文件。
|
||||
const target = path.join(root, file);
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true });
|
||||
const patch = `*** Begin Patch\n*** Add File: ${target}\n${code
|
||||
@@ -598,6 +618,7 @@ for (const { file, code } of patchFiles) {
|
||||
}
|
||||
// Static binary assets retain their deployed paths; no JS bundle is copied.
|
||||
const cssFiles = [
|
||||
// 样式保留发布版 scope 选择器,避免在模板已丢失时重新分配作用域导致外观失配。
|
||||
...new Set(
|
||||
Object.values(pc.modules)
|
||||
.filter((v) => v.resourceType === "style")
|
||||
@@ -608,6 +629,7 @@ fs.mkdirSync(path.join(root, "app/assets/styles"), { recursive: true });
|
||||
for (const file of cssFiles) {
|
||||
let css = fs.readFileSync(path.join(chunkDir, file), "utf8");
|
||||
css = css.replace(
|
||||
// 图标字体改用 public/icons 的稳定地址,脱离旧 _nuxt 输出目录。
|
||||
/url\(([^)]*primeicons[^)]*)\)/g,
|
||||
(_, v) =>
|
||||
`url(${v.replace(/^["']?|["']?$/g, "").replace(/^\.\//, "/icons/")})`,
|
||||
@@ -621,6 +643,7 @@ for (const file of cssFiles) {
|
||||
if (result.status !== 0) throw Error(result.stdout + result.stderr);
|
||||
}
|
||||
for (const name of fs.readdirSync(path.join(input, "public")))
|
||||
// public 的图片和字体保留原访问路径;不复制 _nuxt bundle 或构建版本目录。
|
||||
if (
|
||||
name !== "_nuxt" &&
|
||||
!name.startsWith("._") &&
|
||||
@@ -634,6 +657,7 @@ for (const name of fs.readdirSync(path.join(input, "public")))
|
||||
});
|
||||
}
|
||||
fs.mkdirSync(path.join(root, "public/icons"), { recursive: true });
|
||||
// PrimeIcons 字体单独归档,配合前面的 CSS URL 重写使用。
|
||||
for (const name of fs.readdirSync(chunkDir))
|
||||
if (name.startsWith("primeicons.") && !name.endsWith(".js"))
|
||||
fs.copyFileSync(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 浏览器端到端验收
|
||||
|
||||
先在 `frontend` 安装项目依赖和 `playwright`,执行 `npm run build`,再执行 `npm test`。
|
||||
先在项目根目录执行 `npm ci` 安装锁定依赖,执行 `npm run build`,再执行 `npm test`。
|
||||
测试使用 Node 自带的 `node:test`,直接操作实际构建后的页面,不替换项目内部模块。
|
||||
|
||||
测试会自动启动两个仅监听 `127.0.0.1` 的独立进程:构建后的 Nuxt 预览服务和 HTTP 假后端。
|
||||
@@ -14,8 +14,8 @@
|
||||
CHROME_PATH=/absolute/path/to/chrome npm test
|
||||
```
|
||||
|
||||
默认测试当前 `frontend/.output`。如需验证其他工作树中已经构建的前端,可设置
|
||||
`E2E_APP_ROOT=/absolute/path/to/frontend`;测试依赖仍从当前工作树解析。
|
||||
默认测试当前项目的 `.output`。如需验证其他工作树中已经构建的前端,可设置
|
||||
`E2E_APP_ROOT=/absolute/path/to/project`;测试依赖仍从当前工作树解析。
|
||||
|
||||
每次运行在 `tests/fixtures/runs/run-*` 下生成截图和 `manifest.json`。
|
||||
清单只包含稳定的场景结果、行为观察、请求形状以及浏览器错误/网络越界;不包含随机端口、
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* 通过浏览器验证实际生产构建,不替换内部模块、不预置 JWT。
|
||||
* 测试只连接独立本地 HTTP 假后端,覆盖页面行为和接口契约,不代表真实支付/供应商已验收。
|
||||
* 每个场景保存截图、脱敏请求形状及稳定结果,方便失败后复核。
|
||||
*/
|
||||
import assert from "node:assert/strict";
|
||||
import { spawn } from "node:child_process";
|
||||
import { createDecipheriv } from "node:crypto";
|
||||
@@ -12,6 +17,7 @@ import { chromium } from "playwright";
|
||||
|
||||
const testDirectory = dirname(fileURLToPath(import.meta.url));
|
||||
const appRoot = resolve(process.env.E2E_APP_ROOT || join(testDirectory, ".."));
|
||||
// 默认复用系统 Chrome;CI/其他平台可指定可执行文件,不自动下载或修改用户浏览器。
|
||||
const chromePath =
|
||||
process.env.CHROME_PATH ||
|
||||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
|
||||
@@ -25,6 +31,7 @@ let browser;
|
||||
let runDirectory;
|
||||
|
||||
async function eventually(read, accept, message, timeout = 10000) {
|
||||
// 等待可观察结果稳定,而不是假设渲染/网络会在固定休眠后完成;失败信息过滤敏感字段。
|
||||
const deadline = Date.now() + timeout;
|
||||
let value;
|
||||
do {
|
||||
@@ -41,6 +48,7 @@ async function eventually(read, accept, message, timeout = 10000) {
|
||||
}
|
||||
|
||||
function startProcess(arguments_, options = {}) {
|
||||
// 统一收集子进程日志和句柄,异常退出可诊断,after 钩子负责结束所有本地服务。
|
||||
const child = spawn(process.execPath, arguments_, {
|
||||
...options,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
@@ -57,6 +65,7 @@ function startProcess(arguments_, options = {}) {
|
||||
}
|
||||
|
||||
async function fixtureRequest(path, body) {
|
||||
// 控制口只属于本地测试后端,调整外部响应,不给产品源码增加测试专用分支。
|
||||
const response = await fetch(`${backendOrigin}/__control/${path}`, {
|
||||
method: body === undefined ? "GET" : "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
@@ -68,6 +77,7 @@ async function fixtureRequest(path, body) {
|
||||
}
|
||||
|
||||
async function freePort() {
|
||||
// 让系统分配回环端口,降低并行运行多个验收进程时的固定端口冲突风险。
|
||||
const server = createServer();
|
||||
await new Promise((resolve_, reject) => {
|
||||
server.once("error", reject);
|
||||
@@ -80,6 +90,7 @@ async function freePort() {
|
||||
|
||||
before(
|
||||
async () => {
|
||||
// 必须先构建;此处运行 .output,而非开发服务器,确保测试走实际交付入口。
|
||||
const runs = join(testDirectory, "fixtures", "runs");
|
||||
await mkdir(runs, { recursive: true });
|
||||
runDirectory = await mkdtemp(join(runs, "run-"));
|
||||
@@ -89,6 +100,7 @@ before(
|
||||
const api = startProcess([
|
||||
join(testDirectory, "fixtures", "api-server.mjs"),
|
||||
]);
|
||||
// 假后端首行只报告监听地址,避免在启动协议中输出账号密码或请求内容。
|
||||
backendOrigin = await new Promise((resolve_, reject) => {
|
||||
const lines = createInterface({ input: api.stdout });
|
||||
const timer = setTimeout(
|
||||
@@ -117,6 +129,7 @@ before(
|
||||
{
|
||||
cwd: appRoot,
|
||||
env: {
|
||||
// 使用生产已有的公开 API 配置接入外部边界,不读取或修改项目 .env。
|
||||
...process.env,
|
||||
NODE_ENV: "production",
|
||||
HOST: "127.0.0.1",
|
||||
@@ -155,6 +168,7 @@ before(
|
||||
);
|
||||
|
||||
after(async () => {
|
||||
// 先关闭浏览器,再逆序结束服务;挂起接口也不能让验收进程永久留在后台。
|
||||
await browser?.close();
|
||||
for (const child of children.reverse()) {
|
||||
if (child.exitCode !== null) continue;
|
||||
@@ -178,6 +192,7 @@ after(async () => {
|
||||
});
|
||||
|
||||
function normalizeRequests(requests) {
|
||||
// 只记录字段名和是否认证;去重、排序移除并发请求顺序等非业务差异,不保存凭证值。
|
||||
const distinct = requests.map(
|
||||
({ method, path, query, body, authorization }) => ({
|
||||
method,
|
||||
@@ -197,6 +212,7 @@ function normalizeRequests(requests) {
|
||||
}
|
||||
|
||||
async function scenario(t, options, exercise) {
|
||||
// 每个场景重新初始化后端状态和浏览器上下文,账号与列表缓存不能污染下一场景。
|
||||
await fixtureRequest("reset", options);
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1680, height: 1050 },
|
||||
@@ -206,6 +222,7 @@ async function scenario(t, options, exercise) {
|
||||
const networkViolations = [];
|
||||
const allowedOrigins = new Set([appOrigin, backendOrigin]);
|
||||
await context.route("**/*", async (route) => {
|
||||
// 只允许两个本地 origin;意外的生产/CDN访问会被阻断,并在场景结束时作为失败报告。
|
||||
const url = new URL(route.request().url());
|
||||
if (
|
||||
!["http:", "https:"].includes(url.protocol) ||
|
||||
@@ -234,6 +251,7 @@ async function scenario(t, options, exercise) {
|
||||
} catch (error) {
|
||||
failure = error;
|
||||
} finally {
|
||||
// 即使业务断言失败,也尽量保存截图与请求诊断,不能只留下“超时”这一条错误。
|
||||
try {
|
||||
assert.equal(
|
||||
await page
|
||||
@@ -257,6 +275,7 @@ async function scenario(t, options, exercise) {
|
||||
}
|
||||
await context.close();
|
||||
requests = await fixtureRequest("requests");
|
||||
// 诊断附件保留先后顺序,但不落盘 body、Authorization 或 referrer 原文。
|
||||
await writeFile(
|
||||
join(runDirectory, screenshot.replace(/\.png$/, ".requests.json")),
|
||||
`${JSON.stringify(
|
||||
@@ -301,6 +320,7 @@ async function scenario(t, options, exercise) {
|
||||
} catch (error) {
|
||||
failure ??= error;
|
||||
}
|
||||
// 主清单保留稳定观察结果;失败文本另外替换随机 origin、测试密码和令牌。
|
||||
manifest.scenarios.push({
|
||||
name: t.name,
|
||||
result: failure ? "failed" : "passed",
|
||||
@@ -327,6 +347,7 @@ async function scenario(t, options, exercise) {
|
||||
}
|
||||
|
||||
async function login(page, redirect = "/security/ssh-keys") {
|
||||
// 所有普通场景经真实登录表单建立会话,顺带验证密码输入框没有退化成明文框。
|
||||
await page.goto(
|
||||
`${appOrigin}/login?redirect=${encodeURIComponent(redirect)}`,
|
||||
);
|
||||
@@ -339,6 +360,7 @@ async function login(page, redirect = "/security/ssh-keys") {
|
||||
}
|
||||
|
||||
async function visibleKeyIds(page) {
|
||||
// 从用户实际看到的表格读取 ID,而不是访问组件状态或请求客户端的内部缓存。
|
||||
const rows = await page
|
||||
.getByRole("row")
|
||||
.filter({ hasText: /#\d+/ })
|
||||
@@ -351,6 +373,7 @@ test(
|
||||
{ timeout: 20000 },
|
||||
(t) =>
|
||||
scenario(t, {}, async (page) => {
|
||||
// 访客导航保留查询串和锚点,但不能先发受保护的列表请求再跳转。
|
||||
await page.goto(`${appOrigin}/security/ssh-keys?m=7#details`);
|
||||
await page.waitForURL((url) => url.pathname === "/login");
|
||||
const url = new URL(page.url());
|
||||
@@ -394,6 +417,7 @@ test(
|
||||
"Email accounts must not carry a phone prefix",
|
||||
);
|
||||
assert.notEqual(loginRequest.body.password, loginPassword);
|
||||
// 固定 AES 参数是既有接口协议;这里核对发送字段,不把它误当安全的密码存储方案。
|
||||
const decipher = createDecipheriv(
|
||||
"aes-128-cbc",
|
||||
Buffer.from("idcsmart.finance"),
|
||||
@@ -422,6 +446,7 @@ test(
|
||||
{ timeout: 30000 },
|
||||
(t) =>
|
||||
scenario(t, { loginMode: "sms-only" }, async (page) => {
|
||||
// 外部浏览器时钟推进验证码倒计时,不向产品增加测试时间开关。
|
||||
await page.clock.install();
|
||||
await page.goto(
|
||||
`${appOrigin}/login?redirect=${encodeURIComponent("/security/ssh-keys")}`,
|
||||
@@ -629,6 +654,7 @@ test(
|
||||
ordinarySecond,
|
||||
Array.from({ length: 10 }, (_, index) => index + 11),
|
||||
);
|
||||
// 普通后端分页与前端搜索分页分别验证,防止二次切片或每页重复显示全部匹配项。
|
||||
await page.getByRole("textbox", { name: "搜索 SSH 密钥" }).fill("audit");
|
||||
await page.getByRole("button", { name: "搜索", exact: true }).click();
|
||||
const first = await eventually(
|
||||
@@ -716,6 +742,7 @@ test(
|
||||
{ timeout: 30000 },
|
||||
(t) =>
|
||||
scenario(t, { sshLastPageSingleRow: true }, async (page) => {
|
||||
// 通过真实新增/编辑/删除制造末页消失,验证列表能回到合法页,而不只验证按钮关闭。
|
||||
await login(page);
|
||||
await eventually(
|
||||
() => visibleKeyIds(page),
|
||||
@@ -848,6 +875,7 @@ test(
|
||||
{ timeout: 25000 },
|
||||
(t) =>
|
||||
scenario(t, { hangSshBody: true }, async (page) => {
|
||||
// HTTP 头已返回但 JSON 体挂起,证明请求截止时间覆盖了解码阶段而非只覆盖 fetch。
|
||||
await page.clock.install();
|
||||
const headers = page.waitForResponse(
|
||||
(response) =>
|
||||
@@ -888,6 +916,7 @@ test(
|
||||
{ timeout: 25000 },
|
||||
(t) =>
|
||||
scenario(t, { hangLogout: true }, async (page) => {
|
||||
// 后端退出故意不返回;本地会话仍须立即撤销,不能把安全退出绑在网络成功上。
|
||||
await login(page);
|
||||
await page.getByRole("button", { name: "用户菜单", exact: true }).click();
|
||||
await page
|
||||
@@ -945,6 +974,7 @@ test(
|
||||
);
|
||||
|
||||
for (const [name, redirect] of [
|
||||
// 三种危险返回地址经真实登录入口验证,不能跳出本站 origin。
|
||||
["protocol relative", "//external.invalid/steal"],
|
||||
["encoded slash", "/%2fexternal.invalid/steal"],
|
||||
["backslash", "/\\external.invalid/steal"],
|
||||
@@ -987,6 +1017,7 @@ test(
|
||||
{ timeout: 30000 },
|
||||
(t) =>
|
||||
scenario(t, { hangPaymentStatus: true }, async (page) => {
|
||||
// 支付状态接口永久挂起时,独立五分钟期限仍应终止支付等待。
|
||||
await login(page, "/orders/order/pay?id=42");
|
||||
await page.clock.install();
|
||||
await page.getByRole("button", { name: "确认支付", exact: true }).click();
|
||||
@@ -1098,6 +1129,7 @@ test(
|
||||
{ timeout: 25000 },
|
||||
(t) =>
|
||||
scenario(t, { expiredLegacy: true }, async (page) => {
|
||||
// 临时凭证首次拒绝后仍留在内存供原页重试,但不得重新进入地址栏或持久化存储。
|
||||
await page.goto(`${appOrigin}/home.htm?queryParam=e2e-session-token`);
|
||||
await page
|
||||
.getByRole("heading", { name: "暂时无法进入会员中心", exact: true })
|
||||
@@ -1214,6 +1246,7 @@ test(
|
||||
"Payment initialization must settle before expiry",
|
||||
);
|
||||
const baseline = (await fixtureRequest("requests")).length;
|
||||
// 付款前预检发现401后,不应因响应式身份变更重新触发支付初始化。
|
||||
await fixtureRequest("options", { expiredPayment: true });
|
||||
await page.getByRole("button", { name: "确认支付", exact: true }).click();
|
||||
await page.waitForURL((url) => url.pathname === "/login");
|
||||
@@ -1256,6 +1289,7 @@ test(
|
||||
"The first tab must show an authenticated page",
|
||||
);
|
||||
const secondTab = await page.context().newPage();
|
||||
// 使用同一浏览器真实 localStorage/storage 事件,不伪造跨标签页退出通知。
|
||||
await secondTab.goto(`${appOrigin}/security/ssh-keys`);
|
||||
await eventually(
|
||||
() => visibleKeyIds(secondTab),
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* 独立进程的 HTTP 假后端,仅监听回环地址,按既有 /console/v1 信封响应。
|
||||
* 只模拟测试需要的外部接口,不是可部署的业务后端,也不执行真实付款或短信投递。
|
||||
* 控制口允许场景重置数据/制造挂起和失效;它不进入产品接口实现。
|
||||
*/
|
||||
import { createServer } from "node:http";
|
||||
|
||||
const token = "e2e-session-token";
|
||||
@@ -8,6 +13,7 @@ const account = {
|
||||
credit: "100.00",
|
||||
};
|
||||
const initialKeys = Array.from({ length: 28 }, (_, index) => ({
|
||||
// 25个audit匹配项 + 3个other项,覆盖搜索结果10/10/5分页与切换过滤条件。
|
||||
id: index + 1,
|
||||
name:
|
||||
index < 25
|
||||
@@ -23,11 +29,13 @@ let paid = false;
|
||||
let keys = initialKeys.map((key) => ({ ...key }));
|
||||
|
||||
function reply(response, data = {}, extra = {}, httpStatus = 200) {
|
||||
// HTTP状态与业务信封可以独立覆盖,确保前端同时处理传输401和信封401。
|
||||
response.writeHead(httpStatus, { "Content-Type": "application/json" });
|
||||
response.end(JSON.stringify({ status: httpStatus, msg: "", data, ...extra }));
|
||||
}
|
||||
|
||||
const server = createServer(async (request, response) => {
|
||||
// 预览服务和假后端端口不同,需要允许测试浏览器发送JSON与Bearer请求。
|
||||
response.setHeader("Access-Control-Allow-Origin", "*");
|
||||
response.setHeader(
|
||||
"Access-Control-Allow-Headers",
|
||||
@@ -51,6 +59,7 @@ const server = createServer(async (request, response) => {
|
||||
}
|
||||
|
||||
if (url.pathname === "/__control/reset" && request.method === "POST") {
|
||||
// 场景从全新列表和未付款状态开始;11行变体用于删除最后一页唯一记录。
|
||||
options = body ?? {};
|
||||
requests = [];
|
||||
paid = false;
|
||||
@@ -67,6 +76,7 @@ const server = createServer(async (request, response) => {
|
||||
|
||||
const path = url.pathname.replace(/^\/console\/v1/, "");
|
||||
const record = {
|
||||
// 请求原文仅在测试进程内存供断言使用,测试输出端另做脱敏,不能直接导出此对象。
|
||||
method: request.method,
|
||||
path,
|
||||
query: Object.fromEntries(url.searchParams),
|
||||
@@ -78,6 +88,7 @@ const server = createServer(async (request, response) => {
|
||||
requests.push(record);
|
||||
|
||||
if (path === "/common" && request.method === "GET") {
|
||||
// 模拟站点配置决定允许的登录方式;离线场景关闭外部CAPTCHA与OAuth。
|
||||
return reply(response, {
|
||||
website_name: "本地前端验收",
|
||||
login_phone_password: options.loginMode === "sms-only" ? 0 : 1,
|
||||
@@ -99,6 +110,7 @@ const server = createServer(async (request, response) => {
|
||||
if (path === "/phone/code" && request.method === "POST")
|
||||
return reply(response);
|
||||
|
||||
// 公共配置和登录之后的接口必须携带实际登录得到的测试令牌。
|
||||
if (request.headers.authorization !== `Bearer ${token}`) {
|
||||
record.invalidAuthorization = true;
|
||||
return reply(response, {}, { msg: "Missing fixture session" }, 401);
|
||||
@@ -138,6 +150,7 @@ const server = createServer(async (request, response) => {
|
||||
}
|
||||
if (path === "/ssh_key" && request.method === "GET") {
|
||||
if (options.hangSshBody) {
|
||||
// 只发送响应头和不完整JSON,保持连接以复现“响应体解码永不结束”。
|
||||
response.writeHead(200, { "Content-Type": "application/json" });
|
||||
response.write('{"status":200,"msg":"","data":');
|
||||
return;
|
||||
@@ -149,6 +162,7 @@ const server = createServer(async (request, response) => {
|
||||
{ status: 401, msg: "Expired fixture session" },
|
||||
);
|
||||
const page = Math.max(1, Number(url.searchParams.get("page")) || 1);
|
||||
// 列表接口仍走后端分页;搜索聚合/切片由真实前端路径负责。
|
||||
const limit = Math.max(1, Number(url.searchParams.get("limit")) || 10);
|
||||
return reply(response, {
|
||||
list: keys.slice((page - 1) * limit, page * limit),
|
||||
@@ -172,6 +186,7 @@ const server = createServer(async (request, response) => {
|
||||
return reply(response);
|
||||
}
|
||||
if (path === "/order/42" && request.method === "GET") {
|
||||
// 余额、订单与支付状态共享paid事实,支付成功后回读必须反映已付金额。
|
||||
if (options.expiredPayment)
|
||||
return reply(
|
||||
response,
|
||||
@@ -216,6 +231,7 @@ const server = createServer(async (request, response) => {
|
||||
if (path === "/voucher/pay" && request.method === "GET")
|
||||
return reply(response, { list: [] });
|
||||
if (path === "/pay" && request.method === "POST") {
|
||||
// Paid是现有后端的顶层字段;挂起变体返回渠道HTML,随后状态查询不响应。
|
||||
if (options.hangPaymentStatus)
|
||||
return reply(response, { html: '<div role="status">本地支付渠道</div>' });
|
||||
paid = true;
|
||||
@@ -247,6 +263,7 @@ const server = createServer(async (request, response) => {
|
||||
if (path === "/recommend/promoter" && request.method === "GET")
|
||||
return reply(response, { promoter: { permission: 0 } });
|
||||
|
||||
// 未实现接口显式失败并留标记,防止新增产品请求被“统一成功”假后端掩盖。
|
||||
record.handled = false;
|
||||
reply(
|
||||
response,
|
||||
@@ -257,12 +274,14 @@ const server = createServer(async (request, response) => {
|
||||
});
|
||||
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
// 由系统选择空闲端口,首行JSON作为父测试进程的启动握手。
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ origin: `http://127.0.0.1:${server.address().port}` })}\n`,
|
||||
);
|
||||
});
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
// 先关闭挂起连接,确保超时测试也能正常清理进程。
|
||||
server.closeAllConnections();
|
||||
server.close(() => process.exit(0));
|
||||
});
|
||||