feat(sync): 添加 Vue TypeScript 管理控制台
This commit is contained in:
@@ -1,9 +1,18 @@
|
||||
FROM node:22-alpine AS console
|
||||
WORKDIR /console
|
||||
RUN corepack enable
|
||||
COPY console/package.json console/pnpm-lock.yaml ./
|
||||
RUN pnpm install --frozen-lockfile
|
||||
COPY console ./
|
||||
RUN pnpm build
|
||||
|
||||
FROM python:3.12-slim
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.9.24 /uv /bin/uv
|
||||
WORKDIR /service
|
||||
COPY pyproject.toml uv.lock ./
|
||||
RUN uv sync --frozen --no-dev
|
||||
COPY sync_server ./sync_server
|
||||
COPY --from=console /sync_server/static ./sync_server/static
|
||||
RUN useradd --uid 10001 --create-home opennexus && mkdir /staging && chown opennexus /staging
|
||||
USER 10001
|
||||
ENV SYNC_STAGING_DIR=/staging
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
协议及限制见 [Sync v1](../docs/contracts/Sync-v1契约.md)。服务独立于 AI Core,生产入口仅支持 PostgreSQL 和 S3。当前尚未达到 M3 运维退出条件。
|
||||
|
||||
服务根路径 `/` 与 `/console/` 提供同源的 Vue 3 + TypeScript Sync Console,可查看服务健康与依赖就绪状态,并使用普通 Sync 账户管理自己的 Vault 和设备。页面只调用公开的 Sync v1 API;密码在请求发出前从输入框清除,访问和刷新令牌只保留在页面内存,刷新或关闭页面即丢弃。控制台源码位于 `console/`,生产静态文件由 Docker 多阶段构建生成。
|
||||
|
||||
```powershell
|
||||
cd console
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## 隔离测试
|
||||
|
||||
```powershell
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="color-scheme" content="dark">
|
||||
<meta name="theme-color" content="#07120f">
|
||||
<title>OpenNexus Sync Console</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "opennexus-sync-console",
|
||||
"private": true,
|
||||
"version": "0.3.0-alpha.1",
|
||||
"packageManager": "pnpm@10.28.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"dev": "vite",
|
||||
"type-check": "vue-tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "3.5.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "5.2.4",
|
||||
"typescript": "5.9.2",
|
||||
"vite": "6.1.0",
|
||||
"vue-tsc": "2.2.12"
|
||||
}
|
||||
}
|
||||
Generated
+958
@@ -0,0 +1,958 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
vue:
|
||||
specifier: 3.5.21
|
||||
version: 3.5.21(typescript@5.9.2)
|
||||
devDependencies:
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: 5.2.4
|
||||
version: 5.2.4(vite@6.1.0)(vue@3.5.21(typescript@5.9.2))
|
||||
typescript:
|
||||
specifier: 5.9.2
|
||||
version: 5.9.2
|
||||
vite:
|
||||
specifier: 6.1.0
|
||||
version: 6.1.0
|
||||
vue-tsc:
|
||||
specifier: 2.2.12
|
||||
version: 2.2.12(typescript@5.9.2)
|
||||
|
||||
packages:
|
||||
|
||||
'@babel/helper-string-parser@7.29.7':
|
||||
resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-validator-identifier@7.29.7':
|
||||
resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/parser@7.29.8':
|
||||
resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
'@babel/types@7.29.8':
|
||||
resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@esbuild/aix-ppc64@0.24.2':
|
||||
resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/android-arm64@0.24.2':
|
||||
resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.24.2':
|
||||
resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.24.2':
|
||||
resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/darwin-arm64@0.24.2':
|
||||
resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.24.2':
|
||||
resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.24.2':
|
||||
resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.24.2':
|
||||
resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/linux-arm64@0.24.2':
|
||||
resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.24.2':
|
||||
resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.24.2':
|
||||
resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.24.2':
|
||||
resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.24.2':
|
||||
resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.24.2':
|
||||
resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.24.2':
|
||||
resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.24.2':
|
||||
resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.24.2':
|
||||
resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/netbsd-arm64@0.24.2':
|
||||
resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.24.2':
|
||||
resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.24.2':
|
||||
resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.24.2':
|
||||
resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/sunos-x64@0.24.2':
|
||||
resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/win32-arm64@0.24.2':
|
||||
resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.24.2':
|
||||
resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.24.2':
|
||||
resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.6.0':
|
||||
resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==}
|
||||
|
||||
'@napi-rs/lzma-linux-x64-gnu@1.5.1':
|
||||
resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==}
|
||||
engines: {node: ^22.20 || ^24.12 || >=25}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.63.1':
|
||||
resolution: {integrity: sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@rollup/rollup-android-arm64@4.63.1':
|
||||
resolution: {integrity: sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@rollup/rollup-darwin-arm64@4.63.1':
|
||||
resolution: {integrity: sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@rollup/rollup-darwin-x64@4.63.1':
|
||||
resolution: {integrity: sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@rollup/rollup-freebsd-arm64@4.63.1':
|
||||
resolution: {integrity: sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rollup/rollup-freebsd-x64@4.63.1':
|
||||
resolution: {integrity: sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.63.1':
|
||||
resolution: {integrity: sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.63.1':
|
||||
resolution: {integrity: sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.63.1':
|
||||
resolution: {integrity: sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.63.1':
|
||||
resolution: {integrity: sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.63.1':
|
||||
resolution: {integrity: sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-loong64-musl@4.63.1':
|
||||
resolution: {integrity: sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.63.1':
|
||||
resolution: {integrity: sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-musl@4.63.1':
|
||||
resolution: {integrity: sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.63.1':
|
||||
resolution: {integrity: sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.63.1':
|
||||
resolution: {integrity: sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.63.1':
|
||||
resolution: {integrity: sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.63.1':
|
||||
resolution: {integrity: sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.63.1':
|
||||
resolution: {integrity: sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openbsd-x64@4.63.1':
|
||||
resolution: {integrity: sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@rollup/rollup-openharmony-arm64@4.63.1':
|
||||
resolution: {integrity: sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.63.1':
|
||||
resolution: {integrity: sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-ia32-msvc@4.63.1':
|
||||
resolution: {integrity: sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-x64-gnu@4.63.1':
|
||||
resolution: {integrity: sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-x64-msvc@4.63.1':
|
||||
resolution: {integrity: sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@types/estree@1.0.9':
|
||||
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
|
||||
|
||||
'@vitejs/plugin-vue@5.2.4':
|
||||
resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
peerDependencies:
|
||||
vite: ^5.0.0 || ^6.0.0
|
||||
vue: ^3.2.25
|
||||
|
||||
'@volar/language-core@2.4.15':
|
||||
resolution: {integrity: sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==}
|
||||
|
||||
'@volar/source-map@2.4.15':
|
||||
resolution: {integrity: sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==}
|
||||
|
||||
'@volar/typescript@2.4.15':
|
||||
resolution: {integrity: sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==}
|
||||
|
||||
'@vue/compiler-core@3.5.21':
|
||||
resolution: {integrity: sha512-8i+LZ0vf6ZgII5Z9XmUvrCyEzocvWT+TeR2VBUVlzIH6Tyv57E20mPZ1bCS+tbejgUgmjrEh7q/0F0bibskAmw==}
|
||||
|
||||
'@vue/compiler-core@3.5.42':
|
||||
resolution: {integrity: sha512-2Ye1ilMtKXxl8qZUrQ5j0CdgenFp/HFQmta6rfRyfEsTG69L6Wk+tWuNoHYHMx9E8tF2Slvdg1FuwDvAXdy1LQ==}
|
||||
|
||||
'@vue/compiler-dom@3.5.21':
|
||||
resolution: {integrity: sha512-jNtbu/u97wiyEBJlJ9kmdw7tAr5Vy0Aj5CgQmo+6pxWNQhXZDPsRr1UWPN4v3Zf82s2H3kF51IbzZ4jMWAgPlQ==}
|
||||
|
||||
'@vue/compiler-dom@3.5.42':
|
||||
resolution: {integrity: sha512-qbhQZEFmycr+ni/qyuccS4sucNN7VAbDfbkvNxWOX2VfgFm90MNs3/UhRNKoPMEIVn0F8gdlYjLPvqxHwHeQOA==}
|
||||
|
||||
'@vue/compiler-sfc@3.5.21':
|
||||
resolution: {integrity: sha512-SXlyk6I5eUGBd2v8Ie7tF6ADHE9kCR6mBEuPyH1nUZ0h6Xx6nZI29i12sJKQmzbDyr2tUHMhhTt51Z6blbkTTQ==}
|
||||
|
||||
'@vue/compiler-ssr@3.5.21':
|
||||
resolution: {integrity: sha512-vKQ5olH5edFZdf5ZrlEgSO1j1DMA4u23TVK5XR1uMhvwnYvVdDF0nHXJUblL/GvzlShQbjhZZ2uvYmDlAbgo9w==}
|
||||
|
||||
'@vue/compiler-vue2@2.7.16':
|
||||
resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==}
|
||||
|
||||
'@vue/language-core@2.2.12':
|
||||
resolution: {integrity: sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==}
|
||||
peerDependencies:
|
||||
typescript: '*'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
'@vue/reactivity@3.5.21':
|
||||
resolution: {integrity: sha512-3ah7sa+Cwr9iiYEERt9JfZKPw4A2UlbY8RbbnH2mGCE8NwHkhmlZt2VsH0oDA3P08X3jJd29ohBDtX+TbD9AsA==}
|
||||
|
||||
'@vue/runtime-core@3.5.21':
|
||||
resolution: {integrity: sha512-+DplQlRS4MXfIf9gfD1BOJpk5RSyGgGXD/R+cumhe8jdjUcq/qlxDawQlSI8hCKupBlvM+3eS1se5xW+SuNAwA==}
|
||||
|
||||
'@vue/runtime-dom@3.5.21':
|
||||
resolution: {integrity: sha512-3M2DZsOFwM5qI15wrMmNF5RJe1+ARijt2HM3TbzBbPSuBHOQpoidE+Pa+XEaVN+czbHf81ETRoG1ltztP2em8w==}
|
||||
|
||||
'@vue/server-renderer@3.5.21':
|
||||
resolution: {integrity: sha512-qr8AqgD3DJPJcGvLcJKQo2tAc8OnXRcfxhOJCPF+fcfn5bBGz7VCcO7t+qETOPxpWK1mgysXvVT/j+xWaHeMWA==}
|
||||
peerDependencies:
|
||||
vue: 3.5.21
|
||||
|
||||
'@vue/shared@3.5.21':
|
||||
resolution: {integrity: sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw==}
|
||||
|
||||
'@vue/shared@3.5.42':
|
||||
resolution: {integrity: sha512-2rPxex1jQf4jvl9MOHl6YaXCPcrNqz/FstMOEh3QWY+/OME9nQTvl9WYeCwhW7AFjaR0SnngZGlp/wkR6rkI6g==}
|
||||
|
||||
alien-signals@1.0.13:
|
||||
resolution: {integrity: sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==}
|
||||
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
brace-expansion@2.1.4:
|
||||
resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==}
|
||||
|
||||
csstype@3.2.3:
|
||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||
|
||||
de-indent@1.0.2:
|
||||
resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
|
||||
|
||||
entities@4.5.0:
|
||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||
engines: {node: '>=0.12'}
|
||||
|
||||
entities@7.0.1:
|
||||
resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
|
||||
engines: {node: '>=0.12'}
|
||||
|
||||
esbuild@0.24.2:
|
||||
resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
estree-walker@2.0.2:
|
||||
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
he@1.2.0:
|
||||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||
hasBin: true
|
||||
|
||||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
minimatch@9.0.9:
|
||||
resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
muggle-string@0.4.1:
|
||||
resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
|
||||
|
||||
nanoid@3.3.18:
|
||||
resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
path-browserify@1.0.1:
|
||||
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
postcss@8.5.28:
|
||||
resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
rollup@4.63.1:
|
||||
resolution: {integrity: sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==}
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
|
||||
source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
typescript@5.9.2:
|
||||
resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
vite@6.1.0:
|
||||
resolution: {integrity: sha512-RjjMipCKVoR4hVfPY6GQTgveinjNuyLw+qruksLDvA5ktI1150VmcMBKmQaEWJhg/j6Uaf6dNCNA0AfdzUb/hQ==}
|
||||
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
|
||||
jiti: '>=1.21.0'
|
||||
less: '*'
|
||||
lightningcss: ^1.21.0
|
||||
sass: '*'
|
||||
sass-embedded: '*'
|
||||
stylus: '*'
|
||||
sugarss: '*'
|
||||
terser: ^5.16.0
|
||||
tsx: ^4.8.1
|
||||
yaml: ^2.4.2
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
jiti:
|
||||
optional: true
|
||||
less:
|
||||
optional: true
|
||||
lightningcss:
|
||||
optional: true
|
||||
sass:
|
||||
optional: true
|
||||
sass-embedded:
|
||||
optional: true
|
||||
stylus:
|
||||
optional: true
|
||||
sugarss:
|
||||
optional: true
|
||||
terser:
|
||||
optional: true
|
||||
tsx:
|
||||
optional: true
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
vscode-uri@3.2.0:
|
||||
resolution: {integrity: sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==}
|
||||
|
||||
vue-tsc@2.2.12:
|
||||
resolution: {integrity: sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
typescript: '>=5.0.0'
|
||||
|
||||
vue@3.5.21:
|
||||
resolution: {integrity: sha512-xxf9rum9KtOdwdRkiApWL+9hZEMWE90FHh8yS1+KJAiWYh+iGWV1FquPjoO9VUHQ+VIhsCXNNyZ5Sf4++RVZBA==}
|
||||
peerDependencies:
|
||||
typescript: '*'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
snapshots:
|
||||
|
||||
'@babel/helper-string-parser@7.29.7': {}
|
||||
|
||||
'@babel/helper-validator-identifier@7.29.7': {}
|
||||
|
||||
'@babel/parser@7.29.8':
|
||||
dependencies:
|
||||
'@babel/types': 7.29.8
|
||||
|
||||
'@babel/types@7.29.8':
|
||||
dependencies:
|
||||
'@babel/helper-string-parser': 7.29.7
|
||||
'@babel/helper-validator-identifier': 7.29.7
|
||||
|
||||
'@esbuild/aix-ppc64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.6.0': {}
|
||||
|
||||
'@napi-rs/lzma-linux-x64-gnu@1.5.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.63.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-android-arm64@4.63.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-darwin-arm64@4.63.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-darwin-x64@4.63.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-freebsd-arm64@4.63.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-freebsd-x64@4.63.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.63.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.63.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.63.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.63.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.63.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-loong64-musl@4.63.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.63.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-ppc64-musl@4.63.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.63.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.63.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.63.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.63.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.63.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-openbsd-x64@4.63.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-openharmony-arm64@4.63.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.63.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-ia32-msvc@4.63.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-x64-gnu@4.63.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-x64-msvc@4.63.1':
|
||||
optional: true
|
||||
|
||||
'@types/estree@1.0.9': {}
|
||||
|
||||
'@vitejs/plugin-vue@5.2.4(vite@6.1.0)(vue@3.5.21(typescript@5.9.2))':
|
||||
dependencies:
|
||||
vite: 6.1.0
|
||||
vue: 3.5.21(typescript@5.9.2)
|
||||
|
||||
'@volar/language-core@2.4.15':
|
||||
dependencies:
|
||||
'@volar/source-map': 2.4.15
|
||||
|
||||
'@volar/source-map@2.4.15': {}
|
||||
|
||||
'@volar/typescript@2.4.15':
|
||||
dependencies:
|
||||
'@volar/language-core': 2.4.15
|
||||
path-browserify: 1.0.1
|
||||
vscode-uri: 3.2.0
|
||||
|
||||
'@vue/compiler-core@3.5.21':
|
||||
dependencies:
|
||||
'@babel/parser': 7.29.8
|
||||
'@vue/shared': 3.5.21
|
||||
entities: 4.5.0
|
||||
estree-walker: 2.0.2
|
||||
source-map-js: 1.2.1
|
||||
|
||||
'@vue/compiler-core@3.5.42':
|
||||
dependencies:
|
||||
'@babel/parser': 7.29.8
|
||||
'@vue/shared': 3.5.42
|
||||
entities: 7.0.1
|
||||
estree-walker: 2.0.2
|
||||
source-map-js: 1.2.1
|
||||
|
||||
'@vue/compiler-dom@3.5.21':
|
||||
dependencies:
|
||||
'@vue/compiler-core': 3.5.21
|
||||
'@vue/shared': 3.5.21
|
||||
|
||||
'@vue/compiler-dom@3.5.42':
|
||||
dependencies:
|
||||
'@vue/compiler-core': 3.5.42
|
||||
'@vue/shared': 3.5.42
|
||||
|
||||
'@vue/compiler-sfc@3.5.21':
|
||||
dependencies:
|
||||
'@babel/parser': 7.29.8
|
||||
'@vue/compiler-core': 3.5.21
|
||||
'@vue/compiler-dom': 3.5.21
|
||||
'@vue/compiler-ssr': 3.5.21
|
||||
'@vue/shared': 3.5.21
|
||||
estree-walker: 2.0.2
|
||||
magic-string: 0.30.21
|
||||
postcss: 8.5.28
|
||||
source-map-js: 1.2.1
|
||||
|
||||
'@vue/compiler-ssr@3.5.21':
|
||||
dependencies:
|
||||
'@vue/compiler-dom': 3.5.21
|
||||
'@vue/shared': 3.5.21
|
||||
|
||||
'@vue/compiler-vue2@2.7.16':
|
||||
dependencies:
|
||||
de-indent: 1.0.2
|
||||
he: 1.2.0
|
||||
|
||||
'@vue/language-core@2.2.12(typescript@5.9.2)':
|
||||
dependencies:
|
||||
'@volar/language-core': 2.4.15
|
||||
'@vue/compiler-dom': 3.5.42
|
||||
'@vue/compiler-vue2': 2.7.16
|
||||
'@vue/shared': 3.5.42
|
||||
alien-signals: 1.0.13
|
||||
minimatch: 9.0.9
|
||||
muggle-string: 0.4.1
|
||||
path-browserify: 1.0.1
|
||||
optionalDependencies:
|
||||
typescript: 5.9.2
|
||||
|
||||
'@vue/reactivity@3.5.21':
|
||||
dependencies:
|
||||
'@vue/shared': 3.5.21
|
||||
|
||||
'@vue/runtime-core@3.5.21':
|
||||
dependencies:
|
||||
'@vue/reactivity': 3.5.21
|
||||
'@vue/shared': 3.5.21
|
||||
|
||||
'@vue/runtime-dom@3.5.21':
|
||||
dependencies:
|
||||
'@vue/reactivity': 3.5.21
|
||||
'@vue/runtime-core': 3.5.21
|
||||
'@vue/shared': 3.5.21
|
||||
csstype: 3.2.3
|
||||
|
||||
'@vue/server-renderer@3.5.21(vue@3.5.21(typescript@5.9.2))':
|
||||
dependencies:
|
||||
'@vue/compiler-ssr': 3.5.21
|
||||
'@vue/shared': 3.5.21
|
||||
vue: 3.5.21(typescript@5.9.2)
|
||||
|
||||
'@vue/shared@3.5.21': {}
|
||||
|
||||
'@vue/shared@3.5.42': {}
|
||||
|
||||
alien-signals@1.0.13: {}
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
brace-expansion@2.1.4:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
|
||||
csstype@3.2.3: {}
|
||||
|
||||
de-indent@1.0.2: {}
|
||||
|
||||
entities@4.5.0: {}
|
||||
|
||||
entities@7.0.1: {}
|
||||
|
||||
esbuild@0.24.2:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.24.2
|
||||
'@esbuild/android-arm': 0.24.2
|
||||
'@esbuild/android-arm64': 0.24.2
|
||||
'@esbuild/android-x64': 0.24.2
|
||||
'@esbuild/darwin-arm64': 0.24.2
|
||||
'@esbuild/darwin-x64': 0.24.2
|
||||
'@esbuild/freebsd-arm64': 0.24.2
|
||||
'@esbuild/freebsd-x64': 0.24.2
|
||||
'@esbuild/linux-arm': 0.24.2
|
||||
'@esbuild/linux-arm64': 0.24.2
|
||||
'@esbuild/linux-ia32': 0.24.2
|
||||
'@esbuild/linux-loong64': 0.24.2
|
||||
'@esbuild/linux-mips64el': 0.24.2
|
||||
'@esbuild/linux-ppc64': 0.24.2
|
||||
'@esbuild/linux-riscv64': 0.24.2
|
||||
'@esbuild/linux-s390x': 0.24.2
|
||||
'@esbuild/linux-x64': 0.24.2
|
||||
'@esbuild/netbsd-arm64': 0.24.2
|
||||
'@esbuild/netbsd-x64': 0.24.2
|
||||
'@esbuild/openbsd-arm64': 0.24.2
|
||||
'@esbuild/openbsd-x64': 0.24.2
|
||||
'@esbuild/sunos-x64': 0.24.2
|
||||
'@esbuild/win32-arm64': 0.24.2
|
||||
'@esbuild/win32-ia32': 0.24.2
|
||||
'@esbuild/win32-x64': 0.24.2
|
||||
|
||||
estree-walker@2.0.2: {}
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
he@1.2.0: {}
|
||||
|
||||
magic-string@0.30.21:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.6.0
|
||||
|
||||
minimatch@9.0.9:
|
||||
dependencies:
|
||||
brace-expansion: 2.1.4
|
||||
|
||||
muggle-string@0.4.1: {}
|
||||
|
||||
nanoid@3.3.18: {}
|
||||
|
||||
path-browserify@1.0.1: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
postcss@8.5.28:
|
||||
dependencies:
|
||||
nanoid: 3.3.18
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
rollup@4.63.1:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.9
|
||||
optionalDependencies:
|
||||
'@napi-rs/lzma-linux-x64-gnu': 1.5.1
|
||||
'@rollup/rollup-android-arm-eabi': 4.63.1
|
||||
'@rollup/rollup-android-arm64': 4.63.1
|
||||
'@rollup/rollup-darwin-arm64': 4.63.1
|
||||
'@rollup/rollup-darwin-x64': 4.63.1
|
||||
'@rollup/rollup-freebsd-arm64': 4.63.1
|
||||
'@rollup/rollup-freebsd-x64': 4.63.1
|
||||
'@rollup/rollup-linux-arm-gnueabihf': 4.63.1
|
||||
'@rollup/rollup-linux-arm-musleabihf': 4.63.1
|
||||
'@rollup/rollup-linux-arm64-gnu': 4.63.1
|
||||
'@rollup/rollup-linux-arm64-musl': 4.63.1
|
||||
'@rollup/rollup-linux-loong64-gnu': 4.63.1
|
||||
'@rollup/rollup-linux-loong64-musl': 4.63.1
|
||||
'@rollup/rollup-linux-ppc64-gnu': 4.63.1
|
||||
'@rollup/rollup-linux-ppc64-musl': 4.63.1
|
||||
'@rollup/rollup-linux-riscv64-gnu': 4.63.1
|
||||
'@rollup/rollup-linux-riscv64-musl': 4.63.1
|
||||
'@rollup/rollup-linux-s390x-gnu': 4.63.1
|
||||
'@rollup/rollup-linux-x64-gnu': 4.63.1
|
||||
'@rollup/rollup-linux-x64-musl': 4.63.1
|
||||
'@rollup/rollup-openbsd-x64': 4.63.1
|
||||
'@rollup/rollup-openharmony-arm64': 4.63.1
|
||||
'@rollup/rollup-win32-arm64-msvc': 4.63.1
|
||||
'@rollup/rollup-win32-ia32-msvc': 4.63.1
|
||||
'@rollup/rollup-win32-x64-gnu': 4.63.1
|
||||
'@rollup/rollup-win32-x64-msvc': 4.63.1
|
||||
fsevents: 2.3.3
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
typescript@5.9.2: {}
|
||||
|
||||
vite@6.1.0:
|
||||
dependencies:
|
||||
esbuild: 0.24.2
|
||||
postcss: 8.5.28
|
||||
rollup: 4.63.1
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
vscode-uri@3.2.0: {}
|
||||
|
||||
vue-tsc@2.2.12(typescript@5.9.2):
|
||||
dependencies:
|
||||
'@volar/typescript': 2.4.15
|
||||
'@vue/language-core': 2.2.12(typescript@5.9.2)
|
||||
typescript: 5.9.2
|
||||
|
||||
vue@3.5.21(typescript@5.9.2):
|
||||
dependencies:
|
||||
'@vue/compiler-dom': 3.5.21
|
||||
'@vue/compiler-sfc': 3.5.21
|
||||
'@vue/runtime-dom': 3.5.21
|
||||
'@vue/server-renderer': 3.5.21(vue@3.5.21(typescript@5.9.2))
|
||||
'@vue/shared': 3.5.21
|
||||
optionalDependencies:
|
||||
typescript: 5.9.2
|
||||
@@ -0,0 +1,250 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
||||
import { SyncApi, type Device, type ServiceStatus, type Vault } from './api'
|
||||
|
||||
const api = new SyncApi()
|
||||
const service = reactive<ServiceStatus>({ health: false, ready: false, protocol: 1, maxObjectSize: 100 * 1024 * 1024 })
|
||||
const checking = ref(true)
|
||||
const signedIn = ref(false)
|
||||
const busy = ref(false)
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const deviceName = ref('OpenNexus Web Console')
|
||||
const sessionLabel = ref('')
|
||||
const newVaultName = ref('')
|
||||
const vaults = ref<Vault[]>([])
|
||||
const devices = ref<Device[]>([])
|
||||
const toast = ref('')
|
||||
const toastError = ref(false)
|
||||
let toastTimer: number | undefined
|
||||
let statusTimer: number | undefined
|
||||
|
||||
const used = computed(() => vaults.value.reduce((total, vault) => total + Number(vault.used || 0), 0))
|
||||
const quota = computed(() => vaults.value.reduce((total, vault) => total + Number(vault.quota || 0), 0))
|
||||
const activeDevices = computed(() => devices.value.filter(device => !device.revoked).length)
|
||||
|
||||
function formatBytes(value: number) {
|
||||
const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB']
|
||||
let size = Number(value) || 0
|
||||
let unit = 0
|
||||
while (size >= 1024 && unit < units.length - 1) { size /= 1024; unit += 1 }
|
||||
return `${size >= 10 || unit === 0 ? size.toFixed(0) : size.toFixed(1)} ${units[unit]}`
|
||||
}
|
||||
|
||||
function shortId(value: string) { return `${value.slice(0, 12)}…` }
|
||||
function usagePercent(vault: Vault) { return Math.min(100, vault.quota ? vault.used / vault.quota * 100 : 0) }
|
||||
function firstCharacter(value: string, fallback: string) { return value.trim().slice(0, 1).toUpperCase() || fallback }
|
||||
|
||||
function notify(text: string, error = false) {
|
||||
toast.value = text
|
||||
toastError.value = error
|
||||
window.clearTimeout(toastTimer)
|
||||
toastTimer = window.setTimeout(() => { toast.value = '' }, 4800)
|
||||
}
|
||||
|
||||
async function refreshStatus() {
|
||||
checking.value = true
|
||||
try { Object.assign(service, await api.status()) }
|
||||
finally { checking.value = false }
|
||||
}
|
||||
|
||||
async function loadAccount() {
|
||||
const [vaultPayload, devicePayload] = await Promise.all([api.vaults(), api.devices()])
|
||||
vaults.value = vaultPayload.items
|
||||
devices.value = devicePayload.items
|
||||
}
|
||||
|
||||
async function refreshAccount() {
|
||||
if (busy.value) return
|
||||
busy.value = true
|
||||
try {
|
||||
await loadAccount()
|
||||
notify('账户数据已刷新')
|
||||
} catch (error) {
|
||||
if (!api.signedIn) leaveConsole()
|
||||
notify(error instanceof Error ? error.message : 'REFRESH_FAILED', true)
|
||||
} finally { busy.value = false }
|
||||
}
|
||||
|
||||
function leaveConsole() {
|
||||
api.clear()
|
||||
signedIn.value = false
|
||||
password.value = ''
|
||||
vaults.value = []
|
||||
devices.value = []
|
||||
}
|
||||
|
||||
async function signIn() {
|
||||
if (busy.value) return
|
||||
busy.value = true
|
||||
const secret = password.value
|
||||
const account = username.value.trim()
|
||||
const device = deviceName.value.trim()
|
||||
password.value = ''
|
||||
try {
|
||||
await api.login(account, secret, device)
|
||||
sessionLabel.value = `${account} · ${device}`
|
||||
signedIn.value = true
|
||||
await loadAccount()
|
||||
notify('设备会话已建立')
|
||||
} catch (error) {
|
||||
leaveConsole()
|
||||
notify(error instanceof Error ? error.message : 'LOGIN_FAILED', true)
|
||||
} finally { busy.value = false }
|
||||
}
|
||||
|
||||
async function createVault() {
|
||||
const name = newVaultName.value.trim()
|
||||
if (!name || busy.value) return
|
||||
busy.value = true
|
||||
try {
|
||||
await api.createVault(name)
|
||||
newVaultName.value = ''
|
||||
await loadAccount()
|
||||
notify(`已创建 Vault:${name}`)
|
||||
} catch (error) {
|
||||
notify(error instanceof Error ? error.message : 'CREATE_VAULT_FAILED', true)
|
||||
} finally { busy.value = false }
|
||||
}
|
||||
|
||||
async function revokeDevice(device: Device) {
|
||||
if (!window.confirm(`撤销设备“${device.name}”?该设备的下一次请求会被拒绝。`)) return
|
||||
busy.value = true
|
||||
try {
|
||||
await api.revokeDevice(device.id)
|
||||
if (device.id === api.deviceId) {
|
||||
leaveConsole()
|
||||
notify('当前设备已撤销')
|
||||
} else {
|
||||
await loadAccount()
|
||||
notify(`已撤销设备:${device.name}`)
|
||||
}
|
||||
} catch (error) {
|
||||
notify(error instanceof Error ? error.message : 'REVOKE_FAILED', true)
|
||||
} finally { busy.value = false }
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
if (busy.value) return
|
||||
busy.value = true
|
||||
try { await api.logout() }
|
||||
finally {
|
||||
leaveConsole()
|
||||
busy.value = false
|
||||
notify('会话已退出')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void refreshStatus()
|
||||
statusTimer = window.setInterval(() => { void refreshStatus() }, 15_000)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
window.clearInterval(statusTimer)
|
||||
window.clearTimeout(toastTimer)
|
||||
leaveConsole()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ambient ambient-one" aria-hidden="true" />
|
||||
<div class="ambient ambient-two" aria-hidden="true" />
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/console" aria-label="OpenNexus Sync Console">
|
||||
<span class="brand-mark" aria-hidden="true"><i /><i /><i /></span>
|
||||
<span><strong>OpenNexus</strong><small>Sync Console</small></span>
|
||||
</a>
|
||||
<div class="service-strip" aria-label="服务状态">
|
||||
<span class="status-chip" :class="{ ok: service.health, bad: !checking && !service.health }"><i /><b>服务</b><em>{{ checking ? '检测中' : service.health ? '在线' : '离线' }}</em></span>
|
||||
<span class="status-chip" :class="{ ok: service.ready, bad: !checking && !service.ready }"><i /><b>依赖</b><em>{{ checking ? '检测中' : service.ready ? '就绪' : '不可用' }}</em></span>
|
||||
<button class="icon-button" type="button" :disabled="checking" aria-label="刷新服务状态" title="刷新服务状态" @click="refreshStatus">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 11a8 8 0 1 0-2.3 5.7M20 4v7h-7" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section v-if="!signedIn" class="hero">
|
||||
<div class="hero-copy">
|
||||
<div class="eyebrow"><span /> Self-hosted workspace sync</div>
|
||||
<h1>让知识在设备之间<br><em>安静地抵达。</em></h1>
|
||||
<p>OpenNexus Sync 为每个 Vault 保留完整修订历史、附件完整性校验和设备级撤销。此控制台只连接当前服务器,不在浏览器持久保存密码或令牌。</p>
|
||||
<div class="protocol-grid" aria-label="协议能力">
|
||||
<article><strong>v{{ service.protocol }}</strong><span>同步协议</span></article>
|
||||
<article><strong>{{ formatBytes(service.maxObjectSize) }}</strong><span>单对象上限</span></article>
|
||||
<article><strong>SHA-256</strong><span>内容完整性</span></article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="login-card" aria-labelledby="login-title">
|
||||
<div class="card-glow" aria-hidden="true" />
|
||||
<div class="card-heading">
|
||||
<span class="lock-mark" aria-hidden="true"><svg viewBox="0 0 24 24"><rect x="5" y="10" width="14" height="11" rx="3" /><path d="M8 10V7a4 4 0 0 1 8 0v3" /></svg></span>
|
||||
<div><p>设备会话</p><h2 id="login-title">登录 Sync Server</h2></div>
|
||||
</div>
|
||||
<form @submit.prevent="signIn">
|
||||
<label>账户<input v-model="username" autocomplete="username" maxlength="80" required placeholder="输入 Sync 账户"></label>
|
||||
<label>密码<input v-model="password" type="password" autocomplete="current-password" minlength="12" maxlength="256" required placeholder="至少 12 个字符"></label>
|
||||
<label>设备名称<input v-model="deviceName" autocomplete="off" maxlength="120" required></label>
|
||||
<button class="primary-button" type="submit" :disabled="busy || !password"><span>{{ busy ? '正在建立会话…' : '建立安全会话' }}</span><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m9 18 6-6-6-6" /></svg></button>
|
||||
</form>
|
||||
<p class="privacy-note"><span>●</span>密码在发出请求前即从输入框清除;会话令牌只保存在当前页面内存。</p>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section v-else class="console-view">
|
||||
<div class="console-heading">
|
||||
<div><div class="eyebrow"><span /> Connected workspace</div><h1>同步空间</h1><p>{{ sessionLabel }}</p></div>
|
||||
<button class="secondary-button" type="button" :disabled="busy" @click="logout">退出登录</button>
|
||||
</div>
|
||||
|
||||
<div class="metric-grid">
|
||||
<article><span>远端 Vault</span><strong>{{ vaults.length }}</strong><small>当前账户可访问</small></article>
|
||||
<article><span>已使用空间</span><strong>{{ formatBytes(used) }}</strong><small>总配额 {{ formatBytes(quota) }}</small></article>
|
||||
<article><span>设备</span><strong>{{ devices.length }}</strong><small>{{ activeDevices }} 台可访问</small></article>
|
||||
<article><span>协议</span><strong>v{{ service.protocol }}</strong><small>历史长期保留</small></article>
|
||||
</div>
|
||||
|
||||
<div class="content-grid">
|
||||
<section class="surface">
|
||||
<div class="surface-heading">
|
||||
<div><p>Vaults</p><h2>远端知识库</h2></div>
|
||||
<button class="icon-button" type="button" :disabled="busy" aria-label="刷新账户数据" title="刷新账户数据" @click="refreshAccount">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 11a8 8 0 1 0-2.3 5.7M20 4v7h-7" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
<form class="create-form" @submit.prevent="createVault">
|
||||
<label>创建新 Vault</label>
|
||||
<div><input v-model="newVaultName" maxlength="120" required placeholder="例如:产品知识库"><button type="submit" :disabled="busy || !newVaultName.trim()">创建</button></div>
|
||||
</form>
|
||||
<div class="vault-list">
|
||||
<div v-if="!vaults.length" class="empty-state">还没有远端 Vault</div>
|
||||
<article v-for="vault in vaults" :key="vault.id" class="vault-item">
|
||||
<div class="vault-symbol">N</div>
|
||||
<div class="item-copy">
|
||||
<strong>{{ vault.name }}</strong><small>{{ shortId(vault.id) }} · revision {{ vault.sequence }}</small>
|
||||
<progress :value="usagePercent(vault)" max="100" />
|
||||
</div>
|
||||
<div class="item-side"><strong>{{ formatBytes(vault.used) }}</strong><small>of {{ formatBytes(vault.quota) }}</small></div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="surface">
|
||||
<div class="surface-heading"><div><p>Devices</p><h2>设备会话</h2></div><span class="secure-badge">逐次鉴权</span></div>
|
||||
<p class="surface-intro">撤销会让该设备的下一次请求立即失败。Vault 内容和本地文件不会因此删除。</p>
|
||||
<div class="device-list">
|
||||
<article v-for="device in devices" :key="device.id" class="device-item" :class="{ revoked: device.revoked }">
|
||||
<div class="device-symbol">{{ firstCharacter(device.name, 'D') }}</div>
|
||||
<div class="item-copy"><strong>{{ device.name }}</strong><small>{{ device.revoked ? '已撤销' : '可访问' }}<template v-if="device.id === api.deviceId"> · 当前设备</template></small></div>
|
||||
<button v-if="!device.revoked" class="danger-button" type="button" :disabled="busy" @click="revokeDevice(device)">撤销</button>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer><span>OpenNexus Sync v1</span><span>Transport encryption · Device revocation · Immutable history</span></footer>
|
||||
<div v-if="toast" class="toast" :class="{ error: toastError }" role="status" aria-live="polite">{{ toast }}</div>
|
||||
</template>
|
||||
@@ -0,0 +1,140 @@
|
||||
export interface ServiceStatus {
|
||||
health: boolean
|
||||
ready: boolean
|
||||
protocol: number
|
||||
maxObjectSize: number
|
||||
}
|
||||
|
||||
export interface Session {
|
||||
access_token: string
|
||||
refresh_token: string
|
||||
expires_in: number
|
||||
device_id: string
|
||||
}
|
||||
|
||||
export interface Vault {
|
||||
id: string
|
||||
name: string
|
||||
sequence: number
|
||||
used: number
|
||||
quota: number
|
||||
}
|
||||
|
||||
export interface Device {
|
||||
id: string
|
||||
name: string
|
||||
revoked: number | boolean
|
||||
}
|
||||
|
||||
interface ErrorPayload {
|
||||
error?: { code?: string }
|
||||
}
|
||||
|
||||
async function safeJson<T>(response: Response): Promise<T | null> {
|
||||
try { return await response.json() as T }
|
||||
catch { return null }
|
||||
}
|
||||
|
||||
export class SyncApi {
|
||||
private access = ''
|
||||
private refresh = ''
|
||||
deviceId = ''
|
||||
|
||||
get signedIn() { return Boolean(this.access) }
|
||||
|
||||
private async raw(path: string, init: RequestInit = {}, authenticate = true): Promise<Response> {
|
||||
const headers = new Headers(init.headers)
|
||||
if (init.body && !headers.has('Content-Type')) headers.set('Content-Type', 'application/json')
|
||||
if (authenticate && this.access) headers.set('Authorization', `Bearer ${this.access}`)
|
||||
return fetch(path, { ...init, headers, cache: 'no-store', credentials: 'same-origin' })
|
||||
}
|
||||
|
||||
private async rotate(): Promise<boolean> {
|
||||
if (!this.refresh) return false
|
||||
const response = await this.raw('/sync/v1/auth/refresh', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ refresh_token: this.refresh }),
|
||||
}, false)
|
||||
if (!response.ok) return false
|
||||
const session = await safeJson<Session>(response)
|
||||
if (!session) return false
|
||||
this.accept(session)
|
||||
return true
|
||||
}
|
||||
|
||||
private async request<T>(path: string, init: RequestInit = {}, allowRotate = true): Promise<T> {
|
||||
let response = await this.raw(path, init)
|
||||
if (response.status === 401 && allowRotate && await this.rotate()) response = await this.raw(path, init)
|
||||
if (!response.ok) {
|
||||
const payload = await safeJson<ErrorPayload>(response)
|
||||
if (response.status === 401) this.clear()
|
||||
throw new Error(payload?.error?.code ?? `HTTP_${response.status}`)
|
||||
}
|
||||
if (response.status === 204) return undefined as T
|
||||
const payload = await safeJson<T>(response)
|
||||
if (payload === null) throw new Error('INVALID_RESPONSE')
|
||||
return payload
|
||||
}
|
||||
|
||||
private accept(session: Session) {
|
||||
this.access = session.access_token
|
||||
this.refresh = session.refresh_token
|
||||
this.deviceId = session.device_id
|
||||
}
|
||||
|
||||
async status(): Promise<ServiceStatus> {
|
||||
const [health, ready, handshake] = await Promise.allSettled([
|
||||
this.raw('/health', {}, false),
|
||||
this.raw('/ready', {}, false),
|
||||
this.raw('/sync/v1/handshake?protocol=1', {}, false),
|
||||
])
|
||||
let protocol = 1
|
||||
let maxObjectSize = 100 * 1024 * 1024
|
||||
if (handshake.status === 'fulfilled' && handshake.value.ok) {
|
||||
const payload = await safeJson<{ protocol: number; max_object_size: number }>(handshake.value)
|
||||
protocol = payload?.protocol ?? protocol
|
||||
maxObjectSize = payload?.max_object_size ?? maxObjectSize
|
||||
}
|
||||
return {
|
||||
health: health.status === 'fulfilled' && health.value.ok,
|
||||
ready: ready.status === 'fulfilled' && ready.value.ok,
|
||||
protocol,
|
||||
maxObjectSize,
|
||||
}
|
||||
}
|
||||
|
||||
async login(username: string, password: string, deviceName: string): Promise<void> {
|
||||
const response = await this.raw('/sync/v1/auth/sessions', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ username, password, device_name: deviceName }),
|
||||
}, false)
|
||||
if (!response.ok) {
|
||||
const payload = await safeJson<ErrorPayload>(response)
|
||||
throw new Error(payload?.error?.code ?? `HTTP_${response.status}`)
|
||||
}
|
||||
const session = await safeJson<Session>(response)
|
||||
if (!session) throw new Error('INVALID_RESPONSE')
|
||||
this.accept(session)
|
||||
}
|
||||
|
||||
vaults() { return this.request<{ items: Vault[] }>('/sync/v1/vaults') }
|
||||
devices() { return this.request<{ items: Device[] }>('/sync/v1/devices') }
|
||||
createVault(name: string) {
|
||||
return this.request<{ vault_id: string; name: string }>('/sync/v1/vaults', {
|
||||
method: 'POST', body: JSON.stringify({ name }),
|
||||
})
|
||||
}
|
||||
revokeDevice(id: string) {
|
||||
return this.request<void>(`/sync/v1/devices/${encodeURIComponent(id)}`, { method: 'DELETE' })
|
||||
}
|
||||
async logout(): Promise<void> {
|
||||
try {
|
||||
if (this.access) await this.request<void>('/sync/v1/auth/sessions', { method: 'DELETE' }, false)
|
||||
} finally { this.clear() }
|
||||
}
|
||||
clear() {
|
||||
this.access = ''
|
||||
this.refresh = ''
|
||||
this.deviceId = ''
|
||||
}
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import './style.css'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
@@ -0,0 +1,175 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg: #06100d;
|
||||
--surface: rgba(13, 29, 24, .84);
|
||||
--surface-strong: #10241d;
|
||||
--line: rgba(169, 222, 196, .15);
|
||||
--line-strong: rgba(169, 222, 196, .28);
|
||||
--text: #edf7f2;
|
||||
--muted: #8fa99c;
|
||||
--mint: #72e0ad;
|
||||
--mint-bright: #a6f4cf;
|
||||
--purple: #a991ff;
|
||||
--danger: #ff8e8e;
|
||||
--warning: #f5ca72;
|
||||
--shadow: 0 26px 90px rgba(0, 0, 0, .34);
|
||||
font-family: Inter, "Segoe UI", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html { min-width: 320px; min-height: 100%; background: var(--bg); }
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
color: var(--text);
|
||||
background:
|
||||
linear-gradient(rgba(255,255,255,.018) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255,255,255,.018) 1px, transparent 1px),
|
||||
radial-gradient(circle at 48% 0%, #15372c 0, var(--bg) 46%);
|
||||
background-size: 52px 52px, 52px 52px, auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
button, input { font: inherit; }
|
||||
button { color: inherit; }
|
||||
button:focus-visible, input:focus-visible { outline: 2px solid var(--mint); outline-offset: 3px; }
|
||||
button:disabled { cursor: default; opacity: .5; }
|
||||
|
||||
.ambient { position: fixed; border-radius: 50%; filter: blur(80px); pointer-events: none; opacity: .16; }
|
||||
.ambient-one { width: 420px; height: 420px; background: var(--mint); top: -220px; right: 8%; }
|
||||
.ambient-two { width: 360px; height: 360px; background: var(--purple); bottom: -220px; left: -100px; opacity: .1; }
|
||||
.topbar {
|
||||
width: min(1280px, calc(100% - 48px));
|
||||
height: 88px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--line);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
.brand { color: inherit; text-decoration: none; display: inline-flex; align-items: center; gap: 13px; }
|
||||
.brand > span:last-child { display: grid; gap: 1px; }
|
||||
.brand strong { font-size: 17px; letter-spacing: -.02em; }
|
||||
.brand small { color: var(--muted); font-size: 11px; letter-spacing: .12em; text-transform: uppercase; }
|
||||
.brand-mark { width: 34px; height: 34px; display: grid; place-items: center; position: relative; transform: rotate(30deg); }
|
||||
.brand-mark i { position: absolute; display: block; border: 1.5px solid var(--mint); border-radius: 4px; }
|
||||
.brand-mark i:nth-child(1) { width: 25px; height: 25px; opacity: .45; }
|
||||
.brand-mark i:nth-child(2) { width: 17px; height: 17px; opacity: .72; }
|
||||
.brand-mark i:nth-child(3) { width: 8px; height: 8px; background: var(--mint); box-shadow: 0 0 20px rgba(114,224,173,.8); }
|
||||
.service-strip { display: flex; align-items: center; gap: 9px; }
|
||||
.status-chip { min-height: 34px; padding: 0 12px; border: 1px solid var(--line); border-radius: 999px; display: flex; align-items: center; gap: 7px; background: rgba(7,18,15,.7); }
|
||||
.status-chip i { width: 7px; height: 7px; border-radius: 50%; background: var(--warning); box-shadow: 0 0 12px currentColor; }
|
||||
.status-chip b { color: var(--muted); font-size: 11px; font-weight: 500; }
|
||||
.status-chip em { font-size: 11px; font-style: normal; }
|
||||
.status-chip.ok i { background: var(--mint); }
|
||||
.status-chip.bad i { background: var(--danger); }
|
||||
.status-chip.bad em { color: var(--danger); }
|
||||
.icon-button { width: 36px; height: 36px; border: 1px solid var(--line); border-radius: 10px; display: grid; place-items: center; background: rgba(14,31,25,.7); cursor: pointer; transition: .18s ease; }
|
||||
.icon-button:hover { border-color: var(--line-strong); background: var(--surface-strong); transform: translateY(-1px); }
|
||||
.icon-button svg { width: 17px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
|
||||
|
||||
main { width: min(1180px, calc(100% - 48px)); margin: 0 auto; position: relative; z-index: 1; }
|
||||
.hero { min-height: calc(100vh - 150px); display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(360px, .75fr); gap: clamp(48px, 8vw, 116px); align-items: center; padding: 64px 0 82px; }
|
||||
.hero-copy { max-width: 680px; }
|
||||
.eyebrow { color: var(--mint); font-size: 11px; font-weight: 700; letter-spacing: .18em; text-transform: uppercase; display: flex; align-items: center; gap: 10px; }
|
||||
.eyebrow span { width: 24px; height: 1px; background: var(--mint); box-shadow: 0 0 10px var(--mint); }
|
||||
.hero h1, .console-heading h1 { margin: 20px 0; letter-spacing: -.06em; line-height: .99; font-size: clamp(50px, 6.4vw, 86px); font-weight: 620; }
|
||||
.hero h1 em { font-style: normal; color: transparent; background: linear-gradient(95deg, var(--mint-bright), #82d8c1 47%, var(--purple)); background-clip: text; -webkit-background-clip: text; }
|
||||
.hero-copy > p { max-width: 620px; color: var(--muted); font-size: 16px; line-height: 1.85; }
|
||||
.protocol-grid { display: grid; grid-template-columns: repeat(3, 1fr); margin-top: 42px; border: 1px solid var(--line); border-radius: 16px; overflow: hidden; background: rgba(8,22,17,.45); backdrop-filter: blur(15px); }
|
||||
.protocol-grid article { padding: 19px 22px; display: grid; gap: 5px; border-right: 1px solid var(--line); }
|
||||
.protocol-grid article:last-child { border-right: 0; }
|
||||
.protocol-grid strong { font-size: 18px; font-weight: 600; }
|
||||
.protocol-grid span { color: var(--muted); font-size: 11px; }
|
||||
.login-card { padding: 31px; border: 1px solid var(--line-strong); border-radius: 24px; background: linear-gradient(145deg, rgba(18,40,32,.94), rgba(8,21,17,.9)); box-shadow: var(--shadow); position: relative; overflow: hidden; backdrop-filter: blur(25px); }
|
||||
.card-glow { position: absolute; width: 210px; height: 210px; right: -100px; top: -120px; border-radius: 50%; background: var(--mint); filter: blur(60px); opacity: .16; }
|
||||
.card-heading { display: flex; gap: 14px; align-items: center; margin-bottom: 28px; position: relative; }
|
||||
.card-heading p, .surface-heading p { margin: 0 0 3px; color: var(--mint); font-size: 10px; font-weight: 700; letter-spacing: .13em; text-transform: uppercase; }
|
||||
.card-heading h2, .surface-heading h2 { margin: 0; font-size: 21px; letter-spacing: -.025em; }
|
||||
.lock-mark { width: 43px; height: 43px; border-radius: 13px; display: grid; place-items: center; background: rgba(114,224,173,.1); color: var(--mint); border: 1px solid rgba(114,224,173,.18); }
|
||||
.lock-mark svg { width: 21px; fill: none; stroke: currentColor; stroke-width: 1.7; }
|
||||
form { position: relative; }
|
||||
.login-card form { display: grid; gap: 17px; }
|
||||
label { display: grid; gap: 7px; color: var(--muted); font-size: 11px; font-weight: 600; letter-spacing: .04em; }
|
||||
input { width: 100%; color: var(--text); background: rgba(3,12,9,.58); border: 1px solid var(--line); border-radius: 11px; padding: 12px 13px; transition: border-color .18s, background .18s; }
|
||||
input::placeholder { color: #5f786c; }
|
||||
input:hover, input:focus { border-color: rgba(114,224,173,.46); background: rgba(3,12,9,.8); }
|
||||
.primary-button { width: 100%; margin-top: 5px; padding: 13px 16px; border: 0; border-radius: 11px; display: flex; justify-content: center; align-items: center; gap: 8px; color: #062015; background: linear-gradient(100deg, var(--mint-bright), var(--mint)); font-weight: 750; cursor: pointer; box-shadow: 0 10px 35px rgba(114,224,173,.16); transition: .18s ease; }
|
||||
.primary-button:hover { transform: translateY(-1px); filter: brightness(1.05); }
|
||||
.primary-button svg { width: 18px; fill: none; stroke: currentColor; stroke-width: 2; }
|
||||
.privacy-note { margin: 18px 0 0; color: #718b7e; font-size: 10px; line-height: 1.5; display: flex; gap: 8px; }
|
||||
.privacy-note span { color: var(--mint); font-size: 7px; margin-top: 3px; }
|
||||
|
||||
.console-view { padding: 64px 0 90px; min-height: calc(100vh - 150px); }
|
||||
.console-heading { display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 38px; }
|
||||
.console-heading h1 { font-size: clamp(42px, 5vw, 64px); margin: 13px 0 7px; }
|
||||
.console-heading p { margin: 0; color: var(--muted); }
|
||||
.secondary-button { padding: 10px 15px; border-radius: 10px; border: 1px solid var(--line-strong); background: transparent; cursor: pointer; }
|
||||
.secondary-button:hover { border-color: var(--mint); color: var(--mint-bright); }
|
||||
.metric-grid { display: grid; grid-template-columns: repeat(4, 1fr); border: 1px solid var(--line); background: rgba(8,22,17,.52); border-radius: 18px; overflow: hidden; margin-bottom: 22px; }
|
||||
.metric-grid article { min-height: 134px; padding: 23px; display: flex; flex-direction: column; border-right: 1px solid var(--line); }
|
||||
.metric-grid article:last-child { border-right: 0; }
|
||||
.metric-grid span { color: var(--muted); font-size: 11px; }
|
||||
.metric-grid strong { margin: 12px 0 8px; font-size: 27px; font-weight: 620; letter-spacing: -.04em; }
|
||||
.metric-grid small { color: #668075; font-size: 10px; }
|
||||
.content-grid { display: grid; grid-template-columns: 1.25fr .75fr; gap: 22px; }
|
||||
.surface { border: 1px solid var(--line); border-radius: 18px; background: var(--surface); padding: 25px; backdrop-filter: blur(18px); }
|
||||
.surface-heading { display: flex; justify-content: space-between; align-items: center; margin-bottom: 22px; }
|
||||
.secure-badge { color: var(--mint); background: rgba(114,224,173,.08); border: 1px solid rgba(114,224,173,.18); padding: 6px 9px; border-radius: 999px; font-size: 9px; letter-spacing: .08em; }
|
||||
.surface-intro { margin: -9px 0 19px; color: var(--muted); font-size: 12px; line-height: 1.65; }
|
||||
.create-form { margin-bottom: 20px; padding: 16px; border-radius: 13px; background: rgba(4,15,11,.42); border: 1px solid var(--line); }
|
||||
.create-form > div { display: flex; gap: 8px; margin-top: 8px; }
|
||||
.create-form input { padding: 10px 12px; }
|
||||
.create-form button { border: 0; border-radius: 9px; padding: 0 17px; color: #082016; background: var(--mint); font-weight: 700; cursor: pointer; }
|
||||
.vault-list, .device-list { display: grid; gap: 9px; }
|
||||
.vault-item, .device-item { min-width: 0; border: 1px solid var(--line); background: rgba(6,19,14,.48); border-radius: 13px; padding: 15px 16px; display: flex; align-items: center; gap: 13px; }
|
||||
.vault-symbol, .device-symbol { width: 38px; height: 38px; flex: 0 0 auto; display: grid; place-items: center; border-radius: 11px; background: rgba(114,224,173,.08); color: var(--mint); font-weight: 700; }
|
||||
.device-symbol { color: var(--purple); background: rgba(169,145,255,.08); }
|
||||
.item-copy { min-width: 0; flex: 1; }
|
||||
.item-copy strong { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; }
|
||||
.item-copy small { color: var(--muted); font-size: 10px; }
|
||||
progress { display: block; width: 100%; height: 4px; margin-top: 9px; border: 0; border-radius: 99px; overflow: hidden; accent-color: var(--mint); }
|
||||
progress::-webkit-progress-bar { background: rgba(255,255,255,.06); }
|
||||
progress::-webkit-progress-value { background: linear-gradient(90deg, var(--mint), var(--purple)); }
|
||||
.item-side { text-align: right; flex: 0 0 auto; }
|
||||
.item-side strong { display: block; font-size: 12px; }
|
||||
.item-side small { color: var(--muted); font-size: 9px; }
|
||||
.danger-button { border: 1px solid rgba(255,142,142,.25); color: var(--danger); background: rgba(255,142,142,.05); border-radius: 8px; padding: 7px 9px; cursor: pointer; font-size: 10px; }
|
||||
.danger-button:hover { background: rgba(255,142,142,.12); }
|
||||
.revoked { opacity: .48; }
|
||||
.empty-state { min-height: 100px; display: grid; place-items: center; color: var(--muted); border: 1px dashed var(--line); border-radius: 12px; font-size: 12px; }
|
||||
footer { width: min(1280px, calc(100% - 48px)); min-height: 62px; margin: 0 auto; border-top: 1px solid var(--line); display: flex; justify-content: space-between; align-items: center; color: #597166; font-size: 9px; letter-spacing: .04em; position: relative; z-index: 1; }
|
||||
.toast { position: fixed; z-index: 10; right: 24px; bottom: 24px; max-width: min(420px, calc(100% - 48px)); padding: 13px 16px; border: 1px solid var(--line-strong); border-radius: 11px; background: #142b23; box-shadow: var(--shadow); font-size: 12px; }
|
||||
.toast.error { color: #ffd2d2; border-color: rgba(255,142,142,.35); background: #321a1a; }
|
||||
|
||||
@media (max-width: 920px) {
|
||||
.hero { grid-template-columns: 1fr; padding-top: 76px; }
|
||||
.hero-copy { max-width: none; }
|
||||
.login-card { max-width: 560px; width: 100%; }
|
||||
.content-grid { grid-template-columns: 1fr; }
|
||||
.metric-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.metric-grid article:nth-child(2) { border-right: 0; }
|
||||
.metric-grid article:nth-child(-n+2) { border-bottom: 1px solid var(--line); }
|
||||
}
|
||||
@media (max-width: 620px) {
|
||||
.topbar, main, footer { width: min(100% - 28px, 1180px); }
|
||||
.topbar { height: 74px; }
|
||||
.brand small, .status-chip b, footer span:last-child { display: none; }
|
||||
.status-chip { padding: 0 9px; }
|
||||
.hero { padding: 52px 0 64px; gap: 45px; }
|
||||
.hero h1 { font-size: 43px; }
|
||||
.hero-copy > p { font-size: 14px; }
|
||||
.protocol-grid article { padding: 15px 11px; }
|
||||
.protocol-grid strong { font-size: 14px; }
|
||||
.login-card, .surface { padding: 21px; }
|
||||
.console-heading { align-items: flex-start; gap: 18px; }
|
||||
.metric-grid { grid-template-columns: 1fr; }
|
||||
.metric-grid article { min-height: 116px; border-right: 0; border-bottom: 1px solid var(--line); }
|
||||
.metric-grid article:last-child { border-bottom: 0; }
|
||||
.create-form > div { flex-direction: column; }
|
||||
.create-form button { padding: 10px; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after { scroll-behavior: auto !important; transition: none !important; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"types": ["vite/client"],
|
||||
"skipLibCheck": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.vue", "vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
base: '/console/',
|
||||
plugins: [vue()],
|
||||
build: {
|
||||
outDir: '../sync_server/static',
|
||||
emptyOutDir: true,
|
||||
sourcemap: false,
|
||||
assetsInlineLimit: 0,
|
||||
},
|
||||
server: {
|
||||
port: 5174,
|
||||
strictPort: true,
|
||||
proxy: {
|
||||
'/health': 'http://127.0.0.1:18081',
|
||||
'/ready': 'http://127.0.0.1:18081',
|
||||
'/sync': 'http://127.0.0.1:18081',
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -16,6 +16,7 @@ from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.responses import JSONResponse, FileResponse
|
||||
from starlette.background import BackgroundTask
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
from starlette.staticfiles import StaticFiles
|
||||
|
||||
from .database import Database, password_hash, row, rows, run
|
||||
from .models import Commit, Login, Refresh, Upload, VaultCreate
|
||||
@@ -69,6 +70,28 @@ def create_app(db: Database, objects, staging: Path, *, quota=1024**3, clock=tim
|
||||
app = FastAPI(title="OpenNexus Sync", version="1.0.0", lifespan=lifespan)
|
||||
app.state.database = db
|
||||
worker_id = secrets.token_hex(8)
|
||||
console_root = Path(__file__).with_name("static")
|
||||
|
||||
@app.middleware("http")
|
||||
async def console_security(request: Request, call_next):
|
||||
response = await call_next(request)
|
||||
if request.url.path == "/" or request.url.path.startswith("/console"):
|
||||
response.headers["Cache-Control"] = "no-store"
|
||||
response.headers["Content-Security-Policy"] = (
|
||||
"default-src 'none'; script-src 'self'; style-src 'self'; "
|
||||
"img-src 'self' data:; connect-src 'self'; font-src 'self'; "
|
||||
"base-uri 'none'; form-action 'self'; frame-ancestors 'none'"
|
||||
)
|
||||
response.headers["Referrer-Policy"] = "no-referrer"
|
||||
response.headers["X-Content-Type-Options"] = "nosniff"
|
||||
response.headers["X-Frame-Options"] = "DENY"
|
||||
return response
|
||||
|
||||
@app.get("/", include_in_schema=False)
|
||||
def console():
|
||||
return FileResponse(console_root / "index.html", media_type="text/html; charset=utf-8")
|
||||
|
||||
app.mount("/console", StaticFiles(directory=console_root, html=True), name="sync-console")
|
||||
|
||||
@app.exception_handler(SyncError)
|
||||
async def error(_request, exc):
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="color-scheme" content="dark">
|
||||
<meta name="theme-color" content="#07120f">
|
||||
<title>OpenNexus Sync Console</title>
|
||||
<script type="module" crossorigin src="/console/assets/index-CV7tuz7b.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/console/assets/index-BS5pfOkS.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,63 @@
|
||||
"""Same-origin Vue console delivery and its public account workflow."""
|
||||
|
||||
import re
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from sync_server.app import create_app
|
||||
from sync_server.database import Database
|
||||
from sync_server.storage import DiskObjects
|
||||
|
||||
|
||||
def test_vue_console_is_hardened_and_uses_the_public_sync_api(tmp_path):
|
||||
database = Database("sqlite:///" + str(tmp_path / "sync.db"))
|
||||
app = create_app(
|
||||
database,
|
||||
DiskObjects(tmp_path / "objects"),
|
||||
tmp_path / "staging",
|
||||
)
|
||||
database.add_user("demo", "controlled-demo-password")
|
||||
with TestClient(app) as client:
|
||||
root = client.get("/")
|
||||
console = client.get("/console/")
|
||||
assert root.status_code == console.status_code == 200
|
||||
assert "OpenNexus Sync Console" in console.text
|
||||
assert "<script type=\"module\"" in console.text
|
||||
assert "controlled-demo-password" not in console.text
|
||||
for response in (root, console):
|
||||
assert response.headers["cache-control"] == "no-store"
|
||||
assert response.headers["x-content-type-options"] == "nosniff"
|
||||
assert response.headers["x-frame-options"] == "DENY"
|
||||
assert "script-src 'self'" in response.headers["content-security-policy"]
|
||||
assert "frame-ancestors 'none'" in response.headers["content-security-policy"]
|
||||
|
||||
assets = re.findall(r'(?:src|href)="(/console/assets/[^"]+)"', console.text)
|
||||
assert len(assets) == 2
|
||||
bodies = []
|
||||
for asset in assets:
|
||||
response = client.get(asset)
|
||||
assert response.status_code == 200
|
||||
assert response.headers["cache-control"] == "no-store"
|
||||
assert response.headers["x-content-type-options"] == "nosniff"
|
||||
bodies.append(response.content)
|
||||
bundle = b"".join(bodies)
|
||||
assert b"localStorage" not in bundle
|
||||
assert b"sessionStorage" not in bundle
|
||||
assert b"controlled-demo-password" not in bundle
|
||||
|
||||
session = client.post(
|
||||
"/sync/v1/auth/sessions",
|
||||
json={
|
||||
"username": "demo",
|
||||
"password": "controlled-demo-password",
|
||||
"device_name": "Console test",
|
||||
},
|
||||
)
|
||||
assert session.status_code == 200
|
||||
token = session.json()["access_token"]
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
created = client.post("/sync/v1/vaults", headers=headers, json={"name": "Demo Vault"})
|
||||
assert created.status_code == 200
|
||||
assert client.get("/sync/v1/vaults", headers=headers).json()["items"][0]["name"] == "Demo Vault"
|
||||
assert client.get("/sync/v1/devices", headers=headers).json()["items"][0]["name"] == "Console test"
|
||||
database.engine.dispose()
|
||||
Reference in New Issue
Block a user