From 3fb7b4da8c47254a9e90fe4500e4db55018d5d2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=8B=E4=B8=80=E9=B8=A3?= <1000000008@qq.com> Date: Mon, 9 Nov 2026 17:54:52 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=9A=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E4=BA=91=E7=AE=A1API=E4=B8=8EWeb=E6=8E=A7=E5=88=B6=E5=8F=B0?= =?UTF-8?q?=E5=B9=B6=E5=AE=8C=E6=88=90=E7=8E=B0=E5=9C=BA=E6=8E=A5=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 12 + .gitignore | 12 + console/.env.example | 2 + console/.gitkeep | 1 - console/Dockerfile | 13 + console/README.md | 21 + console/e2e/console.spec.ts | 102 + console/e2e/live.spec.ts | 40 + console/e2e/ui.spec.ts | 84 + console/eslint.config.js | 11 + console/index.html | 2 + console/package-lock.json | 4690 ++++++++++++++++++++++ console/package.json | 52 + console/playwright.config.ts | 11 + console/playwright.live.config.ts | 5 + console/public/mockServiceWorker.js | 361 ++ console/src/App.vue | 1 + console/src/api/index.ts | 74 + console/src/api/schema.d.ts | 3732 ++++++++++++++++++ console/src/assets/brand-mark.svg | 1 + console/src/assets/kylinsoft.png | Bin 0 -> 10564 bytes console/src/assets/ubuntu.svg | 1 + console/src/components/StatusTag.vue | 7 + console/src/components/SystemIcon.vue | 13 + console/src/components/ThemeToggle.vue | 8 + console/src/composables/useAction.ts | 20 + console/src/composables/useTheme.ts | 19 + console/src/layouts/ConsoleLayout.vue | 53 + console/src/main.ts | 21 + console/src/mocks/browser.ts | 3 + console/src/mocks/handlers.ts | 47 + console/src/router/index.ts | 25 + console/src/stores/auth.test.ts | 13 + console/src/stores/auth.ts | 27 + console/src/stores/platform.ts | 29 + console/src/styles/tokens.css | 74 + console/src/views/Desktops.vue | 47 + console/src/views/Forbidden.vue | 1 + console/src/views/Login.vue | 27 + console/src/views/NotFound.vue | 1 + console/src/views/Overview.vue | 47 + console/src/views/Resources.vue | 16 + console/src/views/Sessions.vue | 30 + console/src/views/Settings.vue | 18 + console/src/views/Tasks.vue | 33 + console/src/views/Terminals.vue | 29 + console/src/views/Users.vue | 26 + console/tsconfig.json | 12 + console/vite.config.ts | 8 + console/vitest.config.ts | 2 + deploy/.gitkeep | 1 - deploy/compose.dev.yml | 44 + deploy/compose.live.yml | 59 + deploy/nginx.conf | 13 + deploy/nginx.live.conf | 17 + docs/openapi/platform.v1.json | 4824 +++++++++++++++++++++++ platform/.env.example | 11 + platform/.gitkeep | 1 - platform/Dockerfile | 13 + platform/README.md | 14 + platform/cmd/api-gateway/main.go | 58 + platform/go.mod | 47 + platform/go.sum | 109 + platform/internal/config/config.go | 39 + platform/internal/model/model.go | 91 + platform/internal/server/admin.go | 553 +++ platform/internal/server/server.go | 214 + platform/internal/server/server_test.go | 375 ++ platform/internal/server/terminal.go | 291 ++ platform/internal/store/schema.sql | 59 + platform/internal/store/store.go | 119 + platform/pkg/proxmox/client.go | 146 + platform/pkg/proxmox/client_test.go | 77 + tools/generate-platform-openapi.mjs | 98 + tools/platform-terminal-simulator.py | 53 + tools/prepare-live-config.py | 43 + tools/provision-pve-access.py | 62 + tools/start-cloud-dev.ps1 | 42 + tools/start-cloud-live.ps1 | 13 + 79 files changed, 17367 insertions(+), 3 deletions(-) create mode 100644 .dockerignore create mode 100644 console/.env.example delete mode 100644 console/.gitkeep create mode 100644 console/Dockerfile create mode 100644 console/README.md create mode 100644 console/e2e/console.spec.ts create mode 100644 console/e2e/live.spec.ts create mode 100644 console/e2e/ui.spec.ts create mode 100644 console/eslint.config.js create mode 100644 console/index.html create mode 100644 console/package-lock.json create mode 100644 console/package.json create mode 100644 console/playwright.config.ts create mode 100644 console/playwright.live.config.ts create mode 100644 console/public/mockServiceWorker.js create mode 100644 console/src/App.vue create mode 100644 console/src/api/index.ts create mode 100644 console/src/api/schema.d.ts create mode 100644 console/src/assets/brand-mark.svg create mode 100644 console/src/assets/kylinsoft.png create mode 100644 console/src/assets/ubuntu.svg create mode 100644 console/src/components/StatusTag.vue create mode 100644 console/src/components/SystemIcon.vue create mode 100644 console/src/components/ThemeToggle.vue create mode 100644 console/src/composables/useAction.ts create mode 100644 console/src/composables/useTheme.ts create mode 100644 console/src/layouts/ConsoleLayout.vue create mode 100644 console/src/main.ts create mode 100644 console/src/mocks/browser.ts create mode 100644 console/src/mocks/handlers.ts create mode 100644 console/src/router/index.ts create mode 100644 console/src/stores/auth.test.ts create mode 100644 console/src/stores/auth.ts create mode 100644 console/src/stores/platform.ts create mode 100644 console/src/styles/tokens.css create mode 100644 console/src/views/Desktops.vue create mode 100644 console/src/views/Forbidden.vue create mode 100644 console/src/views/Login.vue create mode 100644 console/src/views/NotFound.vue create mode 100644 console/src/views/Overview.vue create mode 100644 console/src/views/Resources.vue create mode 100644 console/src/views/Sessions.vue create mode 100644 console/src/views/Settings.vue create mode 100644 console/src/views/Tasks.vue create mode 100644 console/src/views/Terminals.vue create mode 100644 console/src/views/Users.vue create mode 100644 console/tsconfig.json create mode 100644 console/vite.config.ts create mode 100644 console/vitest.config.ts delete mode 100644 deploy/.gitkeep create mode 100644 deploy/compose.dev.yml create mode 100644 deploy/compose.live.yml create mode 100644 deploy/nginx.conf create mode 100644 deploy/nginx.live.conf create mode 100644 docs/openapi/platform.v1.json create mode 100644 platform/.env.example delete mode 100644 platform/.gitkeep create mode 100644 platform/Dockerfile create mode 100644 platform/README.md create mode 100644 platform/cmd/api-gateway/main.go create mode 100644 platform/go.mod create mode 100644 platform/go.sum create mode 100644 platform/internal/config/config.go create mode 100644 platform/internal/model/model.go create mode 100644 platform/internal/server/admin.go create mode 100644 platform/internal/server/server.go create mode 100644 platform/internal/server/server_test.go create mode 100644 platform/internal/server/terminal.go create mode 100644 platform/internal/store/schema.sql create mode 100644 platform/internal/store/store.go create mode 100644 platform/pkg/proxmox/client.go create mode 100644 platform/pkg/proxmox/client_test.go create mode 100644 tools/generate-platform-openapi.mjs create mode 100644 tools/platform-terminal-simulator.py create mode 100644 tools/prepare-live-config.py create mode 100644 tools/provision-pve-access.py create mode 100644 tools/start-cloud-dev.ps1 create mode 100644 tools/start-cloud-live.ps1 diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..03774f2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +.git +**/node_modules +**/dist +**/test-results +**/playwright-report +**/.env +**/.env.local +**/*.exe +terminal +docs +tools +.local diff --git a/.gitignore b/.gitignore index b08eb11..4437378 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,19 @@ CMakeCache.txt *.tmp .zcode/ __pycache__/ +*.py[cod] +~$* test-output/ screenshots/ .DS_Store .playwright-cli/ +node_modules/ +dist/ +.env +.env.local +.env.*.local +playwright-report/ +coverage/ +test-results/ +.local/ +*.exe diff --git a/console/.env.example b/console/.env.example new file mode 100644 index 0000000..7006ded --- /dev/null +++ b/console/.env.example @@ -0,0 +1,2 @@ +# api: connect the Go backend; mock: browser-only demo, no real operations. +VITE_API_MODE=api diff --git a/console/.gitkeep b/console/.gitkeep deleted file mode 100644 index 728945f..0000000 --- a/console/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -# Keep the monorepo directory structure in Git. diff --git a/console/Dockerfile b/console/Dockerfile new file mode 100644 index 0000000..61114d8 --- /dev/null +++ b/console/Dockerfile @@ -0,0 +1,13 @@ +FROM node:24-alpine AS build +WORKDIR /src +COPY console/package.json console/package-lock.json ./ +RUN npm ci +COPY console/ ./ +ARG VITE_DEMO_HINT=false +ENV VITE_API_MODE=api VITE_DEMO_HINT=$VITE_DEMO_HINT +RUN npm run build + +FROM nginx:alpine +COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /src/dist /usr/share/nginx/html +EXPOSE 80 diff --git a/console/README.md b/console/README.md new file mode 100644 index 0000000..b884e24 --- /dev/null +++ b/console/README.md @@ -0,0 +1,21 @@ +# VDI Web 管理控制台 + +Vue 3、TypeScript、Vite、Element Plus、Pinia、Vue Router。使用者是管理员,与盒子上的 Qt 桌面选择页分开。 + +运行、演示账号和完整能力边界见[开发说明](../docs/cloud-platform-development.md)。 + +后台样式已按仓库 `ui/` 设计稿接入,支持深浅主题、桌面卡片与列表、远程会话页和窄屏导航;详见 [UI 接入记录](../docs/cloud-console-ui-20261109.md)。真实现场入口为 `http://127.0.0.1:5182`。 + +```text +src/api/ 从 OpenAPI 生成的类型与统一 HTTP 适配 +src/stores/ 会话与平台数据 +src/router/ 登录与角色路由守卫 +src/layouts/ 控制台导航与定时刷新 +src/views/ 第一阶段功能页面 +src/components/ 共用状态等组件 +src/styles/ UI 可替换的设计 token +src/mocks/ 开发专用 MSW 模拟接口 +e2e/ Playwright 主路径验收 +``` + +`npm run api:generate` 更新接口契约与类型;API 适配使用 openapi-fetch,页面不直接发网络请求。默认 `npm run dev` 通过 `/api` 代理到本机 `8088` 端口。开发时设 `VITE_API_MODE=mock` 则使用浏览器模拟接口,生产构建不会启用模拟 Worker。 diff --git a/console/e2e/console.spec.ts b/console/e2e/console.spec.ts new file mode 100644 index 0000000..6b2a51c --- /dev/null +++ b/console/e2e/console.spec.ts @@ -0,0 +1,102 @@ +import { test, expect, type Page } from '@playwright/test' +async function login(page: Page, username = 'admin') { + await page.goto('/login') + await page.getByRole('textbox', { name: '用户名', exact: true }).fill(username) + await page.getByRole('textbox', { name: '密码', exact: true }).fill('Demo@2026') + await page.getByRole('button', { name: '登录', exact: true }).click() + await expect(page.getByRole('heading', { name: '运行总览' })).toBeVisible() +} +test.beforeAll(async ({ request }) => { const r = await request.get('/healthz'); expect(r.ok()).toBe(true); expect((await r.json()).mode).toBe('demo') }) +test('登录失败、登录恢复与退出', async ({ page }) => { + await page.goto('/login') + await page.getByRole('textbox', { name: '用户名', exact: true }).fill('admin') + await page.getByRole('textbox', { name: '密码', exact: true }).fill('incorrect') + await page.getByRole('button', { name: '登录', exact: true }).click() + await expect(page.getByText('用户名或密码错误')).toBeVisible() + await page.getByRole('textbox', { name: '密码', exact: true }).fill('Demo@2026') + await page.getByRole('button', { name: '登录', exact: true }).click() + await expect(page.getByRole('heading', { name: '运行总览' })).toBeVisible() + await page.reload() + await expect(page.getByRole('heading', { name: '运行总览' })).toBeVisible() + await page.getByRole('button', { name: '退出登录' }).click() + await expect(page).toHaveURL(/\/login/) +}) +test('桌面命令与最终任务结果', async ({ page }) => { + await login(page) + await page.getByRole('navigation').getByRole('link', { name: '桌面管理' }).click() + await page.getByRole('button', { name: '列表视图' }).click() + const row = page.getByRole('row').filter({ hasText: '国产办公桌面' }) + await expect(row).toBeVisible() + const currentStopped = await row.getByText('已关机', { exact: true }).count() + const action = currentStopped ? '开机' : '关机' + await row.getByRole('button', { name: action, exact: true }).click() + await page.getByRole('button', { name: '确认提交', exact: true }).click() + await expect(page.getByText('任务已提交,请在任务与审计中查看最终结果')).toBeVisible() + await page.getByRole('navigation').getByRole('link', { name: '任务与审计' }).click() + const task = page.getByRole('row').filter({ hasText: '国产办公桌面' }).first() + await expect(task.getByText('成功', { exact: true })).toBeVisible() + await expect(task).toContainText('演示计算任务已完成') +}) +test('创建用户、编辑多个桌面授权并持久化', async ({ page }) => { + await login(page) + await page.getByRole('navigation').getByRole('link', { name: '用户与授权' }).click() + await page.getByRole('button', { name: '新建用户', exact: true }).click() + const dialog = page.getByRole('dialog') + const name = `测试用户 ${Date.now()}` + await dialog.getByRole('textbox', { name: /姓名/ }).fill(name) + await dialog.getByRole('textbox', { name: /用户账号/ }).fill(`e2e.${Date.now()}`) + await dialog.getByRole('button', { name: '创建用户', exact: true }).click() + await page.getByRole('textbox', { name: '搜索用户' }).fill(name) + await page.getByRole('row').filter({ hasText: name }).getByRole('button', { name: '编辑授权' }).click() + await dialog.locator('label.el-checkbox').filter({ hasText: '设计专属桌面' }).click() + await expect(dialog.getByRole('checkbox', { name: /设计专属桌面/ })).toBeChecked() + await dialog.locator('label.el-checkbox').filter({ hasText: '研发办公桌面' }).click() + await dialog.getByRole('button', { name: '保存授权' }).click() + const row = page.getByRole('row').filter({ hasText: name }) + await expect(row).toContainText('设计专属桌面') + await expect(row).toContainText('研发办公桌面') + await page.reload() + await page.getByRole('textbox', { name: '搜索用户' }).fill(name) + await expect(page.getByRole('row').filter({ hasText: name })).toContainText('设计专属桌面') +}) +test('登记终端只显示一次激活密钥', async ({ page }) => { + await login(page) + await page.getByRole('navigation').getByRole('link', { name: '终端管理' }).click() + await page.getByRole('button', { name: '登记终端' }).click() + const dialog = page.getByRole('dialog') + const serial = `E2E-${Date.now()}` + await dialog.getByRole('textbox', { name: '设备 SN' }).fill(serial) + await dialog.getByRole('textbox', { name: '终端名称' }).fill('联调终端') + await dialog.getByRole('button', { name: '登记并生成激活信息' }).click() + await expect(page.getByTestId('activation-secret')).toContainText('activate_') + await page.getByRole('button', { name: '已保存,关闭' }).click() + await page.getByRole('textbox', { name: '搜索终端' }).fill(serial) + await expect(page.getByRole('row').filter({ hasText: serial })).toContainText('待激活') +}) +for (const username of ['viewer', 'operator']) test(`${username} 权限边界`, async ({ page }) => { + await login(page, username) + await page.getByRole('navigation').getByRole('link', { name: '用户与授权' }).click() + await expect(page.getByRole('button', { name: '新建用户' })).toHaveCount(0) + await expect(page.getByRole('button', { name: '编辑授权' })).toHaveCount(0) + await page.goto('/settings') + await expect(page.getByText('无访问权限', { exact: true })).toBeVisible() + await page.goto('/desktops') + await expect(page.getByRole('heading', { name: '桌面管理' })).toBeVisible() + await page.getByRole('button', { name: '列表视图' }).click() + if (username === 'viewer') await expect(page.getByRole('button', { name: '关机', exact: true })).toHaveCount(0) + else await expect(page.getByRole('button', { name: '关机', exact: true }).first()).toBeVisible() +}) +test('空列表、刷新异常及 401 处理', async ({ page }) => { + await login(page) + await page.getByRole('navigation').getByRole('link', { name: '桌面管理' }).click() + await page.getByRole('textbox', { name: '搜索桌面' }).fill('不存在的桌面') + await expect(page.getByText('暂无符合条件的桌面')).toBeVisible() + await page.route('**/api/v1/overview', route => route.fulfill({ status: 503, json: { code: 'unavailable', message: '测试服务暂不可用', requestId: 'test-offline' } })) + await page.getByRole('navigation').getByRole('link', { name: '云桌面总览', exact: true }).click() + await page.getByRole('button', { name: '刷新数据' }).click() + await expect(page.getByText('数据刷新失败,当前信息可能已过期')).toBeVisible() + await page.unroute('**/api/v1/overview') + await page.evaluate(() => sessionStorage.setItem('vdi.accessToken', 'invalid-token')) + await page.reload() + await expect(page).toHaveURL(/\/login/) +}) diff --git a/console/e2e/live.spec.ts b/console/e2e/live.spec.ts new file mode 100644 index 0000000..fa25e6f --- /dev/null +++ b/console/e2e/live.spec.ts @@ -0,0 +1,40 @@ +import {test,expect} from '@playwright/test' +import {readFileSync,mkdirSync} from 'node:fs' +const env=Object.fromEntries(readFileSync('../.local/live/.env','utf8').trim().split(/\r?\n/).map(line=>{ const i=line.indexOf('='); return [line.slice(0,i),line.slice(i+1)] })) +function required(key: string) { const value=env[key]; if (!value) throw new Error(`Missing live configuration: ${key}`); return value } +test('现场真实资源、设备在线、日志采集与下载',async({page,request})=>{ + const health=await request.get('/healthz'); expect((await health.json()).mode).toBe('production') + await page.goto('/login') + await expect(page.getByText(/演示账号/)).toHaveCount(0) + await page.getByRole('textbox',{name:'用户名',exact:true}).fill(required('VDI_ADMIN_USERNAME')) + await page.getByRole('textbox',{name:'密码',exact:true}).fill(required('VDI_ADMIN_PASSWORD')) + await page.getByRole('button',{name:'登录',exact:true}).click() + await expect(page.getByRole('heading',{name:'运行总览'})).toBeVisible() + await expect(page.getByText('starhe',{exact:true})).toBeVisible() + mkdirSync('../.local/live/screenshots',{recursive:true}) + await page.screenshot({path:'../.local/live/screenshots/overview.png',fullPage:true,animations:'disabled'}) + await page.getByRole('navigation').getByRole('link',{name:'桌面管理'}).click() + await expect(page.getByRole('heading',{name:'桌面管理',exact:true})).toBeVisible() + await page.getByRole('button',{name:'列表视图'}).click() + for(const name of ['Windows 10','Ubuntu','麒麟']) await expect(page.getByRole('row').filter({hasText:name})).toBeVisible() + await page.screenshot({path:'../.local/live/screenshots/desktops.png',fullPage:true,animations:'disabled'}) + await page.getByRole('navigation').getByRole('link',{name:'终端管理'}).click() + const row=page.getByRole('row').filter({hasText:'ee621cae7199a2e9'}) + await expect(row.getByText('在线',{exact:true})).toBeVisible() + await row.getByRole('button',{name:'详情',exact:true}).click() + await expect(page.getByText('qt-platform/0.2.0',{exact:true})).toBeVisible() + await page.getByRole('button',{name:'收集日志',exact:true}).click() + await page.getByRole('button',{name:'确认提交',exact:true}).click() + await page.getByRole('dialog',{name:'终端详情'}).locator('button.el-drawer__close-btn').click() + await page.getByRole('navigation').getByRole('link',{name:'任务与审计'}).click() + const task=page.getByRole('row').filter({hasText:'收集日志'}).first() + await expect(task.getByText('成功',{exact:true})).toBeVisible() + await task.getByRole('button').click() + const download=page.waitForEvent('download') + await page.getByRole('button',{name:'下载诊断日志'}).click() + const artifact=await download + const path='../.local/live/device-diagnostics.json'; await artifact.saveAs(path) + const diagnostic=JSON.parse(readFileSync(path,'utf8')) + expect(diagnostic.agentVersion).toBe('qt-platform/0.2.0'); expect(diagnostic.bootId).toBeTruthy(); expect(diagnostic.uptimeSeconds).toBeGreaterThan(0) + await page.screenshot({path:'../.local/live/screenshots/diagnostics.png',fullPage:true,animations:'disabled'}) +}) diff --git a/console/e2e/ui.spec.ts b/console/e2e/ui.spec.ts new file mode 100644 index 0000000..2b1526f --- /dev/null +++ b/console/e2e/ui.spec.ts @@ -0,0 +1,84 @@ +import { test, expect, type Page } from '@playwright/test' + +// Browser fixtures exercise UI states without sending commands to PVE or a terminal. +async function fixtures(page: Page, role = 'super_admin', state = 'connected') { + const now = new Date().toISOString() + const desktop = { id: 'ui-desk', name: '界面测试桌面', os: 'Ubuntu', node: 'ui-node', vmid: 9000, pool: '', powerState: 'running', serviceState: 'unknown', provider: 'proxmox', endpoint: 'desktop.example', app: 'Desktop', width: 2560, height: 1440, fps: 45, updatedAt: now } + const terminal = { id: 'ui-terminal', sn: 'UI-TEST-DEVICE', name: '界面测试终端', group: '测试组', status: 'online', agentVersion: 'ui-fixture', lastSeen: now, sessionDesktopId: desktop.id, sessionState: state, configVersion: 1, fps: null, rtt: null } + const commands: unknown[] = [] + await page.addInitScript(() => sessionStorage.setItem('vdi.accessToken', 'ui-fixture-token')) + await page.route('**/api/v1/**', async route => { + const request = route.request(), endpoint = new URL(request.url()).pathname.replace('/api/v1', '') + if (request.method() === 'POST' && endpoint.endsWith('/commands')) { + commands.push(request.postDataJSON()) + await route.fulfill({ status: 202, json: { id: 'ui-command', kind: 'terminal', action: 'reset-session', targetId: terminal.id, targetName: terminal.name, actor: 'ui-admin', status: 'queued', message: '等待回执', providerRef: '', createdAt: now, updatedAt: now } }) + return + } + if (endpoint === '/auth/me') { await route.fulfill({ json: { id: 'ui-admin', username: 'ui-admin', displayName: '界面测试管理员', role } }); return } + if (endpoint === '/settings') { await route.fulfill({ json: { mode: 'production', computeProvider: 'proxmox', pveConfigured: true, terminalTransport: 'https-poll', capabilities: [] } }); return } + if (endpoint === '/overview') { await route.fulfill({ json: { terminalCount: 1, onlineTerminals: 1, desktopCount: 1, runningDesktops: 1, activeSessions: state === 'connected' ? 1 : 0, failedTasks: 0, mode: 'production' } }); return } + const lists: Record = { + '/desktops': [desktop], '/terminals': [terminal], '/users': [{ id: 'ui-user', username: 'ui-user', displayName: '测试使用者', enabled: true, terminalId: terminal.id, desktopIds: [desktop.id] }], + '/resources': [{ id: 'ui-node', name: 'ui-node', status: 'online', cpu: .2, memory: .4, storage: .3, provider: 'proxmox', updatedAt: now }], '/tasks': [], '/audit-logs': [], + } + if (endpoint in lists) { const items = lists[endpoint] || []; await route.fulfill({ json: { items, page: 1, pageSize: 100, total: items.length, requestId: 'ui-fixture' } }); return } + await route.fulfill({ status: 404, json: { code: 'fixture_not_found', message: '测试接口未定义', requestId: 'ui-fixture' } }) + }) + return commands +} + +test('远程会话保留缺失指标,取消断开不发命令,确认后使用现有接口', async ({ page }, testInfo) => { + const commands = await fixtures(page) + await page.setViewportSize({ width: 1440, height: 1000 }) + await page.goto('/sessions') + await expect(page.getByRole('heading', { name: '会话信息', exact: true })).toBeVisible() + await expect(page.getByText('桌面正在终端显示')).toBeVisible() + await expect(page.getByText('暂无数据', { exact: true })).toHaveCount(2) + await expect(page.getByText('尚未采集', { exact: true })).toBeVisible() + await page.screenshot({ path: testInfo.outputPath('session-fixture-dark.png'), fullPage: true, animations: 'disabled' }) + await page.getByRole('button', { name: '切换浅色主题' }).click() + await page.screenshot({ path: testInfo.outputPath('session-fixture-light.png'), fullPage: true, animations: 'disabled' }) + await page.getByRole('button', { name: '断开连接', exact: true }).click() + await page.getByRole('button', { name: '取消', exact: true }).click() + expect(commands).toHaveLength(0) + await page.getByRole('button', { name: '断开连接', exact: true }).click() + await page.getByRole('button', { name: '确认提交', exact: true }).click() + await expect(page.getByText('断开命令已提交,等待终端回执')).toBeVisible() + expect(commands).toEqual([{ action: 'reset-session' }]) +}) + +test('只读角色可查看会话,没有断开按钮与系统设置入口', async ({ page }) => { + await fixtures(page, 'viewer') + await page.goto('/sessions') + await expect(page.getByRole('heading', { name: '会话信息', exact: true })).toBeVisible() + await expect(page.getByRole('button', { name: '断开连接', exact: true })).toHaveCount(0) + await expect(page.getByRole('navigation', { name: '主导航' }).getByRole('link', { name: '系统设置' })).toHaveCount(0) + await page.goto('/settings') + await expect(page.getByText('无访问权限', { exact: true })).toBeVisible() +}) + +test('连接预约不显示为已连接,未上报指标保持为空', async ({ page }) => { + await fixtures(page, 'operator', 'connecting') + await page.goto('/sessions') + await expect(page.getByText('终端正在建立连接')).toBeVisible() + await expect(page.getByText('0 个已连接', { exact: true })).toBeVisible() + await expect(page.getByText('1 个连接中', { exact: true })).toBeVisible() + await expect(page.getByText('暂无数据', { exact: true })).toHaveCount(2) +}) + +test('深浅主题跨页面和刷新保留,窄屏导航可用', async ({ page }) => { + await fixtures(page) + await page.setViewportSize({ width: 390, height: 844 }) + await page.goto('/overview') + await expect(page.getByRole('heading', { name: '运行总览' })).toBeVisible() + await page.getByRole('button', { name: '切换浅色主题' }).click() + await expect(page.locator('html')).toHaveAttribute('data-theme', 'light') + await page.getByRole('button', { name: '打开导航' }).click() + await page.getByRole('navigation', { name: '主导航' }).getByRole('link', { name: '桌面管理' }).click() + await expect(page.getByRole('heading', { name: '桌面管理', exact: true })).toBeVisible() + await expect(page.locator('.sidebar')).not.toHaveClass(/is-open/) + await page.reload() + await expect(page.locator('html')).toHaveAttribute('data-theme', 'light') + await expect(page.locator('.desktop-card')).toHaveCount(1) + expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(391) +}) diff --git a/console/eslint.config.js b/console/eslint.config.js new file mode 100644 index 0000000..5e8b9f8 --- /dev/null +++ b/console/eslint.config.js @@ -0,0 +1,11 @@ +import js from '@eslint/js' +import ts from 'typescript-eslint' +import vue from 'eslint-plugin-vue' +import globals from 'globals' +export default ts.config( + { ignores: ['dist/**', 'public/mockServiceWorker.js', 'playwright-report/**', 'test-results/**', 'src/api/schema.d.ts'] }, + js.configs.recommended, + ...ts.configs.recommended, + ...vue.configs['flat/recommended'], + { languageOptions: { globals: { ...globals.browser, ...globals.node }, parserOptions: { parser: ts.parser } }, rules: { 'vue/multi-word-component-names': 'off', 'vue/max-attributes-per-line': 'off', 'vue/html-self-closing': 'off', 'vue/singleline-html-element-content-newline': 'off', 'vue/html-indent': 'off', 'vue/html-closing-bracket-newline': 'off', 'vue/first-attribute-linebreak': 'off', 'vue/attributes-order': 'off' } }, +) diff --git a/console/index.html b/console/index.html new file mode 100644 index 0000000..bfe2c38 --- /dev/null +++ b/console/index.html @@ -0,0 +1,2 @@ + +VDI 云桌面管理平台
diff --git a/console/package-lock.json b/console/package-lock.json new file mode 100644 index 0000000..3ac91e7 --- /dev/null +++ b/console/package-lock.json @@ -0,0 +1,4690 @@ +{ + "name": "vdi-console", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vdi-console", + "version": "0.1.0", + "dependencies": { + "@element-plus/icons-vue": "^2.3.2", + "element-plus": "^2.14.8", + "openapi-fetch": "^0.17.0", + "pinia": "^3.0.3", + "vue": "^3.5.45", + "vue-router": "^4.6.3" + }, + "devDependencies": { + "@eslint/js": "^9.39.0", + "@playwright/test": "^1.58.2", + "@types/node": "^24.10.0", + "@vitejs/plugin-vue": "^6.0.10", + "@vue/tsconfig": "^0.8.1", + "eslint": "^9.39.0", + "eslint-plugin-vue": "^10.5.1", + "globals": "^16.5.0", + "msw": "^2.12.7", + "openapi-typescript": "^7.13.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.3", + "vite": "^7.2.2", + "vitest": "^5.1.0", + "vue-tsc": "^3.1.4" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.13", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.13.tgz", + "integrity": "sha512-fWegr+WsEsiMVW4KUNdIIzzMTZSzNiqhy0//6+2MRi2d5NFNsL/AYwBQW8eBnRbTBVRnDVzjaJEUqKaCWjMMdQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.13", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.13.tgz", + "integrity": "sha512-fJM50YYjlTiBhgdkGmC1tBpv+FMAyo4annf2X6Aks+0yRlaiVkYW/3f07xAfVVkYIoZ9fFZp+mEThUFnuybhkQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.11" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.11", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.11.tgz", + "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.11", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-4.3.0.tgz", + "integrity": "sha512-6V0+8vw/GmH92URQ/Ki12FPhoB3nqaQUJE+5xgbFbB5ok0OXYK5jCQsQQoyR3BzCddHsCpyCFRnIcI1PSOVYkQ==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.3", + "resolved": "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.12.3.tgz", + "integrity": "sha512-R76UKbvsqI68DJJVTVyqFKqU98CN4+B+acxNfEA2IFarSzP0SjtKf/3+fFiXzTyv5Fj3PxLkGHe2wnVWWMxobQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmmirror.com/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmmirror.com/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmmirror.com/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.7", + "resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-3.3.7.tgz", + "integrity": "sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.2", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmmirror.com/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmmirror.com/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmmirror.com/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmmirror.com/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@inquirer/ansi": { + "version": "2.0.10", + "resolved": "https://registry.npmmirror.com/@inquirer/ansi/-/ansi-2.0.10.tgz", + "integrity": "sha512-dwrc+1aqVZpHNkNDswXGtn1typ+Pxpo5FHZRyBw1le7onEJUkPUkYOY1FIFC1LvFMYUqqLuWBwElhAmencL7MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.3.5", + "resolved": "https://registry.npmmirror.com/@inquirer/confirm/-/confirm-6.3.5.tgz", + "integrity": "sha512-BJ59BOyURiZiAxdr9AuRu38/k+rV+DcTIM2fUF/c/BXrab6H8zMZTTOl3LgkSJH96Xob2vOJxDiBkHiz32Xx2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.6", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "12.0.6", + "resolved": "https://registry.npmmirror.com/@inquirer/core/-/core-12.0.6.tgz", + "integrity": "sha512-5yxn2LsJ8Tu4ei2a+yNkmFRhM/cb+JMEsEs/AW6CCOnBnpMy4IjYiPMFyHtYq8weOmlTyqqCx40y0DsD7OsEPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.10", + "@inquirer/figures": "^2.0.10", + "@inquirer/type": "4.2.1", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.10", + "resolved": "https://registry.npmmirror.com/@inquirer/figures/-/figures-2.0.10.tgz", + "integrity": "sha512-51elrUJV3jZpvD0ygz8+U10Kc6+Lzqln+/tYmnbhy6jo5oXpE6hMCcv+OTBC0enwJXsb3JIuT3VtEinRNCSK2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/type": { + "version": "4.2.1", + "resolved": "https://registry.npmmirror.com/@inquirer/type/-/type-4.2.1.tgz", + "integrity": "sha512-lDkPsnv5M5CcYB2T1qTc3dobNTPkVr4DU3Ic2Q6SW8/qVRHgvzdRdmVZ6YqG9Ag1aJTeMsLU0s0WPRh3CZWDQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/nonrecursive-glob-match": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@isaacs/nonrecursive-glob-match/-/nonrecursive-glob-match-1.1.0.tgz", + "integrity": "sha512-GPm/Dbvk5RLmhRnxXIa7nTpK0xtl4E0wj0MslsuNiJ1FIROU+2+9lgUC/dgF7Ey4QCWhfEsgxilMRMCdHPfDfw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mswjs/interceptors": { + "version": "0.41.9", + "resolved": "https://registry.npmmirror.com/@mswjs/interceptors/-/interceptors-0.41.9.tgz", + "integrity": "sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@mswjs/interceptors/node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@open-draft/deferred-promise": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/@open-draft/deferred-promise/-/deferred-promise-3.0.0.tgz", + "integrity": "sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@playwright/test": { + "version": "1.65.0", + "resolved": "https://registry.npmmirror.com/@playwright/test/-/test-1.65.0.tgz", + "integrity": "sha512-iGURLGuKDLakSEl7Pgfv8h7f+XLT+V861Wfhz0zlEl5nyFuKUopdkFBPKqTllTUp06aOwPuH8iuUEb3yp3MpbQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.65.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.8", + "resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz", + "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@redocly/ajv": { + "version": "8.11.2", + "resolved": "https://registry.npmmirror.com/@redocly/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js-replace": "^1.0.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@redocly/ajv/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/config": { + "version": "0.22.0", + "resolved": "https://registry.npmmirror.com/@redocly/config/-/config-0.22.0.tgz", + "integrity": "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/openapi-core": { + "version": "1.34.24", + "resolved": "https://registry.npmmirror.com/@redocly/openapi-core/-/openapi-core-1.34.24.tgz", + "integrity": "sha512-jDFMu4zirFVOF9nEWCT31s7OzzVhkxZkb4lO3yZhkdPXAOspLo10ED9thVgm6gtI0dJBTLa+mNP0IYeZpVSYGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/ajv": "8.11.2", + "@redocly/config": "0.22.0", + "colorette": "1.4.0", + "https-proxy-agent": "7.0.6", + "js-levenshtein": "1.1.6", + "js-yaml": "4.3.2", + "minimatch": "5.1.9", + "pluralize": "8.0.0", + "yaml-ast-parser": "0.0.43" + }, + "engines": { + "node": ">=18.17.0", + "npm": ">=9.5.0" + } + }, + "node_modules/@redocly/openapi-core/node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@redocly/openapi-core/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.1.0.tgz", + "integrity": "sha512-K+NU2HDCEem4sClgHH4ZZXwdJeNFkJo+hRk479DIwdIviAPN2hFJH1KHHwg8CxHQXvi0M5XY4deTXINfIiSG6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.68.1.tgz", + "integrity": "sha512-67xgK/taQ7w18anfSxH4dy3RVwiu7GFT8aZglHVUnYisqIe4Qf1xQW7hWxe3lB7R5XSVfPJJKdKP31eGMID2zw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.68.1.tgz", + "integrity": "sha512-nGmSABuQDWUGPgGOy3VJygaIQl9/2SMPEwwR2+W+uTxit+MoZcve1y2ebaO9Dhg/MZXMeeoYL+HPGq8X5LTzgg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-x64": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-x64/-/rollup-android-x64-4.68.1.tgz", + "integrity": "sha512-Es4xgvucd6umxthid6WojNNYHLsfB6oySkLypUn4VXxR7IuK2TS2sdPB6+3U/mw/uU7ahQk9aw7fDUI6y2GkKA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.68.1.tgz", + "integrity": "sha512-biPWTUPYDWejnysuD4A1kJ4mjT121duL6WJ8JJ0lyFWuLSnC91WF6C/X63CSz7z8yJIQEXixlHOpPK/VzPQ3Gw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.68.1.tgz", + "integrity": "sha512-EhxsI1TfSp3Lvyc9W6Y4o9W/zR43zaUgDsENK3hCix/OP/jrGc1CAxNhgmA28SJTsODpAv9ZBYYgP1rO27dfRA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.68.1.tgz", + "integrity": "sha512-p6s+MBrmkj41sOH4no/z1g/5E446H9//kUblCvZY/uy+NWijHT+0/F6sAcRqI3l3RWLyeW6kHl0IqZ0+p3VjrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.68.1.tgz", + "integrity": "sha512-sm2K6aFA1tn+5gH59LLsm3SbNJ736cXOZ3IcpmxsUxuAEn6h04LFKWk3ji/Jjw1IEDdZ8QhgdxLPzGHnnOD4fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.68.1.tgz", + "integrity": "sha512-kirHmtPRCh5dVC6v8/9i/ldS60r6TYLiGunHCXpba/4ws1SPyvdW4PYkRLOxpAfYC+A9r2Km2zOLpm4cMty1Rg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.68.1.tgz", + "integrity": "sha512-TR1ec0+aZylxhD9/4UkelsSiCQ5EhtHjzty0LXKiMwwyWa4rgF55bIGExYqxp/+BwCH4GEV8F/EZB9JYwLDTdw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.68.1.tgz", + "integrity": "sha512-dxsv4BC45+TGKImva8fIgVdvuJ1/o2Bov+sGjPX/RFcDp/UtXNJ7K5/6UTS0fbAw0P7kTIvH5M0h6ViS5CP9xQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.68.1.tgz", + "integrity": "sha512-/ATJPUYJUdlm1/Zil0IyU9x14zyBXicfoOvNbvHAUH4c8IjU5xNBCtnDteTD+xd08Ym6fdGVrdZLLThYRvsZOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.68.1.tgz", + "integrity": "sha512-Bp17x/tj1Fi45pNE4A654jkKAiSUgB6UjQuPltwT+e2jfYw7TfaYzQNGgbzx0MNH27eYQDNVUNxGk9m9L/xUaA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.68.1.tgz", + "integrity": "sha512-QvECQp0CyvIhiAv9mn1ouYZfIcDBIRdZek7M/ZYqRtiWzpiKAPu2gnGD13yNtI0KykSPk41szQitphAbHfHdvQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.68.1.tgz", + "integrity": "sha512-+ACzFhkXUiYvXLebVBb4ge8gr7EKM/xGDW+J8cmnwv8SSlYyoKXD8Es3OpOPFnHLV5vevmrqlQgCP/qQxr69JQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.68.1.tgz", + "integrity": "sha512-/wMEhoQ0Ithx4CrmhMogO57CKyt0LRB+2A7i97v+GjBDGLh+uhTNNSlIm1NixaTEwMXyiIIZ3bXVkSh4jfAhKg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.68.1.tgz", + "integrity": "sha512-N92PYV7Ulw6pOS16eE32Q/ykWSj2ly+ojYX/IHCsD9+FIj7n10qjeiPhdht7r8UTnCpYdmrGf7VeNonovOvuQQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.68.1.tgz", + "integrity": "sha512-ab332+PwM7/sgbqRlE0BYb2eGNme8Rs1T/2Ob9mYSwrQqK7a1zawpLVYrXtnnRBPQn4TNGGtwaBy6toClPU7aQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.68.1.tgz", + "integrity": "sha512-zaQ2AHQP401CBAa+qhWLLTGU78MQ6nsB0RfWYbw5+AUWNLjp9juAokzZbjtyiXf7CBe8JfBNV7g2lNwbxmLKFg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.68.1.tgz", + "integrity": "sha512-J2KJofHPtGP7AZQUbRfylo7f1ZKdSjY2XG5VXjDwh+Mu2oM/bJN53mDWSjBGzNU3AGaXBqIxIhEAa0tSHQdO6w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.68.1.tgz", + "integrity": "sha512-WMXzHCmeAhFpAwP2w3ugGucdDp5jYJm8fwLO1e9uUtUQXhceWGfobu1E+seQ/zSixxGB+foAX8cs3iiebF+QjA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.68.1.tgz", + "integrity": "sha512-PZadv8s8G/omPyl31lvtraKxlM//Ad0P32kLBBHGjImR31vCXZkiDNqnIQCZ6gXTZ5xN5GkTGhzZdJzMz3EOnw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.68.1.tgz", + "integrity": "sha512-eXI1iou+f8g5iJFGnF9koInoopknJdulR51phO8ggVNGyTT2Ya68M7scDLYpIOTWaTN9cDPxRJ1o0d6ce/P2cQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.68.1.tgz", + "integrity": "sha512-la8czh2p2DlfCC+/XgVbxItS01f2/1ljPSy+U/Z1j5sWtlQv+Ammx04gP2w5MZWsGuKD/r85cTWudlASulQMEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.68.1.tgz", + "integrity": "sha512-WMc++sC1YObbiuITB73IYD6Neejvz0591glNh9kpJj2ZUA3YkwtYzVVXXkADyyGhttdJkJndR/dd4XKKxCHO+A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.68.1.tgz", + "integrity": "sha512-jDDAn+OoI7R63WRCsJDMLa9v4Qii/zTrnstftTy8+1kVZpZBJFSF6K6PDyot51Wh9wTcjh0zUU9dHztVNo1+uw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.68.1.tgz", + "integrity": "sha512-kR8W3osyXy6mB3EaaTsC2FTAdgqZgRsOXXs31aIvC/KjbxeMqDon9l3Xyajo1ynQuS1JHlMhkYyqL10VKA3VZw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmmirror.com/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.10.tgz", + "integrity": "sha512-lTyOMnOcmRBzdCv6/4zUZCrEhV0xrqQiUZBZtVcbmLHBXKLnN8q89v/wKRF/Q0HbwVsqbIG3o5HDoFl9sduM+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.27", + "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.27.tgz", + "integrity": "sha512-VenMRv/eKNrspX9v3H4dbmjsrnCOYdYBG3kxXKk3dG6X+urBGcj2Prww3xw1U5K4FV/mE4h5GbWd0szEKf+9ew==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/node": { + "version": "24.19.2", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-24.19.2.tgz", + "integrity": "sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/set-cookie-parser": { + "version": "2.4.10", + "resolved": "https://registry.npmmirror.com/@types/set-cookie-parser/-/set-cookie-parser-2.4.10.tgz", + "integrity": "sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/statuses": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/@types/statuses/-/statuses-2.0.6.tgz", + "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.72.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.1.tgz", + "integrity": "sha512-5CokTWXpmbylfFabsbvHC0CAw3XLaGnoa+nFJX5FR592h/LoZsENUE6p/rg5xvkKrFEXz6pYmgI29AgXc2N3BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.72.1", + "@typescript-eslint/type-utils": "8.72.1", + "@typescript-eslint/utils": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.72.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.14", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-7.0.14.tgz", + "integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.72.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.72.1.tgz", + "integrity": "sha512-ZGwZP4OCfyA0AQTpEAMWz+rtX01DRhQWW5hB0ucKDEIX5U7jdmPSaMx94rQfaPAQzIYz2BimOgKOJtjfpRFQ9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.72.1", + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/typescript-estree": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.72.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.72.1.tgz", + "integrity": "sha512-fAuVMkwdO/huI8nyo/WKnNKf6Gag93S56Rh3Ts5mGcavwSdaN8tRg1gCeIGX8PF+nzp1BjGfO4gHhGzW1J6jTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.72.1", + "@typescript-eslint/types": "^8.72.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.72.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.72.1.tgz", + "integrity": "sha512-lC/t6eMCZZQAAKZEYGL6RDVSuFehpdmMT6h+nJIjIa6oylN/PwDgDVtEIbB7O9xFfZYtZAYLoLlDNWKJhtF+uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.72.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.1.tgz", + "integrity": "sha512-e4D5R5vpeAEPBxT2BAzsFQL3sbwyeeeo5+aczgNSRK1V734jE7pTqaGFU+fXapWEQwnZbpFaTn32+Z8DwS6OTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.72.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.72.1.tgz", + "integrity": "sha512-iY2wlmyrXJmx4yxUGP3aYCFF3cA/6ShddOKmLU2dx7LnEykbAM8WThlxZEfd1NddqceF6eJmZUayxx9oKIYNlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/typescript-estree": "8.72.1", + "@typescript-eslint/utils": "8.72.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.72.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.72.1.tgz", + "integrity": "sha512-LmE5IhQet1BWOWEXqBrVtBPsMAOQ6Nzk35d+WLh3mk4EUF33sqlfxjio1MdYeSvKSG5skactKKuixBIKXhL4Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.72.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.1.tgz", + "integrity": "sha512-DAlUoUV93hIvrtIQLwCbmZQYVZwOMhiHJrOVUUIWicFZaIYjTRqlY3I/NZevfkLmSqkNHRu2qYDcq9lrn16oOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.72.1", + "@typescript-eslint/tsconfig-utils": "8.72.1", + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "11.0.1", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-11.0.1.tgz", + "integrity": "sha512-t+Vn6DX4wPCxS23y03C0uhF7S1jpi1GfqPrnlEipCJaA2ykiNbMkVlDutZtmvLfRcUN6R8AYVlzTsQrT4RZoLQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/nonrecursive-glob-match": "^1.0.1", + "brace-expansion": "^5.0.12" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.72.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.72.1.tgz", + "integrity": "sha512-5v5liypurJBFHCBNqh/RjsymAD+VWcE0djuGY/BUqp5fC3Qn+luWDEksjuTtWjPn6i5zQlUtkZCRG/CYG0yCkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.72.1", + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/typescript-estree": "8.72.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.72.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.1.tgz", + "integrity": "sha512-SB8xqipSzZHUePCCLNeYxqnxq83X61/qzNhjQGbTfjxGZIeOEP7tBgWYX05Tcd6CgE02WdTJnh5b8kMZJ/+bnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.10", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.10.tgz", + "integrity": "sha512-b80aUQh3n+qEzzZm5oQp3rT2QImeCx58K6eFl/RkJ3d65KW0kTuv5lNsLRVAjbW9YRUpo/wzmvzMgV34bTAItA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25 || ^3.6.0-rc.10" + } + }, + "node_modules/@vitest/mocker": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/@vitest/mocker/-/mocker-5.1.0.tgz", + "integrity": "sha512-5umB9/jlWcqVS6fsrFmv5Gdnpu6wAUio/lEOwAmvB7v+lFo/IW2B9FhAD0p19GCULTgMzLCdUmuCDCnAzWgppQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.1.0", + "estree-walker": "^3.0.3", + "magic-string": "^1.4.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9 || ^3.0.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/magic-string": { + "version": "1.4.5", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-1.4.5.tgz", + "integrity": "sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/@vitest/spy": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/@vitest/spy/-/spy-5.1.0.tgz", + "integrity": "sha512-2DPO5OMHWKrBngC+fCmTQqemNF0gidRhp3lxbBmRhW+M1XJrfgPpqNqWVevXtn2f2ufD5ZaR1B6B641aAlcX5A==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.30", + "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.30.tgz", + "integrity": "sha512-73fjwEV8wat6aOZ6U/y/hqtE30ocrIWECg67b0txGczOMNw+MffLmvJ5fnFfN9drVUEYZzLdaNo6VAy6G6avMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.30" + } + }, + "node_modules/@volar/language-service": { + "version": "2.4.30", + "resolved": "https://registry.npmmirror.com/@volar/language-service/-/language-service-2.4.30.tgz", + "integrity": "sha512-w/xDsASiqC71ggCOm7sTDXDNk/l72XEptodeRC68tgZ0e5QM4hM3zAqBGC7mBctvhrpYwKfMG+t9xcUo2hEFtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.30", + "vscode-languageserver-protocol": "^3.17.5", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "typescript": "<7" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.30", + "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.30.tgz", + "integrity": "sha512-zym1ob/KwDDyN+0jGVtxwbxJSRRnUvv5HspCtz3yEfVVdCG4/dzxJktMGPAgemqRuVs81O0o/y0q3qOUErR8kg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.30", + "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.30.tgz", + "integrity": "sha512-/DgA5tgYFPfrLFuSjIMXMGHk1LQv2+P8eny6+BhGNzFJELWIW7UZlc56ZLIRFhLsPpMfIFxM8/HvOPBggrjTZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.30", + "@volar/language-service": "2.4.30", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "typescript": "<7" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.45", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.45.tgz", + "integrity": "sha512-XGHJrM6ufZARAc/w+tyOswStw9tdKiqCKBScI4gGaNxD+aaGu/HQWgKJ+JItjRLPqenQ7iE7G8AJC7T5ew1p+g==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.12", + "@vue/shared": "3.5.45", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.2" + } + }, + "node_modules/@vue/compiler-core/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.45", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.45.tgz", + "integrity": "sha512-LbP2d19GSOYGPmvMBBCVJVlv7TKtjM1Cfeyu3cYia720pknZP+1cLX7H5MVatvruT6MuJhiPuNbQBW/MDn/EwA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.45", + "@vue/shared": "3.5.45" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.45", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.45.tgz", + "integrity": "sha512-MKFjb2UX0M/l/jhfzqMLrI8VM4pfvFKG3pqFZkSZFHRLtNld/O4RtUPZFWSwhro/14qP88UQEkWzHxAZAQ0tsw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.12", + "@vue/compiler-core": "3.5.45", + "@vue/compiler-dom": "3.5.45", + "@vue/compiler-ssr": "3.5.45", + "@vue/shared": "3.5.45", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.32", + "source-map-js": "^1.2.2" + } + }, + "node_modules/@vue/compiler-sfc/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.45", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.45.tgz", + "integrity": "sha512-RF5sbZOfYz5AK00tOJUn2PwFXR2fS1u2HVzI8D9VF9rGAFxuGBqg2bB1kJ3ZHMprqSltXnT9MZE1IsqjSM8oew==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.45", + "@vue/shared": "3.5.45" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.10", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-7.7.10.tgz", + "integrity": "sha512-KxtEpUOOpFz/qOGRrAwA36QF7DqIA+FXgCYit9mk9wjbaZt0sXOFz81ElOZtKA4HbWHUdwNjZHBFsFFyp5BZiA==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.10" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.10", + "resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-7.7.10.tgz", + "integrity": "sha512-3WNi2Kq4tbpVbmhml7RiphmAt0279oh3fKNeWMQIrltfX8Q91b4i5PL8DtyNKdwmcsGrV4fg+erwWOmD05CLIw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.10", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.10", + "resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-7.7.10.tgz", + "integrity": "sha512-wOPslzB8vTvpxwdaOcR2qAbwmuSP0L+rhpoC6Cf56V3Jip+HWb7PQQXOUPgBNQARpXsbQX/+mvi8kKucmBGRwQ==", + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/language-core": { + "version": "3.3.14", + "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-3.3.14.tgz", + "integrity": "sha512-kRB+3hHg0c+BdD1XLAvwjVMQd5pC05zijSlPDRI3ERvC/s04JlRs5upy2KjeMg/9m9oD5zuMVh9yVvkKfZqoJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.30", + "@vue/compiler-dom": "^3.5.0", + "@vue/shared": "^3.5.0", + "alien-signals": "^3.2.1", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.4" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.45", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.45.tgz", + "integrity": "sha512-WqkyubNWpI4OYBOipSrjIssMdJO8K1i9Zts/Wm8Er10/0msOEwCyWWWvZnAI7WW3kQsTong6DJp8Cbb+vd4cPA==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.45" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.45", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.45.tgz", + "integrity": "sha512-bzIO3+FABjpj85uIKSc+Qnj36/sxDNI3tcH9i6w6xKA1k6e0ejv0G0jMCfeJBvK3yGL5XbJ3DMZj+ClqyCHFYw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.45", + "@vue/shared": "3.5.45" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.45", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.45.tgz", + "integrity": "sha512-ZvRTFMDgfqTDYWvJfoVpQTJx1RN5M6l7Pyd6wfdY2NaXzbPQVh9ezeQYFbNPP6bIcRVn2SqSSR25N/jSW69BFw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.45", + "@vue/runtime-core": "3.5.45", + "@vue/shared": "3.5.45", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.45", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.45.tgz", + "integrity": "sha512-1fNXUHhhsWf4EVL4XixbGBU5mKdiIXJAB82feHRTyPPo6ZUEYWIJuEeUl3mVV6QDBp50HJOX4obLhxYXlrjrzA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.45", + "@vue/runtime-dom": "3.5.45", + "@vue/shared": "3.5.45" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.45", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.45.tgz", + "integrity": "sha512-ue9fgHmVNPlP7iW3+cxoBX/Cnlw53dHlVSw0JKGY6z1lObg8pHoj928M2nzG4/yVsMZ3NaRZ2s2z61uC16NDwQ==", + "license": "MIT" + }, + "node_modules/@vue/tsconfig": { + "version": "0.8.1", + "resolved": "https://registry.npmmirror.com/@vue/tsconfig/-/tsconfig-0.8.1.tgz", + "integrity": "sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": "5.x", + "vue": "^3.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@vueuse/core": { + "version": "15.0.0", + "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-15.0.0.tgz", + "integrity": "sha512-Zv67h5mrk7YHDJsRObx+DXaCBz6RKBUpxqLesJ+3xZNPG6yuQs7ja7kg3veFrrouYLWAqydQ7CwGEbLVxB43Ng==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "15.0.0", + "@vueuse/shared": "15.0.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/metadata": { + "version": "15.0.0", + "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-15.0.0.tgz", + "integrity": "sha512-sfWE5LADAKJ0KnjtGliMdSN8Ij/L7uP6rXIZlgPloAI4zvvefbw6H5Lt7KJA6RqQOYgVqWF5Guj50HHMIzygBg==", + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "15.0.0", + "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-15.0.0.tgz", + "integrity": "sha512-vbNXhGrbV53xPOMSyOzMHQRvhMgZf7oNDw2RChUetVxwTIEkInQ1joZ9JbT0G3z2n2AJNjvmCWeM9ySYrnvogA==", + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/alien-signals": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-3.2.1.tgz", + "integrity": "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmmirror.com/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "6.3.0", + "resolved": "https://registry.npmmirror.com/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmmirror.com/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/copy-anything": { + "version": "4.1.5", + "resolved": "https://registry.npmmirror.com/copy-anything/-/copy-anything-4.1.5.tgz", + "integrity": "sha512-vePst2VLTSqPglW7Q98VwFpGiGpOHTyXIo81MO5VyLecA5zMbZw/P/V1vmwD1jksa2iJpn/YkDrJ3XhTfzUyhQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.23", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.23.tgz", + "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/element-plus": { + "version": "2.14.8", + "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.14.8.tgz", + "integrity": "sha512-Z4Br45W9b8vWKvVHSTvbS6rRKmsLjmHCPXICJR6osl35+i+S2LCBs4NN8apLBkQiUh0pzS9labK5eCS7yzDhWw==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^4.2.2", + "@element-plus/icons-vue": "^2.3.2", + "@floating-ui/dom": "^1.8.0", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.8", + "@types/lodash": "^4.17.24", + "@types/lodash-es": "^4.17.12", + "@vueuse/core": "15.0.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.20", + "lodash": "^4.18.1", + "lodash-es": "^4.18.1", + "lodash-unified": "^1.0.3", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0", + "vue-component-type-helpers": "^3.3.11" + }, + "peerDependencies": { + "vue": "^3.3.7" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmmirror.com/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-vue": { + "version": "10.12.0", + "resolved": "https://registry.npmmirror.com/eslint-plugin-vue/-/eslint-plugin-vue-10.12.0.tgz", + "integrity": "sha512-YsrHHtMaxjWqmxr/Kv9yXUfakUlbTCWtIBvr9eounzj6ZsGJdz1I4UuTIHkhzKHes793Cwo3PGTcvsoBqNyzCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "natural-compare": "^1.4.0", + "nth-check": "^2.1.1", + "postcss-selector-parser": "^7.1.4", + "semver": "^7.8.5", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "@stylistic/eslint-plugin": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", + "@typescript-eslint/parser": "^7.0.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "vue-eslint-parser": "^10.3.0" + }, + "peerDependenciesMeta": { + "@stylistic/eslint-plugin": { + "optional": true + }, + "@typescript-eslint/parser": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmmirror.com/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmmirror.com/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphql": { + "version": "16.15.0", + "resolved": "https://registry.npmmirror.com/graphql/-/graphql-16.15.0.tgz", + "integrity": "sha512-32kz9FecohDiC/tgcViwnRXM7HFBVTPiW7pHO+DikKOI5KjhU/jNULOReXsrFqt2WAYbE9xFXsTffJw6iTO8qw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/headers-polyfill": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/headers-polyfill/-/headers-polyfill-5.0.1.tgz", + "integrity": "sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/set-cookie-parser": "^2.4.10", + "set-cookie-parser": "^3.0.1" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmmirror.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "license": "MIT", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/msw": { + "version": "2.15.0", + "resolved": "https://registry.npmmirror.com/msw/-/msw-2.15.0.tgz", + "integrity": "sha512-2wQAmKkQKxRuXvYJxVhPGG0wZNBQyD06oJvxqw90XqLvptdqxdlHrFUfEteKkpaNORX3Xzc+HtEl/q0nfmN2wQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@inquirer/confirm": "^6.0.11", + "@mswjs/interceptors": "^0.41.3", + "@open-draft/deferred-promise": "^3.0.0", + "@types/statuses": "^2.0.6", + "cookie": "^1.1.1", + "graphql": "^16.13.2", + "headers-polyfill": "^5.0.1", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "path-to-regexp": "^6.3.0", + "picocolors": "^1.1.1", + "rettime": "^0.11.11", + "statuses": "^2.0.2", + "strict-event-emitter": "^0.5.1", + "tough-cookie": "^6.0.1", + "type-fest": "^5.5.0", + "until-async": "^3.0.2", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.8.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", + "license": "BSD-3-Clause" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/obug": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/obug/-/obug-3.0.0.tgz", + "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/openapi-fetch": { + "version": "0.17.0", + "resolved": "https://registry.npmmirror.com/openapi-fetch/-/openapi-fetch-0.17.0.tgz", + "integrity": "sha512-PsbZR1wAPcG91eEthKhN+Zn92FMHxv+/faECIwjXdxfTODGSGegYv0sc1Olz+HYPvKOuoXfp+0pA2XVt2cI0Ig==", + "license": "MIT", + "dependencies": { + "openapi-typescript-helpers": "^0.1.0" + } + }, + "node_modules/openapi-typescript": { + "version": "7.13.0", + "resolved": "https://registry.npmmirror.com/openapi-typescript/-/openapi-typescript-7.13.0.tgz", + "integrity": "sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/openapi-core": "^1.34.6", + "ansi-colors": "^4.1.3", + "change-case": "^5.4.4", + "parse-json": "^8.3.0", + "supports-color": "^10.2.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "openapi-typescript": "bin/cli.js" + }, + "peerDependencies": { + "typescript": "^5.x" + } + }, + "node_modules/openapi-typescript-helpers": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/openapi-typescript-helpers/-/openapi-typescript-helpers-0.1.0.tgz", + "integrity": "sha512-OKTGPthhivLw/fHz6c3OPtg72vi86qaMlqbJuVJ23qOvQ+53uw1n7HdmkJFibloF7QEjDrDkzJiOJuockM/ljw==", + "license": "MIT" + }, + "node_modules/openapi-typescript/node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmmirror.com/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true, + "license": "MIT" + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmmirror.com/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/pinia/-/pinia-3.0.4.tgz", + "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^7.7.7" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.5.0", + "vue": "^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/playwright": { + "version": "1.65.0", + "resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.65.0.tgz", + "integrity": "sha512-F3AMWcR44qMBt4kq+6Kb6mX7bjGXshktKFOPULd7reED6ZJWevjbNRqysP7q+AATE9kdPEZBibex1BYsvpVoeA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.65.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.65.0", + "resolved": "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.65.0.tgz", + "integrity": "sha512-vNSKgH6YqtN7cdT/L334zo0a+YwkrpicyrHJlWWPIm/Q+Nn2SWInil01oT0mbu96U4GgoRtCYQD9CzJQgju+lA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss": { + "version": "8.5.32", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.32.tgz", + "integrity": "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-7.2.0.tgz", + "integrity": "sha512-Khh/tERVBn0I1499dvBSHY0pWJloMJWUxDsOBM0CCDPAcVZ10Jl2L1qy1f19uEX22spa8Mv7DCdEQJc0cGwBhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rettime": { + "version": "0.11.12", + "resolved": "https://registry.npmmirror.com/rettime/-/rettime-0.11.12.tgz", + "integrity": "sha512-5ExtR1ppiDBQkVoPw5NAqdbfH4Iq2VOmQKHMtUoaFxA9bo2krzFYBtyq6K40xxbYp+2w/YOr6D+1ToF6tTN53A==", + "dev": true, + "license": "MIT" + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.68.1.tgz", + "integrity": "sha512-kjVU1iN2BbQeFPFTpmUtl1bR/DbMl2HPmiSLYpx6XLromTaMkidR+mlRMdS/Doovm6uKlwigyxMcHRhRe1vZ5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.10" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.68.1", + "@rollup/rollup-android-arm64": "4.68.1", + "@rollup/rollup-android-x64": "4.68.1", + "@rollup/rollup-darwin-arm64": "4.68.1", + "@rollup/rollup-darwin-x64": "4.68.1", + "@rollup/rollup-freebsd-arm64": "4.68.1", + "@rollup/rollup-freebsd-x64": "4.68.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.68.1", + "@rollup/rollup-linux-arm-musleabihf": "4.68.1", + "@rollup/rollup-linux-arm64-gnu": "4.68.1", + "@rollup/rollup-linux-arm64-musl": "4.68.1", + "@rollup/rollup-linux-loong64-gnu": "4.68.1", + "@rollup/rollup-linux-loong64-musl": "4.68.1", + "@rollup/rollup-linux-ppc64-gnu": "4.68.1", + "@rollup/rollup-linux-ppc64-musl": "4.68.1", + "@rollup/rollup-linux-riscv64-gnu": "4.68.1", + "@rollup/rollup-linux-riscv64-musl": "4.68.1", + "@rollup/rollup-linux-s390x-gnu": "4.68.1", + "@rollup/rollup-linux-x64-gnu": "4.68.1", + "@rollup/rollup-linux-x64-musl": "4.68.1", + "@rollup/rollup-openbsd-x64": "4.68.1", + "@rollup/rollup-openharmony-arm64": "4.68.1", + "@rollup/rollup-win32-arm64-msvc": "4.68.1", + "@rollup/rollup-win32-ia32-msvc": "4.68.1", + "@rollup/rollup-win32-x64-gnu": "4.68.1", + "@rollup/rollup-win32-x64-msvc": "4.68.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "7.8.6", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.6.tgz", + "integrity": "sha512-9lpSSU4Mly3mrtruwLs1eLK6+gzbzqeBFwRNjIjE+9LuDpWdYctrQlQ3ej1Ilgi7gknpsTRnPoLP8DpCHqHIJw==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/set-cookie-parser/-/set-cookie-parser-3.2.2.tgz", + "integrity": "sha512-df017qMeYuXo4emL09nlyMpi2i2MtEIKS/6jtXH3vmjFgIRsOi852/5RmhJ4EkvN3ZF8jXadM/EkTPXgc8yPIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmmirror.com/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", + "dev": true, + "license": "MIT" + }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmmirror.com/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmmirror.com/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tinybench": { + "version": "6.2.3", + "resolved": "https://registry.npmmirror.com/tinybench/-/tinybench-6.2.3.tgz", + "integrity": "sha512-Al01C2JIBnO2CXCAPbpAl6sUiOZHeThGtHYNTa51JRw9PyWOCdr+9Ong05R4qg/Sk3+H645E8ov1h2c58ityNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tldts": { + "version": "7.4.19", + "resolved": "https://registry.npmmirror.com/tldts/-/tldts-7.4.19.tgz", + "integrity": "sha512-48dH6aSpQnpbzDUc4vIQZ1VjB8tkK+e8c1yx05/SZK+rkssgjEOlxDd/yU8/a4vGYQ9vRgJr9dwMq9TJ0b3X2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.19" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.20", + "resolved": "https://registry.npmmirror.com/tldts-core/-/tldts-core-7.4.20.tgz", + "integrity": "sha512-C630TWXZCZmLq0F2bW4QqwnaaxCQ/svpAqvJwnC4JvgEM5ohhBnGsEzTLSOXcN+xjz+kEaHS5uoWIw2klnqd0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-6.1.0.tgz", + "integrity": "sha512-FTxc8u8Kl2tz0Lat1u8ksvcy8bwHRyEMawwTWybo51JqaC6O7SvfuhAKg+Ld6VqyIe2QRJBzY9HA7y2WyFRT2w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "5.10.0", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-5.10.0.tgz", + "integrity": "sha512-NoSdpq/WEiAg5sjmBkmV/hfxv6HJH4NqPNrqjtSO5CwRmpsDfaf4begxW34KdJykH/l1yHtwBWQkCRdoXO8mPA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.72.1", + "resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.72.1.tgz", + "integrity": "sha512-3Li9FfPmQp45nxEtFD1ZCrYFI3cYK5kpyNtmm1H7lMagm8u9sy6PTjy3ytwQZs7KNukOyKFHAZ6z6GjYFeaeXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.72.1", + "@typescript-eslint/parser": "8.72.1", + "@typescript-eslint/typescript-estree": "8.72.1", + "@typescript-eslint/utils": "8.72.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/until-async": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/until-async/-/until-async-3.0.2.tgz", + "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/kettanaito" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uri-js-replace": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/uri-js-replace/-/uri-js-replace-1.0.2.tgz", + "integrity": "sha512-VeBBt/nMxedczht3KDL2yBBMJqIxpCQLMZp3UJ1otcWwUHdQW4IiyHpJivExzSK+FdxTowRfGL4FmwDqH9+S8Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.7", + "resolved": "https://registry.npmmirror.com/vite/-/vite-7.3.7.tgz", + "integrity": "sha512-AHEzTORMbi7/7aXp7w7KZ5+DCUdSeLsJz0grY8xHpc9DHdYb0vQu6DqJfm2tOw3gDExNgPgnJTXxM6syYFHaCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "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 + } + } + }, + "node_modules/vitest": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/vitest/-/vitest-5.1.0.tgz", + "integrity": "sha512-MzmNFnrwa8v8lF1bLxw3UJ4EREwVOVb75VqJZmUDRjJ6EluK/x9th7qTbaeDk41WxE1GxWqmTXcdsfobfH5sOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.1.0", + "chai": "^6.3.0", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.4.3", + "obug": "^3.0.0", + "picomatch": "^4.0.7", + "std-env": "^4.3.0", + "tinybench": "^6.2.2", + "tinyexec": "^1.3.1", + "tinyglobby": "^0.2.17", + "why-is-node-running": "3.2.1" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.1.0", + "@vitest/browser-preview": "5.1.0", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.1.0", + "@vitest/coverage-v8": "5.1.0", + "@vitest/ui": "5.1.0", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/magic-string": { + "version": "1.4.5", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-1.4.5.tgz", + "integrity": "sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "10.0.0", + "resolved": "https://registry.npmmirror.com/vscode-jsonrpc/-/vscode-jsonrpc-10.0.0.tgz", + "integrity": "sha512-USSfxg0vDIpTQqlQBGxF3mBARyoS8cU7LHLpbYNh6dOJt32dlskxPWB0Irrh5TllTmyf7oNLAvqDqzklbwwnpA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.17.0" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.18.5", + "resolved": "https://registry.npmmirror.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.18.5.tgz", + "integrity": "sha512-FW4fBMzZ99N8xeob4py0l050wgCoRgTpEq5Xcsb9lM1bc29UKGR/wSV+4knaqFxssFHajy7OzNiKP/m3U1ZEuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "10.0.0", + "vscode-languageserver-types": "3.18.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.16", + "resolved": "https://registry.npmmirror.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.16.tgz", + "integrity": "sha512-qeNEuOi9a3qkqRf7Gfez2WL2VwlBTLCNekHBGO9kxPVUp9XuY15yfTJ9jwvOwKI0I2MhUUfZq95NSp+LK1CLQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.18.5", + "resolved": "https://registry.npmmirror.com/vscode-languageserver-types/-/vscode-languageserver-types-3.18.5.tgz", + "integrity": "sha512-kPd6+JQNz3HMJvjKlqUxERSlZHAntl/aGpsk7ImwIh0ewDKDcjgAEyVSYciPiOJD3ftbK9IrfiqUeTZLSbCizw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.2.0.tgz", + "integrity": "sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.45", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.45.tgz", + "integrity": "sha512-3cvsib8m9c0vB24+MDKsgcW4+Da4xaBl9Z/5Tbm2pmSIsRPxcil4cdXjXlVsmJ2g64XJo5h+IRd7rsdGT9pZaw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.45", + "@vue/compiler-sfc": "3.5.45", + "@vue/runtime-dom": "3.5.45", + "@vue/server-renderer": "3.5.45", + "@vue/shared": "3.5.45" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "3.3.14", + "resolved": "https://registry.npmmirror.com/vue-component-type-helpers/-/vue-component-type-helpers-3.3.14.tgz", + "integrity": "sha512-Pq3+OnbCiPCHk0R7KSZrx+j/XXaUn2kSQ1epxq60w8CPbByLjC1EJgV51n4WY851sTXUVzsNMuitywdkdVyKjA==", + "license": "MIT" + }, + "node_modules/vue-eslint-parser": { + "version": "10.4.2", + "resolved": "https://registry.npmmirror.com/vue-eslint-parser/-/vue-eslint-parser-10.4.2.tgz", + "integrity": "sha512-WMpzulb50kb6snghzoGzS++62wcOszAk/piYcnvzdStM8Wni/BmogYltpoOFv2eK9hDbXxvwFQgnwZGpAoUfpA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "eslint-scope": "^8.2.0 || ^9.0.0", + "eslint-visitor-keys": "^4.2.0 || ^5.0.0", + "espree": "^10.3.0 || ^11.0.0", + "esquery": "^1.6.0", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/vue-router/node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/vue-tsc": { + "version": "3.3.14", + "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-3.3.14.tgz", + "integrity": "sha512-URAu2H9AuMZMg9GCgnqbaHHHd8pBZRAwNV2e2yYS8VDsoTVXHiOSo663OS6DkEvauQ2yixn5TPFJ9WV9QIspkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.30", + "@vue/language-core": "3.3.14" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-3.2.1.tgz", + "integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==", + "dev": true, + "license": "MIT", + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=20.11" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml-ast-parser": { + "version": "0.0.43", + "resolved": "https://registry.npmmirror.com/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", + "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmmirror.com/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/console/package.json b/console/package.json new file mode 100644 index 0000000..09652be --- /dev/null +++ b/console/package.json @@ -0,0 +1,52 @@ +{ + "name": "vdi-console", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --host 127.0.0.1", + "build": "vue-tsc --noEmit && vite build", + "preview": "vite preview --host 127.0.0.1", + "typecheck": "vue-tsc --noEmit", + "lint": "eslint .", + "test": "vitest run", + "test:e2e": "playwright test", + "api:generate": "node ../tools/generate-platform-openapi.mjs && openapi-typescript ../docs/openapi/platform.v1.json -o src/api/schema.d.ts", + "mock:init": "msw init public --save" + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.2", + "element-plus": "^2.14.8", + "openapi-fetch": "^0.17.0", + "pinia": "^3.0.3", + "vue": "^3.5.45", + "vue-router": "^4.6.3" + }, + "devDependencies": { + "@eslint/js": "^9.39.0", + "@playwright/test": "^1.58.2", + "@types/node": "^24.10.0", + "@vitejs/plugin-vue": "^6.0.10", + "@vue/tsconfig": "^0.8.1", + "eslint": "^9.39.0", + "eslint-plugin-vue": "^10.5.1", + "globals": "^16.5.0", + "msw": "^2.12.7", + "openapi-typescript": "^7.13.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.3", + "vite": "^7.2.2", + "vitest": "^5.1.0", + "vue-tsc": "^3.1.4" + }, + "msw": { + "workerDirectory": [ + "public" + ] + }, + "overrides": { + "@typescript-eslint/typescript-estree": { + "minimatch": "11.0.1" + } + } +} diff --git a/console/playwright.config.ts b/console/playwright.config.ts new file mode 100644 index 0000000..dfea80b --- /dev/null +++ b/console/playwright.config.ts @@ -0,0 +1,11 @@ +import { defineConfig, devices } from '@playwright/test' +export default defineConfig({ + testDir: './e2e', + testIgnore: '**/live.spec.ts', + fullyParallel: false, + workers: 1, + timeout: 30000, + expect: { timeout: 12000 }, + use: { baseURL: process.env.VDI_E2E_URL || 'http://127.0.0.1:5180', trace: 'retain-on-failure', screenshot: 'only-on-failure' }, + projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'], ...(process.platform === 'win32' ? { channel: 'msedge' } : {}) } }], +}) diff --git a/console/playwright.live.config.ts b/console/playwright.live.config.ts new file mode 100644 index 0000000..c667fb0 --- /dev/null +++ b/console/playwright.live.config.ts @@ -0,0 +1,5 @@ +import { defineConfig } from '@playwright/test' +export default defineConfig({ + testDir:'./e2e', testMatch:'live.spec.ts', workers:1, timeout:45000, + expect:{timeout:15000}, use:{baseURL:'http://127.0.0.1:5182',...(process.platform==='win32'?{channel:'msedge'}:{})}, +}) diff --git a/console/public/mockServiceWorker.js b/console/public/mockServiceWorker.js new file mode 100644 index 0000000..0c970ef --- /dev/null +++ b/console/public/mockServiceWorker.js @@ -0,0 +1,361 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + */ + +const PACKAGE_VERSION = '2.15.0' +const INTEGRITY_CHECKSUM = '03cb67ac84128e63d7cd722a6e5b7f1e' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() + +addEventListener('install', function () { + self.skipWaiting() +}) + +addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +addEventListener('message', async function (event) { + const clientId = Reflect.get(event.source || {}, 'id') + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, + }) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +addEventListener('fetch', function (event) { + const requestInterceptedAt = Date.now() + + // Bypass navigation requests. + if (event.request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if ( + event.request.cache === 'only-if-cached' && + event.request.mode !== 'same-origin' + ) { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been terminated (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId, requestInterceptedAt)) +}) + +/** + * @param {FetchEvent} event + * @param {string} requestId + * @param {number} requestInterceptedAt + */ +async function handleRequest(event, requestId, requestInterceptedAt) { + const client = await resolveMainClient(event) + const requestCloneForEvents = event.request.clone() + const response = await getResponse( + event, + client, + requestId, + requestInterceptedAt, + ) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + const serializedRequest = await serializeRequest(requestCloneForEvents) + + // Omit the body of server-sent event stream responses. + // Cloning such responses would prevent client-side stream cancelations + // from reaching the original stream (a teed stream only cancels its + // source once both of its branches cancel) and would buffer the + // entire stream into the unconsumed clone indefinitely. + const isEventStreamResponse = response.headers + .get('content-type') + ?.toLowerCase() + .startsWith('text/event-stream') + + // Clone the response so both the client and the library could consume it. + const responseClone = isEventStreamResponse ? null : response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + isMockedResponse: IS_MOCKED_RESPONSE in response, + request: { + id: requestId, + ...serializedRequest, + }, + response: { + type: response.type, + status: response.status, + statusText: response.statusText, + headers: Object.fromEntries(response.headers.entries()), + body: responseClone ? responseClone.body : null, + }, + }, + }, + responseClone && responseClone.body + ? [serializedRequest.body, responseClone.body] + : [], + ) + } + + return response +} + +/** + * Resolve the main client for the given event. + * Client that issues a request doesn't necessarily equal the client + * that registered the worker. It's with the latter the worker should + * communicate with during the response resolving phase. + * @param {FetchEvent} event + * @returns {Promise} + */ +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (activeClientIds.has(event.clientId)) { + return client + } + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +/** + * @param {FetchEvent} event + * @param {Client | undefined} client + * @param {string} requestId + * @param {number} requestInterceptedAt + * @returns {Promise} + */ +async function getResponse(event, client, requestId, requestInterceptedAt) { + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = event.request.clone() + + function passthrough() { + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + const acceptHeader = headers.get('accept') + if (acceptHeader) { + const values = acceptHeader.split(',').map((value) => value.trim()) + const filteredValues = values.filter( + (value) => value !== 'msw/passthrough', + ) + + if (filteredValues.length > 0) { + headers.set('accept', filteredValues.join(', ')) + } else { + headers.delete('accept') + } + } + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const serializedRequest = await serializeRequest(event.request) + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + interceptedAt: requestInterceptedAt, + ...serializedRequest, + }, + }, + [serializedRequest.body], + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +/** + * @param {Client} client + * @param {any} message + * @param {Array} transferrables + * @returns {Promise} + */ +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage(message, [ + channel.port2, + ...transferrables.filter(Boolean), + ]) + }) +} + +/** + * @param {Response} response + * @returns {Response} + */ +function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }) + + return mockedResponse +} + +/** + * @param {Request} request + */ +async function serializeRequest(request) { + return { + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: await request.arrayBuffer(), + keepalive: request.keepalive, + } +} diff --git a/console/src/App.vue b/console/src/App.vue new file mode 100644 index 0000000..4a8f5ef --- /dev/null +++ b/console/src/App.vue @@ -0,0 +1 @@ + diff --git a/console/src/api/index.ts b/console/src/api/index.ts new file mode 100644 index 0000000..4f3eafe --- /dev/null +++ b/console/src/api/index.ts @@ -0,0 +1,74 @@ +import createClient from 'openapi-fetch' +import type { paths, components } from './schema' + +export type Admin = components['schemas']['Administrator'] +export type Desktop = components['schemas']['Desktop'] +export type Terminal = components['schemas']['Terminal'] +export type User = components['schemas']['User'] +export type Resource = components['schemas']['Resource'] +export type Task = components['schemas']['Task'] +export type Audit = components['schemas']['Audit'] +export type Overview = components['schemas']['Overview'] +export type Settings = components['schemas']['Settings'] +export type BindingInput = components['schemas']['BindingInput'] +export type ImportInput = components['schemas']['ImportInput'] +export type EnrollmentInput = components['schemas']['EnrollmentInput'] +export type DiscoveredVM = components['schemas']['DiscoveredVM'] +export type DesktopAction = components['schemas']['DesktopAction']['action'] +export type TerminalAction = components['schemas']['TerminalAction']['action'] +let accessToken = '' +export function setAccessToken(value: string) { accessToken = value } +export class ApiError extends Error { + constructor(message: string, public status = 0, public code = 'network_error', public requestId = '') { super(message) } +} +const client = createClient({ baseUrl: '/api/v1', fetch: async request => { + const timeout = AbortSignal.timeout(15000) + try { return await fetch(new Request(request, { signal: AbortSignal.any([request.signal, timeout]) })) } + catch { throw new ApiError('连接服务失败或请求超时,请检查网络后重试') } +} }) +client.use({ + onRequest({ request }) { if (accessToken) request.headers.set('Authorization', `Bearer ${accessToken}`) }, + onResponse({ request, response }) { if (response.status === 401 && !request.url.endsWith('/auth/login')) window.dispatchEvent(new Event('vdi:unauthorized')) }, +}) +type Result = { data?: T; error?: components['schemas']['Error']; response: Response } +async function result(promise: Promise>): Promise { + const r = await promise + if (r.error) throw new ApiError(r.error.message, r.response.status, r.error.code, r.error.requestId) + if (!r.response.ok) throw new ApiError(`请求失败(${r.response.status})`, r.response.status) + return r.data as T +} +const commandHeaders = () => ({ 'Idempotency-Key': crypto.randomUUID() }) +const query = { page: 1, pageSize: 100 } +export const api = { + login: (username: string, password: string) => result(client.POST('/auth/login', { body: { username, password } })), + me: () => result(client.GET('/auth/me')), + logout: () => result(client.POST('/auth/logout')), + overview: () => result(client.GET('/overview')), + settings: () => result(client.GET('/settings')), + resources: () => result(client.GET('/resources', { params: { query } })), + desktops: (page = 1) => result(client.GET('/desktops', { params: { query: { ...query, page } } })), + terminals: (page = 1) => result(client.GET('/terminals', { params: { query: { ...query, page } } })), + users: (page = 1) => result(client.GET('/users', { params: { query: { ...query, page } } })), + tasks: (page = 1) => result(client.GET('/tasks', { params: { query: { ...query, page } } })), + taskDiagnostics: (id: string) => result(client.GET('/tasks/{id}/diagnostics', { params: { path: { id } } })), + audits: (page = 1) => result(client.GET('/audit-logs', { params: { query: { ...query, page } } })), + discoverVMs: () => result(client.GET('/resources/vms', { params: { query } })), + sync: () => result(client.POST('/resources/sync', { params: { header: commandHeaders() } })), + importDesktop: (body: ImportInput) => result(client.POST('/desktops/import', { body })), + desktopAction: (id: string, action: DesktopAction) => result(client.POST('/desktops/{id}/actions', { params: { path: { id }, header: commandHeaders() }, body: { action } })), + enroll: (body: EnrollmentInput) => result(client.POST('/terminals', { body })), + terminalCommand: (id: string, action: TerminalAction) => result(client.POST('/terminals/{id}/commands', { params: { path: { id }, header: commandHeaders() }, body: { action } })), + createUser: (body: { username: string; displayName: string }) => result(client.POST('/users', { body })), + updateBindings: (id: string, body: BindingInput) => result(client.PUT('/users/{id}/bindings', { params: { path: { id } }, body })), +} +export async function allPages(loader: (page: number) => Promise<{ items: T[]; total: number }>): Promise { + const first = await loader(1) + const items = [...first.items] + for (let page = 2; items.length < first.total && first.items.length > 0; page++) { + const next = await loader(page) + if (!next.items.length) break + items.push(...next.items) + } + return items +} +export const errorMessage = (e: unknown) => e instanceof ApiError ? `${e.message}${e.requestId ? `(请求 ${e.requestId})` : ''}` : e instanceof Error ? e.message : '操作失败,请重试' diff --git a/console/src/api/schema.d.ts b/console/src/api/schema.d.ts new file mode 100644 index 0000000..b0279ed --- /dev/null +++ b/console/src/api/schema.d.ts @@ -0,0 +1,3732 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/auth/login": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["login"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["me"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/logout": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["logout"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/overview": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["overview"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/settings": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["settings"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/resources": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["resources"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/resources/vms": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["discoverVMs"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/resources/sync": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["syncResources"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/desktops": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["desktops"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/desktops/import": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["importDesktop"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/desktops/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["desktop"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/desktops/{id}/actions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["desktopAction"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/terminals": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["terminals"]; + put?: never; + post: operations["enrollTerminal"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/terminals/{id}/commands": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["terminalCommand"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["users"]; + put?: never; + post: operations["createUser"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/{id}/bindings": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put: operations["updateBindings"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/tasks": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["tasks"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/tasks/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["task"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/tasks/{id}/diagnostics": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["taskDiagnostics"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/audit-logs": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["audits"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/terminal/register": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["registerDevice"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/terminal/heartbeat": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["heartbeat"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/terminal/config": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["deviceConfig"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/terminal/desktops": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["deviceDesktops"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/terminal/commands": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["deviceCommands"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/terminal/commands/{id}/ack": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["deviceAck"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/terminal/connect-ticket": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["createTicket"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/terminal/connect-ticket/consume": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["consumeTicket"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/terminal/session": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["deviceSession"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + Error: { + code: string; + message: string; + requestId: string; + }; + Administrator: { + id: string; + username: string; + displayName: string; + /** @enum {string} */ + role: "super_admin" | "operator" | "viewer"; + }; + LoginInput: { + username: string; + password: string; + }; + LoginResult: { + accessToken: string; + expiresIn: number; + user: components["schemas"]["Administrator"]; + }; + Desktop: { + id: string; + name: string; + /** @enum {string} */ + os: "Windows" | "Ubuntu" | "麒麟"; + node: string; + vmid: number; + pool: string; + powerState: string; + serviceState: string; + /** @enum {string} */ + provider: "demo" | "proxmox"; + endpoint: string; + app: string; + width: number; + height: number; + fps: number; + /** Format: date-time */ + updatedAt: string; + }; + Terminal: { + id: string; + sn: string; + name: string; + group: string; + status: string; + agentVersion: string; + /** Format: date-time */ + lastSeen: string | null; + sessionDesktopId: string | null; + configVersion: number; + fps: number | null; + rtt: number | null; + sessionState?: string; + }; + User: { + id: string; + username: string; + displayName: string; + enabled: boolean; + terminalId: string | null; + desktopIds: string[]; + }; + Resource: { + id: string; + name: string; + status: string; + cpu: number; + memory: number; + storage: number; + provider: string; + /** Format: date-time */ + updatedAt: string; + }; + Task: { + id: string; + kind: string; + action: string; + targetId: string; + targetName: string; + actor: string; + /** @enum {string} */ + status: "queued" | "running" | "success" | "failed" | "timeout"; + message: string; + providerRef: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + updatedAt: string; + }; + Audit: { + id: string; + actor: string; + action: string; + target: string; + result: string; + requestId: string; + /** Format: date-time */ + createdAt: string; + }; + Overview: { + terminalCount: number; + onlineTerminals: number; + desktopCount: number; + runningDesktops: number; + activeSessions: number; + failedTasks: number; + mode: string; + }; + Settings: { + /** @enum {string} */ + mode: "demo" | "production"; + computeProvider: string; + pveConfigured: boolean; + terminalTransport: string; + capabilities: string[]; + }; + ImportInput: { + name: string; + /** @enum {string} */ + os: "Windows" | "Ubuntu" | "麒麟"; + node: string; + vmid: number; + endpoint?: string; + width?: number; + height?: number; + fps?: number; + }; + DiscoveredVM: { + name: string; + node: string; + vmid: number; + status: string; + }; + EnrollmentInput: { + sn: string; + name: string; + group: string; + }; + Enrollment: { + id: string; + activationSecret: string; + }; + CreateUserInput: { + username: string; + displayName: string; + }; + BindingInput: { + terminalId: string | null; + desktopIds: string[]; + enabled: boolean; + }; + DesktopAction: { + /** @enum {string} */ + action: "start" | "shutdown" | "reboot"; + }; + TerminalAction: { + /** @enum {string} */ + action: "reboot" | "reset-session" | "collect-logs"; + }; + RegisterInput: { + sn: string; + activationSecret: string; + agentVersion: string; + }; + RegisterResult: { + terminalId: string; + accessToken: string; + heartbeatInterval: number; + }; + HeartbeatInput: { + agentVersion: string; + fps?: number | null; + rtt?: number | null; + }; + Accepted: { + accepted: boolean; + }; + HeartbeatResult: { + accepted: boolean; + /** Format: date-time */ + serverTime: string; + }; + DeviceConfig: { + configVersion: number; + identityMode: string; + commandTransport: string; + heartbeatInterval: number; + }; + TicketInput: { + desktopId: string; + }; + Ticket: { + ticket: string; + desktopId: string; + /** Format: date-time */ + expiresAt: string; + credentialMode: string; + }; + ConsumeInput: { + ticket: string; + }; + Connection: { + desktopId: string; + sessionId: string; + protocol: string; + endpoint: string; + app: string; + width: number; + height: number; + fps: number; + credentialMode: string; + }; + DeviceDiagnostics: { + agentVersion: string; + bootId: string; + uptimeSeconds: number; + kioskState: string; + logTail: string; + }; + SessionInput: { + sessionId: string; + /** @enum {string} */ + state: "connected" | "ended"; + }; + AckInput: { + /** @enum {string} */ + status: "success" | "failed"; + message?: string; + diagnostics?: components["schemas"]["DeviceDiagnostics"]; + }; + DesktopList: { + items: components["schemas"]["Desktop"][]; + page: number; + pageSize: number; + total: number; + requestId: string; + }; + TerminalList: { + items: components["schemas"]["Terminal"][]; + page: number; + pageSize: number; + total: number; + requestId: string; + }; + UserList: { + items: components["schemas"]["User"][]; + page: number; + pageSize: number; + total: number; + requestId: string; + }; + ResourceList: { + items: components["schemas"]["Resource"][]; + page: number; + pageSize: number; + total: number; + requestId: string; + }; + TaskList: { + items: components["schemas"]["Task"][]; + page: number; + pageSize: number; + total: number; + requestId: string; + }; + AuditList: { + items: components["schemas"]["Audit"][]; + page: number; + pageSize: number; + total: number; + requestId: string; + }; + DiscoveredVMList: { + items: components["schemas"]["DiscoveredVM"][]; + page: number; + pageSize: number; + total: number; + requestId: string; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + login: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["LoginInput"]; + }; + }; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["LoginResult"]; + }; + }; + /** @description Request failed */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + me: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Administrator"]; + }; + }; + /** @description Request failed */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + logout: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Request failed */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + overview: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Overview"]; + }; + }; + /** @description Request failed */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + settings: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Settings"]; + }; + }; + /** @description Request failed */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + resources: { + parameters: { + query?: { + page?: number; + pageSize?: number; + keyword?: string; + status?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ResourceList"]; + }; + }; + /** @description Request failed */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + discoverVMs: { + parameters: { + query?: { + page?: number; + pageSize?: number; + keyword?: string; + status?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DiscoveredVMList"]; + }; + }; + /** @description Request failed */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + syncResources: { + parameters: { + query?: never; + header: { + "Idempotency-Key": string; + }; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Task"]; + }; + }; + /** @description Request failed */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + desktops: { + parameters: { + query?: { + page?: number; + pageSize?: number; + keyword?: string; + status?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DesktopList"]; + }; + }; + /** @description Request failed */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + importDesktop: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ImportInput"]; + }; + }; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Desktop"]; + }; + }; + /** @description Request failed */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + desktop: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Desktop"]; + }; + }; + /** @description Request failed */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + desktopAction: { + parameters: { + query?: never; + header: { + "Idempotency-Key": string; + }; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["DesktopAction"]; + }; + }; + responses: { + /** @description Successful response */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Task"]; + }; + }; + /** @description Request failed */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + terminals: { + parameters: { + query?: { + page?: number; + pageSize?: number; + keyword?: string; + status?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TerminalList"]; + }; + }; + /** @description Request failed */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + enrollTerminal: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["EnrollmentInput"]; + }; + }; + responses: { + /** @description Successful response */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Enrollment"]; + }; + }; + /** @description Request failed */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + terminalCommand: { + parameters: { + query?: never; + header: { + "Idempotency-Key": string; + }; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TerminalAction"]; + }; + }; + responses: { + /** @description Successful response */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Task"]; + }; + }; + /** @description Request failed */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + users: { + parameters: { + query?: { + page?: number; + pageSize?: number; + keyword?: string; + status?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserList"]; + }; + }; + /** @description Request failed */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + createUser: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateUserInput"]; + }; + }; + responses: { + /** @description Successful response */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["User"]; + }; + }; + /** @description Request failed */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + updateBindings: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BindingInput"]; + }; + }; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["User"]; + }; + }; + /** @description Request failed */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + tasks: { + parameters: { + query?: { + page?: number; + pageSize?: number; + keyword?: string; + status?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TaskList"]; + }; + }; + /** @description Request failed */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + task: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Task"]; + }; + }; + /** @description Request failed */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + taskDiagnostics: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DeviceDiagnostics"]; + }; + }; + /** @description Request failed */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + audits: { + parameters: { + query?: { + page?: number; + pageSize?: number; + keyword?: string; + status?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AuditList"]; + }; + }; + /** @description Request failed */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + registerDevice: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["RegisterInput"]; + }; + }; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RegisterResult"]; + }; + }; + /** @description Request failed */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + heartbeat: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["HeartbeatInput"]; + }; + }; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HeartbeatResult"]; + }; + }; + /** @description Request failed */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + deviceConfig: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DeviceConfig"]; + }; + }; + /** @description Request failed */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + deviceDesktops: { + parameters: { + query?: { + page?: number; + pageSize?: number; + keyword?: string; + status?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DesktopList"]; + }; + }; + /** @description Request failed */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + deviceCommands: { + parameters: { + query?: { + page?: number; + pageSize?: number; + keyword?: string; + status?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TaskList"]; + }; + }; + /** @description Request failed */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + deviceAck: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AckInput"]; + }; + }; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Accepted"]; + }; + }; + /** @description Request failed */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + createTicket: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TicketInput"]; + }; + }; + responses: { + /** @description Successful response */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Ticket"]; + }; + }; + /** @description Request failed */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + consumeTicket: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ConsumeInput"]; + }; + }; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Connection"]; + }; + }; + /** @description Request failed */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + deviceSession: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SessionInput"]; + }; + }; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Accepted"]; + }; + }; + /** @description Request failed */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request failed */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; +} diff --git a/console/src/assets/brand-mark.svg b/console/src/assets/brand-mark.svg new file mode 100644 index 0000000..da3a44d --- /dev/null +++ b/console/src/assets/brand-mark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/console/src/assets/kylinsoft.png b/console/src/assets/kylinsoft.png new file mode 100644 index 0000000000000000000000000000000000000000..415ec82d94e8bd5beda2400a10ba2837baad7778 GIT binary patch literal 10564 zcmZ{KbyOSO7j1Aa?(QzZ3dIV=-CaxZ;!f~DacFTZQi{8~yF0}lg1g(x_j~W(H!I1k zteKpM|05iV3nq z=offb2{}z<=;4d}EgS%#0?0{E;Jgxyy& zPHrHjK^vHCpEOjTwbNYaLRy6aTh}Z4Z7!W-*S3m4i!cTXx7I7`WW* zO5Q4HWZCSpAs<(5*uneY0^&&dw6x`->|A!ue((1*I2QkGzV2yhW!0o$E7}EvV|9?W zwB)^h;GGS9r2sbVFgqTnCzjHLWFsbE+D}C)&tBy8ueQ5BJFnD%GNw#9Pq>%sTMt)cnj!a5W5P3;7Kbt2EuHaNRkPlRYXau;n_heQlmL zG%+kF4G(DZh}F3Dq$iJ)^0XR#N9=p2qP;YEd!u;8gUO2CJ=i59g$1ntteM(zT5Gf_ zH-Vkyw4{4GX0sB#Fg|HZ{`HcXX-<#3C}Ov|bS3s}pz<4aMxY}YVzXlBr8hJ=oCYri zXbN+sqtd`l|9kwFr}w7E^{OwuZ4kUWd%g;4PWvxFgzA_?QU3Q}=L)?y=0JZ^z1^w6 zbKHwJ&$ITIarAEoyNY-)fZ$n-fvj=U*tc!ky==fm@%I8!U#e7pm)!Xm`KKCJ7X}pu zf-DV>HS*dQJa!u-yZuy++O14ix|*>H8wL7fE+;FI!TZRJ4!KP+@?XYycz1 zfZyr3qrWEdB|X4V=`k4=5@-1V@~U!jRsd`r8Wse82f%<%uhOs*SQ|z{t=(!3cV>sPH-uOUmxDi8hPTg z6L{voZMTRC1WUc!HoyEpaymn!^6?yLoZjQJeB11KV>OwS^;e#l!_~bRE|YmDc)MEK z7I`8Iyf5&6c2h&0~CF;|}RE^*4 zu`Uv9jBDR%Zb;`}*8;NApz{V-nt`DGPuibW`eQMyEvuR)hb}9EJ5SMHx&K|1Veq~Q z!;d^my?&4FqMY1L7k%jmFPu&8{w)_2e8t1&{Vl7(^XaQsY=?;5D>u)(L}Uu_ZosVK z!55XAx+2f$NSIZgxkBO1{zD8x`oX(1-ka3h9pyb61@5zy zm^XM9V!L-3d-MwwzdpK6THxhvWM2*!a65G6IZjRl1gE`g5iACG%I?KY)?GIGH{Fhy zpp)YwAbp0w(bX-&7{(@YE`Lx}eJ#oWXa|B>B6`Lz;_ASYbcIH{Vh9&b|EgU#cIW{g zB~9KkI8YsYafC|6feDziD&3iiC@S)^L5;6Jpmu8Il*dl;Q#vwVY z9{ygK8;{dYkmU!KeLvMtidZ!uS*>rVaFv^UXHn&2{I?cAt|gPqD1E_Vk#Gu*`ee z^*;VF{5Dv}@oO^!Ag!Y-5ko{;VPh|e@x(|B1F+H{SoZ(hHSyg$0_9zhwK6CWq&|}E zb?`Q!&@#DX+-t7hdI0aE?at=aoya-qtan{T-Q%h^gt5Ak@hOf8MDPFF^&WX`2C8gE`E21G(}vsYsehdt<*( z`RW(1!OqN;#-r#xj@tL>@7#xnY;sxKB*-JbLRgR&1BHj!Fu3KAZ@%k0um(|Rked&0 z6?bihRyL%wvRTRBtveJg?i20N+cVG^4udMHTl;bJ(U>H~iCYCDGV_%P#9FHs$?T%O zt*jJ-!;IX};rDzSc~;xar|LDnj`t##&du28Pq+obmrEnGyEXgaxQV;kekt6Qxys;x z8L8te6uQqAfE}8dRF2umun~Eg4Y@oZOmChwQ{`|X?-k%%$n0+HMHCTR<1Y$H%yEfv z1DhpDZWWkMn8*?BX-`z6;I&$QkvE(r$aJ#Z--q{6Xt2{tAxPwsxt6S(pSfkdS-h zxB0LFX7Ltw8*e7q#T8(Ay{@1`r}baN>2>JDPoOu_Flp7DF4rS@{2g&rf|rGt$%|IK zn>_OCP~Nh|8l?1bi%FtRdaaKB_~2-ZnUo>3#EpcibV|`e_TpD? z{3nvKUI)HK{AvLCsLbDD3wIxmOijW;WL3y=FcFb|v3;M|RQ295&l1Sq_dzT9=BD+` z|5=5nRpLdMQ8|kb35)G+3d3jK0i%hdia@G<-q0-irVRZ2B__23NeU<;TkpbESOO8& zA3oFM$s)+=XRP(GantsLc>MR{7t;hWMs5AuUc5j2RRj@h%gsGCuA)KiNI^Q}vqy4% zsP)`ovWgwPfN_TkUp{V;IaTxT@lnda%-flU&jDEDYe2F8;k+$@b?6}j=k3Tg;A5SBbpIYP0Yf%bj~Z?35;M?=6g(p*hGJXwZ8@F$bOz3-$58E9|e&5d8_e6imGP409NsqR@a*p8N>Z){sAGlf8`F>J_~xyB6F*pN z6Y}OIZxd|6-=0Mt=lZJ~7={9~x|@uVyx>@p++>| z7Dh$E+XS14z;uUO(l{rlBToCex3^7ck7o(M*BGj#lV~yz)lR-&(#Eyf%y_yiqi9$D zRewEb-Dz9iU}x}MYc^tq-8+Im6k=dOLKu74h8RK2teXal3*(fdSu`uGb1+u5=&Y zVx2J8e8kI@M!?&e@eKaYs(gPm*2%Ijb=lLpd-AHr28>r=+qGlKy+IXoKtp&}o+pDJ zqcas-!+7g^sKJ9Q37TOnw!K1$@sNs;lIHZzH(ED=A6oR?i+q&8&uzRCQJ3T0GuluV z9cyMp2O|S2*l^;$yEmOmZ*O!hDHHS|sLw~+(Xfd++4^!;c1ub;GD+O-Qk4y>qs}H{ zU)1vW&v@2iJIcl=D)3f7!BB-JEwO7R4cv*j$zwBek}&zuBxLtGTef>Z#_>V!+tNw< z8hdh9fBpewPf@A99ERa21y98$(99xtKKkS`Kz*3*XdPM~pUd|*5O$!24++5a*54rb zUOl#Z;P$*tUz6OPLdA=`D?iLh zXLNwYbX;JrtA(dZKae0pUtxG?9M;`PE#;SK(bO??v5~ABTp;y(%;!HB!)*J(BJ-gkC( zf4)_J4}R=tt3cQ!!}fD%y@3dEL&>!3_AnihPGXJ9dR(1;<%C!E@r7ME}^D9lj**!3TgA^(g7;yt`Ni5af*U-|715ej%++;-&Hie*684c##ub1up z16=6;7`Y_3-kkboSRLFT=+5}fQf+$>a8l@NLpgxdsARWmpW))7%$nA8hLHS=qDQE0 zpC7v&^K(ggCP>#=exdRkoe<-JBzDu;A;Fk0rzKv1nd1Rwem*9kDP{k*6>BTSIGsJN zK1M@%!iI`;(J!Rq%lVy$G1XAi)JIqabe`8`=v>KVgDSa9ykKqh!~JO~^u_j+(1p=S z++7(#@O%wAGFO29`~?-`!R=>$*BkgOctU;7fMj~@8eOZ2kXpLknAtiTW911ZMXkvu zPtdpu_s&7>FJ;VMn+J`Q{=A3&`p`@ebi(uRnPw81v#72)z7c@mozw4K;YC=``Z+ru z2AVq)W1za=kPbJBW8gciL!(DP5^-Ao0J0b9I9^B9E94KRCAAo%70IyV^x^75w+N{7 zQKc(WZj_0pEo>!p(jPx{SOWU&>;6lF^xFBBAP>DDz$zK7A^!A4RRzLr;qIV0uXQn~# zE%d+iD59l7WeF+l2Z~j(do#d0585O8Q{!N@lB@DWe(FdOh{&-w`(<+z=8YX6B9z;E*w4R(TQQsxSNeZ7C?#nqtcBhNXe4DMxQ^k$YzC&y@OFpr45p_9OhO6w zO6|1@&-FXtVu06z=VEd5eRNo`8V@FLa*Q|kANUgry6s#?a4 z+A#V596U6!K%KN35k=U+BBEF|xkl9k_a9JJtUSUa%zQ5>o7Y3|KDu6sBCf*y2UP+; zusfc>-m&nv;=W<1>?*pD_S%Nl-PgseK}ACQ%0q@`O7d8s3HT@9S09`ZlE87>(&qR( z_m-x=SL|3A82NP7ZJT_R7Vc}g&s^nxT!u7F@l`P&R(XG)H`s^_cF_YUh@b$a=wcw- zLLYR>=HHv&R8S9Niqb`(&;gTKykgu-s#5`_f7BP|W`oEV1p4EnJ42kYKEdx&E2Ds* zX)#T#Ej>U4o(R^@F;>;A!NeoTaq0p+tTqfkV#^k%&<}U_Ov0J_@W`ZmI19o?%?@Qr z4Ccdurkbbo$r4zu&qY90+B+TjrUC@6qp_yUn`fi}*q|4dE-hI0Y5>3(jteT6Nt^D} zRc+f9gOh{!S7%-b&^m%9#S!fZ1e4`lmj3j% z>&7Gq$Z;y#eZ}I$HU~=zO$#}ZMVCR;3F<5Iu#hIB-DNbQp}6*W-alQHlwNs@2)~!F z+<@+ZA+Wc`2v$2`KuFf2RfMu9n6m&lLqk0mcL$B6@5M1%sjp@5o}* zcHL0$1aFpjs`&4O9yn5L(+Mk7wFw|ufJA*RDvOJTU+KHYvAIJobQ!e@;?C&C9$`SYI;xyrEi-ot;m(bg(TdXZJm z4C4M4NcgESD^`nG6JCRPtz_s&`{GH)48j4^M&Tvm;54z|(eDHfkJ9F|^o80TU3co` zQnGC`E`clGpgRPD9ea>eGzRkPCc)~|6o4|HsaH(d1(f+LA4Q~5{F6C#3NtoK*_Qa1 zfTHHK(d;sSvSV|E8EjA^alNZ{fPLxeFpO#_2E|$-!h{Wb4=yvPbpL`BqKoK6fA!*I zH(O=U`s+ZM%$~|2pIIxA7ap=zcXbXddsM~x0Fa$R7-x>D4o*QyEt9gU=yyqR7sq<_ zmf^%6Ep~$Ya_Dx16}sN}y(ZtTVKJKT2nyNp=7}lckfpQQuX1Vl7y12W4#qG++SFcs zUHK*(;vLkNU7Cy%Mk8QSxkT0fiW^X;{Rw&Or&sQ_|ZybZRSm*IP1B90-Iq~?No zrGpR_INW5T*(1MPl5R2cP2Z}_W?}ZA@-L`G(jhzp{eVjxIEBy7q_l`#@%q;`8QHW2 zc^_YD`mjbvoG?vP*Q#Msx$s>piOBr!6R8J6RaGqT&ZXX$f(UQc(~s^`@#qd=nGBT9 z!2u2H$;=#``6~*cKUvelZq*=n5v#9@EVn8&wMAb zYQ{J@y`tysu3cN^S%i73l8xUiKnILL7dIMD7ZC5a`~|YL9+GhuoOoYX(~^R)A_>hV z{P&PQJgx7)TF-#c2FPlPk8nvhg)qZ6GjGk0&g!xkMqkx2hJG;U?#GmTZvSn_B_{S8 zKMn=eM;6Zw#L@yszqr-a%`1^kc-Q_^H)TvY4Ed%&{lp>&Jni- zP(SoEn28X$fB#vr(mJ)=y!@I01$tRL34HAohMBC7^1Gk#7{2g*#K{Rux@O1~#e{P$ zwE2NgUk~WQ(a{+^1_n#uj_bhh83X9E4&QYhy$sZLKEj-dW7<$dFr~8Y3xgsKsU$A|SC@ z!?+HR4DYGcfYie=N=>h+3(ECfN^TKpC&4=uR0aIk>4Q?@9gjsdg97D3<-Dr&J|6@P zk~LHQn)qYwF14}GhMP5UZMIc}r*7YRGLfZJ%&LbqcgFNbbLp!@IZ|V)thzS&`d8~D zU}MeoQ%)BktczrS+Vb5@z*i9$Op$HdDjkOkV_(*>9D*snZ!5lJa`bk`53p>A5=Z(+I*&cF_Kk3>#918Vl`#JP>83#NzzsCQ6jJVj zuKt!TVOQtG5&@$u4~WYcHr0auxTHfX)=mDhg(xmFCj^}*85r?k{IkYY>tog@{RC{- z+K<|*g4%QBx+yiT`2IP0lcAP4gwr;y%jTgHO6KJSqy_Aj@Eci`n9&azM#nr}(cN}G z^C-yBv}ULboHTGRU=^S(3r`Y<;<7d=kfY})@L&-+vO8Fn)^@{1(T+XVJ&p;FSU*+} zRn3N#fvlhmkwEx1z#&J>NM*7)2vfS!$`ksBy1YnTrS)8uL|&?~iy=6^ji7`tXZm67 zMdq=@gn7ION$fhg1!+i9lI1T5iLmTnsmAt#yO&M;GseoYFfQ? z`Q65QePD$T^(>5;PD|GF7OSp?7O~cP*H8;9XUH?~Xs3^NhVB|imwERwgtSMILVE9_ zRipFv6vP2HsGHMZuNxHZBoSNpeZFZvB(`B43}M#w``D<#HF} zP0aP(Nzq2SHRF))h-lI+i&yamuAk)%!JDljt@{u63e%5X(6(Tq*}Svnf^R=?pC4>9 z;-cBvvGd>P^5Vm-sQ8go;97mD&qdWo^i#rwfKj%un!0jx)E~;NLlO8Rq6`2ydy2g@ zFA_8^Kuon~hPfym^?Y1-YgUh`gEJg(olT)rth_2%{KSds*de7q{wu6=%4Lo~1_rDa6w-q(MNyV%EzT3nYHu;eTI zu;L@fHRk$_rwhtnBPyh(TIZ;^@Eu=~I)k^f$BqA5F6o6}jqVT?+aPz{y3pXg*7XoM zv{C(S6M1MHV+ZG#Q%r9ElU1)jNSFXDhSD3+ZXL`JH=Q(YX)khQO1em8)A?64y*u)l z+5e!ps5!Y$)`tJYp0~{K_m!>1)t$jd{LswWNzpE=sq%dT0%_U8A;!-7Njr@1C-I;_ z@})uItc%!zXYOt_ey^shzAUdvvaWx#Sk)~@cUozf%)BGH!EQ|0b$VT;1($_@n}ROJ zd2jHLxY2{=32fENFq$^nE0l(QF0g$Nd5*E$KcB1{z8#7k9FB<0*X<-@G&~4<>`?5B z&~0eKhw-zunb||((!h$Dup?wcNjOD_%>(Eqe40G0u}J;V;;Ww9U}|Nk zq}#{R@;7aXiL!d(rX4yS6O3hiddp~Sb!I13@~1Ouq1XqZ-*|{+_8^V=avUDc7_#GE zPSnf#f&p^!-v_IcTe#PYZXwe}$L~SCF~WmKPS-xP>eHe52HX|A z`>5ehhUneA4W(&yNpnqDn-wxsCOUbZQ*xhZ04fr27RKpB$&z`r0Ne20qCM%)XaZO&Eacd?FbxdO93C4x%JU7zk*(Gp%bBcZ>v9|tj zbi1wti7flGZq-seb&;8-hU5+!E6fO>KzJ9zk1ldCq<)YX8y=1~*DUy&!OXBKYu0&9 zuM|eAG9NF*0StDQxTQ*2Rc|hA&@EFypWFxi=|G+lKr6TOY~G!YD&9z!F^Vh_a_mCD zGp&!0tC>quJ&o~)QiP$Yi5$Pla6JwjjLHB@r;SzJ9IiQFC~^gK;S%{mHB_kS#0Nag z4A!OszMIVO>gK8q;f33&|42SM!u)|h0D!#x-(G-W?3Cv?*7jza z4e|&*=ZjzuyWY`%((^`G%X2yf z#q`_VpR@INnT;LSBFK>~-Cf|;+m7I$FT$&kz(Az|kzQc=9>plu*ZF)jwKgtZrC*14 zgXb;(V0C{6PvSy=TqOX~91jHI3XX7VIC2+QTRptVOlcoBAMxBVa!jV*wo*D6E**B6 zEMF$opS-gz?dMypLoG~~Eg_eD1ejyGs_C5yjxcw((+X?z4;qPSK>utc zVZbO=(Q_?1VO|cJbTLYq55v7%B-AOKAtI^2ZC@twqh;>CHEM%-%Ikp3=#se&Rv<=U zy4o%oYM;rl;d)@|qL(7ytn%A898lY+31ShAdxiZoh6 zgn~?+e#ykFf6i2Cy5d5My{Gy;bWw<6&VFAS;f(`VyGul&o|?j@7K0&!YUbbE->0W@ zVB3~A5cNzU#cPoJ5?{Ig#RexjXC$_R=CBQ_mnm)2*=$Rq z^v!dfOA#cfK21RUyaodqcIt;g%@LauMwYUOo7mCP6%J4&j0#mTh*&Q-vss1&1w2)I1t<+Oy}@Utzb z@o~X$!zInaD&{g!CeKf5ZymP^YKTRU>i+ilZ~!6O_844iWKjx7eI1Vp6H+ROyy2tQBY1?)?A%7VG z_8vLVu;+u?b7%M~D>0gFra80KHc%4Ouy}!ZjDg4{i&yc?+?6RF8|<8mwozXJIufW| zs419e2G*e}U+N)p2-faBXtR{{Uji{)f@;Fp;WRZ$d6|wn8C~HN4dBZX`nVPT zL7lf9ZQCuU6g8dH)9%|)H>h_H*E3pDDQaTbJ z;9TCanj}{PLyikiEfc#t`ZCxMA2ce4jMSf&KSO1|>)6$N_`iV~^N9_LM*BM(+SEU# z+OY02&sE{VEdQa%SW!^ZfXlw{Wp|IMoGli*{(WiQ5Mgic?J~BIcgWcz{oWW!M1$>t z3T-N)^`dmC#}gzutpwH`s@7GlIe+D&`A=S7n5tJLzY_!a;!jxt6&Ms&7Z;}eMfdo@ z5Pgi82nTR9n-h8|iMd#Zv^A-i--Sw0q$)cdWGRQk%~MGt zrn_GBwQ$Qa%vpa^(*ASh@@dZ<$#3(q$|8Pl_}u-m z3z<9hj!>so5$wl6&2LxB(Gl`*n3G5x%>`QKOA)JW+qa*wI}CJ5{pDUCy;TFqmNtA6 z%dJ2rUeJPxtt(Rbb-BhsXawKj((oaYTdDA$OXEtws%c$s2k89=#-?p1OpGoW;#{nU z28N$y2G}CY*?2+KQ~zp?&Cwm23?}b6P)+f1jMe$6!=e8*MF}11N2pV6ukWFlhS%e$ zPQ)#P@1HAk(c4GuQTyzWxbnWGC&oSak4SYSjZf+4z?9S$I20Q$;0DgQ+uJ#>3E0_s zzAnS$-FvR#*>X|HHC2EE*JE@C6TBfj0nnx%7-%xmRQUue5z?98P0+x`+-`3{Jzp!H1qB{ZIt9AlgpQ%U zrpKG`i{gQ=duKC5ClXpwDSrpvs?ikQ8M|t%`SH?;VFGCS(t9~=Fx0(|=5YT^gy1J< zUF(m!JT#|W&uhDe5~;8x`G$y0tKh}23P4>*&N%Mz3yp+uO^@VoKCetmNiLSVag)@i z`y`5ofJzx>`%bQ}g`!x(SID|na#1qbhe`>&Mhlg>4)7IAvXZB z{-6tlC1J9J<(b5b61@1G>$3mI-&K}jG!|pOBPGfQQ3Q%vkcfxp$Q9cPbbrr)%{;yUtbD%t z1D}eOlq;ce`E`1)$&)H=T}55uLgh7`JLP|^b!m&8&t%#IOQ3P3fH*7kQ-}ZHtRv4F zZ_&*TcNjqYvNb!xR%a?iVQD`DEh*F_q#ay&HA9Wlc4ZJHu8 ztEFlvaUP-)@Ll^81e2oD8h?-D#cO174<%S6*q6O7+kb*YwN=k=B}`b0HxZP`0_813 z&w0tr9ex#6b+1mdXb5`?-P-r+C#N=JgSwXfs{n#2l4gA0xuQTSwBwvmJqEp*XRmq3 zNloo(oCmOh)~r-^*j*^3GhxW^i7VB6{LVw&wG?dC41xB1u|67~Utbx&iS?hn{qxUb S6ou}D1jtD#OIApj2LB%p?#;^p literal 0 HcmV?d00001 diff --git a/console/src/assets/ubuntu.svg b/console/src/assets/ubuntu.svg new file mode 100644 index 0000000..227f86a --- /dev/null +++ b/console/src/assets/ubuntu.svg @@ -0,0 +1 @@ +Ubuntu \ No newline at end of file diff --git a/console/src/components/StatusTag.vue b/console/src/components/StatusTag.vue new file mode 100644 index 0000000..5aaaf93 --- /dev/null +++ b/console/src/components/StatusTag.vue @@ -0,0 +1,7 @@ + + diff --git a/console/src/components/SystemIcon.vue b/console/src/components/SystemIcon.vue new file mode 100644 index 0000000..2b0e88c --- /dev/null +++ b/console/src/components/SystemIcon.vue @@ -0,0 +1,13 @@ + + + diff --git a/console/src/components/ThemeToggle.vue b/console/src/components/ThemeToggle.vue new file mode 100644 index 0000000..b136b14 --- /dev/null +++ b/console/src/components/ThemeToggle.vue @@ -0,0 +1,8 @@ + + diff --git a/console/src/composables/useAction.ts b/console/src/composables/useAction.ts new file mode 100644 index 0000000..eec0270 --- /dev/null +++ b/console/src/composables/useAction.ts @@ -0,0 +1,20 @@ +import { ref } from 'vue' +import { ElMessage, ElMessageBox } from 'element-plus' +import { errorMessage } from '../api' +import { usePlatform } from '../stores/platform' + +export function useAction() { + const busy = ref(false) + async function run(work: () => Promise, message = '操作已完成') { + if (busy.value) return false + busy.value = true + try { await work(); ElMessage.success(message); await usePlatform().refresh(); return true } + catch (e) { ElMessage.error(errorMessage(e)); return false } + finally { busy.value = false } + } + async function confirm(title: string, message: string) { + try { await ElMessageBox.confirm(message, title, { type: 'warning', confirmButtonText: '确认提交', cancelButtonText: '取消' }); return true } + catch { return false } + } + return { busy, run, confirm } +} diff --git a/console/src/composables/useTheme.ts b/console/src/composables/useTheme.ts new file mode 100644 index 0000000..01f69f1 --- /dev/null +++ b/console/src/composables/useTheme.ts @@ -0,0 +1,19 @@ +import { computed, ref } from 'vue' + +const key = 'vdi.theme' +const saved = (() => { try { return localStorage.getItem(key) } catch { return null } })() +const theme = ref<'dark' | 'light'>(saved === 'light' ? 'light' : 'dark') +function apply() { + document.documentElement.dataset.theme = theme.value + document.documentElement.style.colorScheme = theme.value +} +apply() +export function useTheme() { + const dark = computed(() => theme.value === 'dark') + function toggle() { + theme.value = dark.value ? 'light' : 'dark' + apply() + try { localStorage.setItem(key, theme.value) } catch { /* The theme still works without browser storage. */ } + } + return { theme, dark, toggle } +} diff --git a/console/src/layouts/ConsoleLayout.vue b/console/src/layouts/ConsoleLayout.vue new file mode 100644 index 0000000..37363bc --- /dev/null +++ b/console/src/layouts/ConsoleLayout.vue @@ -0,0 +1,53 @@ + + + diff --git a/console/src/main.ts b/console/src/main.ts new file mode 100644 index 0000000..197c70e --- /dev/null +++ b/console/src/main.ts @@ -0,0 +1,21 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import ElementPlus from 'element-plus' +import zhCn from 'element-plus/es/locale/lang/zh-cn' +import 'element-plus/dist/index.css' +import './styles/tokens.css' +import App from './App.vue' +import { router } from './router' +import { useAuth } from './stores/auth' +import { usePlatform } from './stores/platform' + +async function start() { + if (import.meta.env.DEV && import.meta.env.VITE_API_MODE === 'mock') { + const { worker } = await import('./mocks/browser') + await worker.start({ onUnhandledRequest: 'bypass' }) + } + const app = createApp(App); app.use(createPinia()); app.use(router); app.use(ElementPlus, { locale: zhCn }) + window.addEventListener('vdi:unauthorized', () => { useAuth().clear(); usePlatform().clear(); if (router.currentRoute.value.path !== '/login') void router.replace('/login') }) + await router.isReady(); app.mount('#app') +} +void start() diff --git a/console/src/mocks/browser.ts b/console/src/mocks/browser.ts new file mode 100644 index 0000000..25c8685 --- /dev/null +++ b/console/src/mocks/browser.ts @@ -0,0 +1,3 @@ +import { setupWorker } from 'msw/browser' +import { handlers } from './handlers' +export const worker = setupWorker(...handlers) diff --git a/console/src/mocks/handlers.ts b/console/src/mocks/handlers.ts new file mode 100644 index 0000000..55455e5 --- /dev/null +++ b/console/src/mocks/handlers.ts @@ -0,0 +1,47 @@ +import { http, HttpResponse } from 'msw' +import type { Admin, Desktop, Terminal, User, Task, Resource, Audit } from '../api' +const now = () => new Date().toISOString() +const administrators: Record = { + admin: { id: 'mock-admin', username: 'admin', displayName: '系统管理员', role: 'super_admin' }, + operator: { id: 'mock-operator', username: 'operator', displayName: '运维人员', role: 'operator' }, + viewer: { id: 'mock-viewer', username: 'viewer', displayName: '只读人员', role: 'viewer' }, +} +const desktops: Desktop[] = [ + { id: 'desk_mock_windows', name: '设计专属桌面', os: 'Windows', node: 'demo-node-a', vmid: 201, pool: '设计专属池', powerState: 'running', serviceState: 'unknown', provider: 'demo', endpoint: '', app: 'Desktop', width: 2560, height: 1440, fps: 30, updatedAt: now() }, + { id: 'desk_mock_ubuntu', name: '研发办公桌面', os: 'Ubuntu', node: 'demo-node-a', vmid: 202, pool: '研发专属池', powerState: 'running', serviceState: 'unknown', provider: 'demo', endpoint: '', app: 'Desktop', width: 2560, height: 1440, fps: 30, updatedAt: now() }, + { id: 'desk_mock_kylin', name: '国产办公桌面', os: '麒麟', node: 'demo-node-a', vmid: 203, pool: '通用办公池', powerState: 'stopped', serviceState: 'unknown', provider: 'demo', endpoint: '', app: 'Desktop', width: 2560, height: 1440, fps: 30, updatedAt: now() }, +] +const terminals: Terminal[] = [{ id: 'term_mock_001', sn: 'DEMO-RK-001', name: '设计工位 01', group: '设计部', status: 'offline', agentVersion: '演示版本', lastSeen: null, sessionDesktopId: null, configVersion: 1, fps: null, rtt: null }] +const users: User[] = [{ id: 'user_mock_wang', username: 'wang.demo', displayName: '王工', enabled: true, terminalId: 'term_mock_001', desktopIds: ['desk_mock_windows'] }] +const tasks: Task[] = [], audits: Audit[] = [] +const resources: Resource[] = [{ id: 'demo-node-a', name: 'demo-node-a', status: 'online', cpu: 0.24, memory: 0.48, storage: 0.35, provider: 'demo', updatedAt: now() }] +const revoked = new Set() +const keyFor = (r: Request) => r.headers.get('Authorization')?.replace('Bearer mock-', '') || '' +const admin = (r: Request) => !revoked.has(keyFor(r)) ? administrators[keyFor(r)] : undefined +const error = (status: number, message: string) => HttpResponse.json({ code: `mock_${status}`, message, requestId: `mock-${crypto.randomUUID()}` }, { status }) +function guard(r: Request, write = false, adminOnly = false) { const a = admin(r); if (!a) return error(401, '登录已失效,请重新登录'); if ((write && a.role === 'viewer') || (adminOnly && a.role !== 'super_admin')) return error(403, '当前账号没有该操作权限') } +function list(request: Request, items: T[]) { const q = new URL(request.url).searchParams; const page = Math.max(1, Number(q.get('page')) || 1), pageSize = Math.min(100, Math.max(1, Number(q.get('pageSize')) || 20)); return HttpResponse.json({ items: items.slice((page - 1) * pageSize, page * pageSize), page, pageSize, total: items.length, requestId: `mock-${crypto.randomUUID()}` }) } +function audit(r: Request, action: string, target: string, result = 'success') { audits.unshift({ id: crypto.randomUUID(), actor: admin(r)?.username || '', action, target, result, requestId: crypto.randomUUID(), createdAt: now() }) } +function submit(request: Request, kind: string, action: string, targetId: string, targetName: string, finish: () => void) { + if (tasks.some(t => t.targetId === targetId && ['queued', 'running'].includes(t.status))) return error(409, '该对象已有任务执行中') + const t: Task = { id: `task_mock_${crypto.randomUUID()}`, kind, action, targetId, targetName, actor: admin(request)?.username || '', status: 'queued', message: '模拟任务已提交', providerRef: '', createdAt: now(), updatedAt: now() } + tasks.unshift(t); audit(request, action, targetName, 'submitted') + setTimeout(() => { t.status = 'running'; t.message = '模拟执行中'; t.updatedAt = now() }, 400) + setTimeout(() => { t.status = kind === 'terminal' ? 'failed' : 'success'; t.message = kind === 'terminal' ? '模拟终端离线,未执行' : '模拟任务已完成;没有操作真实资源'; t.updatedAt = now(); if (t.status === 'success') finish(); audit(request, action, targetName, t.status) }, 1500) + return HttpResponse.json(t, { status: 202 }) +} +export const handlers = [ + http.post('/api/v1/auth/login', async ({ request }) => { const b = await request.json() as { username: string; password: string }; const a = administrators[b.username]; if (!a || b.password !== 'Demo@2026') return error(401, '用户名或密码错误'); revoked.delete(b.username); audit(new Request(request.url, { headers: { Authorization: `Bearer mock-${b.username}` } }), 'login', b.username); return HttpResponse.json({ accessToken: `mock-${b.username}`, expiresIn: 3600, user: a }) }), + http.get('/api/v1/auth/me', ({ request }) => guard(request) || HttpResponse.json(admin(request))), + http.post('/api/v1/auth/logout', ({ request }) => { const e = guard(request); if (e) return e; revoked.add(keyFor(request)); return new HttpResponse(null, { status: 204 }) }), + http.get('/api/v1/settings', ({ request }) => guard(request) || HttpResponse.json({ mode: 'demo', computeProvider: 'demo', pveConfigured: false, terminalTransport: 'mock', capabilities: ['auth', 'resources', 'desktops', 'terminals', 'bindings', 'tasks', 'audit'] })), + http.get('/api/v1/overview', ({ request }) => guard(request) || HttpResponse.json({ terminalCount: terminals.length, onlineTerminals: 0, desktopCount: desktops.length, runningDesktops: desktops.filter(d => d.powerState === 'running').length, activeSessions: 0, failedTasks: tasks.filter(t => ['failed', 'timeout'].includes(t.status)).length, mode: 'demo' })), + ...([['desktops', desktops], ['terminals', terminals], ['users', users], ['resources', resources], ['tasks', tasks], ['audit-logs', audits]] as const).map(([path, data]) => http.get(`/api/v1/${path}`, ({ request }) => guard(request) || list(request, data as unknown[]))), + http.post('/api/v1/desktops/:id/actions', async ({ request, params }) => { const e = guard(request, true); if (e) return e; const d = desktops.find(d => d.id === params.id); if (!d) return error(404, '桌面不存在'); const b = await request.json() as { action: string }; if (!['start', 'shutdown', 'reboot'].includes(b.action)) return error(400, '不支持的操作'); return submit(request, 'desktop', b.action, d.id, d.name, () => { d.powerState = b.action === 'shutdown' ? 'stopped' : 'running'; d.updatedAt = now() }) }), + http.post('/api/v1/terminals', async ({ request }) => { const e = guard(request, true, true); if (e) return e; const b = await request.json() as { sn: string; name: string; group: string }; if (terminals.some(t => t.sn === b.sn)) return error(409, 'SN 已登记'); const id = `term_mock_${crypto.randomUUID()}`; terminals.push({ ...b, id, status: 'unregistered', agentVersion: '', lastSeen: null, sessionDesktopId: null, configVersion: 1, fps: null, rtt: null }); audit(request, 'enroll_terminal', id); return HttpResponse.json({ id, activationSecret: `mock-only-${crypto.randomUUID()}` }, { status: 201 }) }), + http.post('/api/v1/terminals/:id/commands', async ({ request, params }) => { const e = guard(request, true); if (e) return e; const t = terminals.find(t => t.id === params.id); if (!t) return error(404, '终端不存在'); const b = await request.json() as { action: string }; return submit(request, 'terminal', b.action, t.id, t.name, () => {}) }), + http.post('/api/v1/users', async ({ request }) => { const e = guard(request, true, true); if (e) return e; const b = await request.json() as { username: string; displayName: string }; if (users.some(u => u.username === b.username)) return error(409, '账号已存在'); const u: User = { ...b, id: `user_mock_${crypto.randomUUID()}`, enabled: true, terminalId: null, desktopIds: [] }; users.push(u); audit(request, 'create_user', u.id); return HttpResponse.json(u, { status: 201 }) }), + http.put('/api/v1/users/:id/bindings', async ({ request, params }) => { const e = guard(request, true, true); if (e) return e; const u = users.find(u => u.id === params.id); if (!u) return error(404, '用户不存在'); const b = await request.json() as { terminalId: string | null; desktopIds: string[]; enabled: boolean }; if (b.terminalId && users.some(x => x.id !== u.id && x.terminalId === b.terminalId)) return error(409, '终端已关联其他用户'); Object.assign(u, b); audit(request, 'update_bindings', u.id); return HttpResponse.json(u) }), + http.get('/api/v1/resources/vms', ({ request }) => guard(request) || error(503, '模拟环境未接入 Proxmox')), + http.post('/api/v1/resources/sync', ({ request }) => guard(request, true, true) || error(503, '模拟环境未接入 Proxmox')), +] diff --git a/console/src/router/index.ts b/console/src/router/index.ts new file mode 100644 index 0000000..39c340b --- /dev/null +++ b/console/src/router/index.ts @@ -0,0 +1,25 @@ +import { createRouter, createWebHistory } from 'vue-router' +import { useAuth } from '../stores/auth' + +export const router = createRouter({ history: createWebHistory(), routes: [ + { path: '/login', component: () => import('../views/Login.vue'), meta: { public: true } }, + { path: '/', component: () => import('../layouts/ConsoleLayout.vue'), children: [ + { path: '', redirect: '/overview' }, + { path: 'overview', component: () => import('../views/Overview.vue'), meta: { title: '总览' } }, + { path: 'resources', component: () => import('../views/Resources.vue'), meta: { title: '计算资源' } }, + { path: 'desktops', component: () => import('../views/Desktops.vue'), meta: { title: '桌面管理' } }, + { path: 'sessions', component: () => import('../views/Sessions.vue'), meta: { title: '远程会话' } }, + { path: 'terminals', component: () => import('../views/Terminals.vue'), meta: { title: '终端管理' } }, + { path: 'users', component: () => import('../views/Users.vue'), meta: { title: '用户与授权' } }, + { path: 'tasks', component: () => import('../views/Tasks.vue'), meta: { title: '任务与审计' } }, + { path: 'settings', component: () => import('../views/Settings.vue'), meta: { title: '系统设置', adminOnly: true } }, + { path: 'forbidden', component: () => import('../views/Forbidden.vue'), meta: { title: '无访问权限' } }, + { path: ':pathMatch(.*)*', component: () => import('../views/NotFound.vue'), meta: { title: '页面不存在' } }, + ] }, +] }) +router.beforeEach(async to => { + const auth = useAuth(); await auth.restore() + if (!to.meta.public && !auth.user) return { path: '/login', query: { redirect: to.fullPath } } + if (to.path === '/login' && auth.user) return '/overview' + if (to.meta.adminOnly && !auth.canAdmin) return '/forbidden' +}) diff --git a/console/src/stores/auth.test.ts b/console/src/stores/auth.test.ts new file mode 100644 index 0000000..9333af9 --- /dev/null +++ b/console/src/stores/auth.test.ts @@ -0,0 +1,13 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createPinia, setActivePinia } from 'pinia' +import { api, setAccessToken, type Admin } from '../api' +import { useAuth } from './auth' +vi.mock('../api', () => ({ api: { login: vi.fn(), me: vi.fn(), logout: vi.fn() }, setAccessToken: vi.fn() })) +let storage: Map +const viewer: Admin = { id: 'viewer', username: 'viewer', displayName: '只读', role: 'viewer' } +beforeEach(() => { vi.clearAllMocks(); storage = new Map(); vi.stubGlobal('sessionStorage', { getItem: (k: string) => storage.get(k) ?? null, setItem: (k: string, v: string) => storage.set(k, v), removeItem: (k: string) => storage.delete(k) }); setActivePinia(createPinia()) }) +describe('管理员会话', () => { + it('只读角色登录后仍没有写权限', async () => { vi.mocked(api.login).mockResolvedValue({ accessToken: 'jwt', expiresIn: 3600, user: viewer }); const auth = useAuth(); await auth.login('viewer', 'password'); expect(auth.canOperate).toBe(false); expect(auth.canAdmin).toBe(false); expect(storage.get('vdi.accessToken')).toBe('jwt') }) + it('同时触发恢复只请求一次;令牌失效清除会话', async () => { storage.set('vdi.accessToken', 'expired'); vi.mocked(api.me).mockRejectedValue(new Error('expired')); const auth = useAuth(); await Promise.all([auth.restore(), auth.restore()]); expect(api.me).toHaveBeenCalledTimes(1); expect(auth.user).toBeNull(); expect(storage.has('vdi.accessToken')).toBe(false); expect(setAccessToken).toHaveBeenLastCalledWith('') }) + it('退出接口失败也移除本地登录凭据', async () => { vi.mocked(api.login).mockResolvedValue({ accessToken: 'jwt', expiresIn: 3600, user: viewer }); vi.mocked(api.logout).mockRejectedValue(new Error('offline')); const auth = useAuth(); await auth.login('viewer', 'password'); await expect(auth.logout()).rejects.toThrow('offline'); expect(auth.token).toBe(''); expect(auth.user).toBeNull(); expect(storage.has('vdi.accessToken')).toBe(false) }) +}) diff --git a/console/src/stores/auth.ts b/console/src/stores/auth.ts new file mode 100644 index 0000000..cdc6a3d --- /dev/null +++ b/console/src/stores/auth.ts @@ -0,0 +1,27 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { api, setAccessToken, type Admin } from '../api' + +const tokenKey = 'vdi.accessToken' +export const useAuth = defineStore('auth', () => { + const user = ref(null) + const token = ref(sessionStorage.getItem(tokenKey) || '') + const initialized = ref(false) + let restoring: Promise | undefined + setAccessToken(token.value) + const canOperate = computed(() => !!user.value && user.value.role !== 'viewer') + const canAdmin = computed(() => user.value?.role === 'super_admin') + function clear() { user.value = null; token.value = ''; initialized.value = true; sessionStorage.removeItem(tokenKey); setAccessToken('') } + async function login(username: string, password: string) { + const data = await api.login(username, password) + token.value = data.accessToken; user.value = data.user; initialized.value = true + setAccessToken(token.value); sessionStorage.setItem(tokenKey, token.value) + } + async function restore() { + if (initialized.value) return + if (!restoring) restoring = (async () => { try { if (token.value) user.value = await api.me() } catch { clear() } finally { initialized.value = true } })() + await restoring + } + async function logout() { try { await api.logout() } finally { clear() } } + return { user, token, initialized, canOperate, canAdmin, login, restore, clear, logout } +}) diff --git a/console/src/stores/platform.ts b/console/src/stores/platform.ts new file mode 100644 index 0000000..f4e5f4f --- /dev/null +++ b/console/src/stores/platform.ts @@ -0,0 +1,29 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { api, allPages, errorMessage, type Desktop, type Terminal, type User, type Task, type Audit, type Resource, type Settings, type Overview } from '../api' + +export const usePlatform = defineStore('platform', () => { + const desktops = ref([]), terminals = ref([]), users = ref([]) + const tasks = ref([]), audits = ref([]), resources = ref([]) + const settings = ref(null), overview = ref(null) + const loading = ref(false), error = ref(''), updatedAt = ref('') + let inFlight: Promise | undefined + let generation = 0 + function refresh() { + if (inFlight) return inFlight + loading.value = !updatedAt.value + const startedGeneration = generation + inFlight = (async () => { + try { + const [d, t, u, k, a, r, s, o] = await Promise.all([allPages(api.desktops), allPages(api.terminals), allPages(api.users), allPages(api.tasks), allPages(api.audits), api.resources(), api.settings(), api.overview()]) + if (startedGeneration !== generation) return + desktops.value = d; terminals.value = t; users.value = u; tasks.value = k; audits.value = a; resources.value = r.items; settings.value = s; overview.value = o + updatedAt.value = new Date().toLocaleTimeString('zh-CN'); error.value = '' + } catch (e) { if (startedGeneration === generation) error.value = errorMessage(e) } + finally { if (startedGeneration === generation) { loading.value = false; inFlight = undefined } } + })() + return inFlight + } + function clear() { generation++; inFlight = undefined; loading.value = false; desktops.value = []; terminals.value = []; users.value = []; tasks.value = []; audits.value = []; resources.value = []; settings.value = null; overview.value = null; error.value = ''; updatedAt.value = '' } + return { desktops, terminals, users, tasks, audits, resources, settings, overview, loading, error, updatedAt, refresh, clear } +}) diff --git a/console/src/styles/tokens.css b/console/src/styles/tokens.css new file mode 100644 index 0000000..635ddf7 --- /dev/null +++ b/console/src/styles/tokens.css @@ -0,0 +1,74 @@ +:root { + --vdi-bg:#f5f7fa; --vdi-panel:#fff; --vdi-panel-2:#f0f3f7; + --vdi-text:#1b2533; --vdi-muted:#64748b; --vdi-border:#e5e9ef; + --vdi-brand:#bd713b; --vdi-accent:#965421; --vdi-accent-soft:#fff0e4; + --vdi-ok:#16845e; --vdi-warn:#9a6b10; --vdi-danger:#ce4652; + --vdi-radius:18px; --vdi-shadow:0 16px 40px rgb(27 37 51 / 6%); + --el-color-primary:var(--vdi-brand); --el-color-primary-light-3:#ce966f; + --el-color-primary-light-5:#ddb495; --el-color-primary-light-7:#ebd1bd; + --el-color-primary-light-8:#f3e2d5; --el-color-primary-light-9:#fcf3ed; + --el-color-primary-dark-2:#a9602e; + --el-color-success:#16845e; --el-color-success-light-3:#5fa98c; + --el-color-success-light-5:#91c2ad; --el-color-success-light-7:#c4ded2; + --el-color-success-light-8:#d9ebe3; --el-color-success-light-9:#eaf5ef; + --el-color-warning:#9a6b10; --el-color-warning-light-5:#cfb78b; + --el-color-warning-light-7:#e6d7bb; --el-color-warning-light-8:#efe5d4; + --el-color-warning-light-9:#faf4e8; + --el-color-danger:#ce4652; --el-color-danger-light-5:#e59ca3; + --el-color-danger-light-7:#f0c4c8; --el-color-danger-light-8:#f5dadd; + --el-color-danger-light-9:#fcf0f1; + --el-color-info:#64748b; --el-color-info-light-5:#a6b0bd; + --el-color-info-light-7:#cdd3db; --el-color-info-light-8:#e0e4e9; + --el-color-info-light-9:#f0f3f7; + font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif; + color:var(--vdi-text); background:var(--vdi-bg); font-synthesis:none; text-rendering:optimizeLegibility; +} +:root[data-theme="dark"] { + --vdi-bg:#151d27; --vdi-panel:#1d2733; --vdi-panel-2:#253241; + --vdi-text:#edf1f6; --vdi-muted:#a5b2c3; --vdi-border:#314050; + --vdi-accent:#f1ad62; --vdi-accent-soft:#493323; + --vdi-ok:#36ca94; --vdi-warn:#efb744; --vdi-danger:#f07881; + --vdi-shadow:0 18px 45px rgb(0 0 0 / 14%); + --el-color-primary-light-3:#ce966f; --el-color-primary-light-5:#88603f; + --el-color-primary-light-7:#5c422f; --el-color-primary-light-8:#493323; + --el-color-primary-light-9:#352c27; + --el-color-success:#36ca94; --el-color-success-light-3:#2fa77e; + --el-color-success-light-5:#24644f; --el-color-success-light-7:#20453f; + --el-color-success-light-8:#203a37; --el-color-success-light-9:#1d3332; + --el-color-warning:#efb744; --el-color-warning-light-5:#77643c; + --el-color-warning-light-7:#514735; --el-color-warning-light-8:#423c30; + --el-color-warning-light-9:#34332e; + --el-color-danger:#f07881; --el-color-danger-light-5:#85505a; + --el-color-danger-light-7:#5c3d47; --el-color-danger-light-8:#49333d; + --el-color-danger-light-9:#372c35; + --el-color-info:#a5b2c3; --el-color-info-light-5:#58697d; + --el-color-info-light-7:#3c4c60; --el-color-info-light-8:#314050; + --el-color-info-light-9:#253241; +} +:root { + --el-bg-color:var(--vdi-panel); --el-bg-color-page:var(--vdi-bg); --el-bg-color-overlay:var(--vdi-panel); + --el-text-color-primary:var(--vdi-text); --el-text-color-regular:var(--vdi-text); + --el-text-color-secondary:var(--vdi-muted); --el-text-color-placeholder:var(--vdi-muted); + --el-text-color-disabled:var(--vdi-muted); + --el-border-color:var(--vdi-border); --el-border-color-light:var(--vdi-border); + --el-border-color-lighter:var(--vdi-border); --el-border-color-extra-light:var(--vdi-border); + --el-border-color-dark:var(--vdi-border); --el-border-color-darker:var(--vdi-muted); + --el-fill-color:var(--vdi-panel-2); --el-fill-color-light:var(--vdi-panel-2); + --el-fill-color-lighter:var(--vdi-panel-2); --el-fill-color-extra-light:var(--vdi-panel-2); + --el-fill-color-blank:var(--vdi-panel); --el-fill-color-dark:var(--vdi-border); + --el-fill-color-darker:var(--vdi-border); --el-fill-color-disabled:var(--vdi-panel-2); + --el-disabled-bg-color:var(--vdi-panel-2); --el-disabled-text-color:var(--vdi-muted); + --el-disabled-border-color:var(--vdi-border); + --el-border-radius-base:10px; --el-border-radius-small:7px; + --el-mask-color:rgb(0 0 0 / 55%); --el-box-shadow-light:var(--vdi-shadow); + --el-box-shadow:var(--vdi-shadow); --el-font-family:inherit; +} +*{box-sizing:border-box}body{margin:0;min-width:320px}button,input{font:inherit}button:focus-visible,a:focus-visible{outline:2px solid var(--vdi-accent);outline-offset:4px}a{color:var(--vdi-accent);text-decoration:none}h1,h2,h3,p{margin:0}h1{font-size:29px;line-height:1.35;font-weight:750;letter-spacing:.4px}h2{font-size:17px;font-weight:650}h3{font-size:15px;font-weight:600} +.page-heading{display:flex;gap:16px;align-items:center;justify-content:space-between;flex-wrap:wrap;margin-bottom:25px}.subtext{font-size:13px;color:var(--vdi-muted);margin-top:7px;line-height:1.7}.eyebrow{font-size:11px;color:var(--vdi-muted);letter-spacing:1.8px;margin-bottom:9px}.panel{background:var(--vdi-panel);border:1px solid var(--vdi-border);border-radius:var(--vdi-radius);padding:22px;margin-bottom:20px;min-width:0;box-shadow:var(--vdi-shadow)}.panel-heading{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:20px}.panel-heading a{font-size:12px} +.toolbar{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-bottom:20px}.toolbar .el-input{width:260px}.toolbar .el-select{width:150px}.actions{display:flex;gap:10px;align-items:center;flex-wrap:wrap}.actions .el-button+.el-button{margin-left:0}.mono{font-family:ui-monospace,monospace;font-size:12px;overflow-wrap:anywhere}.secondary{font-size:12px;color:var(--vdi-muted);line-height:1.7}.stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:18px;margin-bottom:22px}.stat{display:block;width:100%;border:1px solid var(--vdi-border);border-radius:var(--vdi-radius);background:var(--vdi-panel);padding:22px;text-align:left;color:inherit;cursor:pointer;box-shadow:var(--vdi-shadow)}.stat:hover{border-color:var(--vdi-brand)}.stat strong{display:block;font-size:32px;font-weight:750;margin:12px 0 9px;line-height:1.2}.stat span{color:var(--vdi-muted);font-size:12px}.stat-label{font-size:13px;color:var(--vdi-muted);display:flex;justify-content:space-between;align-items:center}.stat-caption{font-size:12px;color:var(--vdi-muted)}.stat-unit{font-size:14px;font-weight:500;margin-left:4px}.two-columns{display:grid;grid-template-columns:1.35fr .85fr;gap:20px}.row{display:flex;justify-content:space-between;gap:18px;align-items:center;flex-wrap:wrap;margin:16px 0}.tone-ok{color:var(--vdi-ok)}.tone-warn{color:var(--vdi-warn)}.tone-danger{color:var(--vdi-danger)}.health-dot{display:inline-block;width:8px;height:8px;background:currentColor;border-radius:50%;margin-right:9px;flex-shrink:0} +.el-button{min-height:39px;padding:10px 16px;font-weight:500;--el-button-hover-text-color:var(--vdi-accent);--el-button-hover-bg-color:var(--vdi-accent-soft);--el-button-hover-border-color:var(--vdi-brand)}.el-button--primary{--el-button-text-color:#fff;--el-button-hover-text-color:#fff;--el-button-hover-bg-color:#ce824a;--el-button-hover-border-color:#ce824a;--el-button-active-bg-color:#a9602e;--el-button-active-border-color:#a9602e;--el-button-disabled-text-color:rgb(255 255 255 / 70%);--el-button-disabled-bg-color:#825a3d;--el-button-disabled-border-color:#825a3d}.el-button.is-link,.el-button.is-text{min-height:30px;padding:5px 8px}.el-button.is-link{color:var(--vdi-accent)}.el-button.is-disabled.is-link{color:var(--vdi-muted);opacity:.6}.el-button-group .el-button{min-height:36px}.theme-toggle.el-button{width:42px;height:42px;padding:0;flex-shrink:0;background:var(--vdi-panel-2);border-radius:11px} +.el-table{--el-table-header-bg-color:var(--vdi-panel-2);--el-table-bg-color:var(--vdi-panel);--el-table-tr-bg-color:var(--vdi-panel);--el-table-row-hover-bg-color:var(--vdi-panel-2);--el-table-border-color:var(--vdi-border);--el-table-header-text-color:var(--vdi-muted);--el-table-text-color:var(--vdi-text);border-radius:11px}.el-table th.el-table__cell{font-weight:500;padding:14px 0;font-size:12px}.el-table td.el-table__cell{padding:17px 0}.el-table .cell{padding:0 14px}.el-table__empty-text{color:var(--vdi-muted)}.el-pagination{margin-top:22px;flex-wrap:wrap;gap:8px;justify-content:flex-end;--el-pagination-button-bg-color:var(--vdi-panel-2);--el-pagination-hover-color:var(--vdi-accent)}.el-dialog{max-width:calc(100vw - 32px);border:1px solid var(--vdi-border);border-radius:18px;padding:24px}.el-dialog__title,.el-drawer__title{color:var(--vdi-text);font-weight:650}.el-drawer{max-width:100vw;border-left:1px solid var(--vdi-border)}.el-drawer__header{margin-bottom:24px;color:var(--vdi-text)}.el-drawer__body{padding:24px}.el-descriptions__cell{overflow-wrap:anywhere}.el-descriptions__label.el-descriptions__cell.is-bordered-label{background:var(--vdi-panel-2);color:var(--vdi-muted);font-weight:500}.el-descriptions__content.el-descriptions__cell.is-bordered-content{background:var(--vdi-panel);color:var(--vdi-text)}.el-input__wrapper,.el-select__wrapper{min-height:39px;border-radius:10px;background:var(--vdi-panel-2)}.el-input__wrapper.is-focus,.el-select__wrapper.is-focused{box-shadow:0 0 0 1px var(--vdi-brand) inset}.el-tabs__item.is-active,.el-tabs__item:hover{color:var(--vdi-accent)}.el-tabs__active-bar{background:var(--vdi-brand)}.el-progress-bar__outer{background:var(--vdi-panel-2)}.el-progress__text{color:var(--vdi-muted);font-size:12px!important}.el-tag{border-radius:7px;height:auto;min-height:24px;padding:3px 8px;line-height:1.4}.el-message-box{border-radius:18px;max-width:calc(100vw - 32px)}.el-alert{border-radius:12px}.el-switch{--el-switch-on-color:var(--vdi-brand)}.el-checkbox__input.is-checked+.el-checkbox__label{color:var(--vdi-accent)} +.secret{white-space:pre-wrap;overflow-wrap:anywhere;padding:16px;border:1px dashed var(--vdi-brand);background:var(--vdi-accent-soft);border-radius:10px}.notice{margin-bottom:20px}.empty-note{padding:26px 0;text-align:center;color:var(--vdi-muted)}.empty-panel{display:grid;place-items:center;min-height:230px;text-align:center;padding:32px}.empty-icon{display:grid;place-items:center;width:58px;height:58px;background:var(--vdi-panel-2);color:var(--vdi-accent);border-radius:18px;margin:0 auto 18px}.section-kicker{font-size:10px;letter-spacing:1.6px;color:var(--vdi-muted);margin-bottom:12px} +@media(min-width:1600px){.stats{gap:22px}.panel{padding:26px}} +@media(max-width:1100px){.stats{grid-template-columns:repeat(2,minmax(0,1fr))}.two-columns{grid-template-columns:1fr}} +@media(max-width:600px){h1{font-size:24px}.panel{padding:16px;border-radius:14px}.stats{gap:12px}.stat{padding:18px}.stat strong{font-size:28px}.toolbar .el-input,.toolbar .el-select{width:100%}.el-pagination{justify-content:flex-start}.el-dialog{padding:20px}.el-drawer__body{padding:18px}.page-heading{gap:14px;margin-bottom:20px}.actions{gap:8px}} diff --git a/console/src/views/Desktops.vue b/console/src/views/Desktops.vue new file mode 100644 index 0000000..bd32d24 --- /dev/null +++ b/console/src/views/Desktops.vue @@ -0,0 +1,47 @@ + + + diff --git a/console/src/views/Forbidden.vue b/console/src/views/Forbidden.vue new file mode 100644 index 0000000..3960a3b --- /dev/null +++ b/console/src/views/Forbidden.vue @@ -0,0 +1 @@ + diff --git a/console/src/views/Login.vue b/console/src/views/Login.vue new file mode 100644 index 0000000..1c06a56 --- /dev/null +++ b/console/src/views/Login.vue @@ -0,0 +1,27 @@ + + + diff --git a/console/src/views/NotFound.vue b/console/src/views/NotFound.vue new file mode 100644 index 0000000..7ed74fc --- /dev/null +++ b/console/src/views/NotFound.vue @@ -0,0 +1 @@ + diff --git a/console/src/views/Overview.vue b/console/src/views/Overview.vue new file mode 100644 index 0000000..eacbdd3 --- /dev/null +++ b/console/src/views/Overview.vue @@ -0,0 +1,47 @@ + + + diff --git a/console/src/views/Resources.vue b/console/src/views/Resources.vue new file mode 100644 index 0000000..d96e11a --- /dev/null +++ b/console/src/views/Resources.vue @@ -0,0 +1,16 @@ + + diff --git a/console/src/views/Sessions.vue b/console/src/views/Sessions.vue new file mode 100644 index 0000000..a1c3a16 --- /dev/null +++ b/console/src/views/Sessions.vue @@ -0,0 +1,30 @@ + + + diff --git a/console/src/views/Settings.vue b/console/src/views/Settings.vue new file mode 100644 index 0000000..7b97873 --- /dev/null +++ b/console/src/views/Settings.vue @@ -0,0 +1,18 @@ + + + diff --git a/console/src/views/Tasks.vue b/console/src/views/Tasks.vue new file mode 100644 index 0000000..c46b51e --- /dev/null +++ b/console/src/views/Tasks.vue @@ -0,0 +1,33 @@ + + diff --git a/console/src/views/Terminals.vue b/console/src/views/Terminals.vue new file mode 100644 index 0000000..99d45df --- /dev/null +++ b/console/src/views/Terminals.vue @@ -0,0 +1,29 @@ + + diff --git a/console/src/views/Users.vue b/console/src/views/Users.vue new file mode 100644 index 0000000..98487dd --- /dev/null +++ b/console/src/views/Users.vue @@ -0,0 +1,26 @@ + + diff --git a/console/tsconfig.json b/console/tsconfig.json new file mode 100644 index 0000000..0d4c64c --- /dev/null +++ b/console/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "compilerOptions": { + "strict": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "types": ["vite/client", "node"], + "skipLibCheck": true + }, + "include": ["src/**/*.ts", "src/**/*.vue", "vite.config.ts", "e2e/**/*.ts", "playwright.config.ts"] +} diff --git a/console/vite.config.ts b/console/vite.config.ts new file mode 100644 index 0000000..a4288d3 --- /dev/null +++ b/console/vite.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], + server: { port: 5180, strictPort: true, proxy: { '/api': process.env.VDI_API_PROXY || 'http://127.0.0.1:8088', '/healthz': process.env.VDI_API_PROXY || 'http://127.0.0.1:8088' } }, + build: { rollupOptions: { output: { manualChunks: { 'element-plus': ['element-plus'], framework: ['vue', 'vue-router', 'pinia'] } } } }, +}) diff --git a/console/vitest.config.ts b/console/vitest.config.ts new file mode 100644 index 0000000..be4f7e5 --- /dev/null +++ b/console/vitest.config.ts @@ -0,0 +1,2 @@ +import { defineConfig } from 'vitest/config' +export default defineConfig({ test: { include: ['src/**/*.test.ts'], environment: 'node' } }) diff --git a/deploy/.gitkeep b/deploy/.gitkeep deleted file mode 100644 index 728945f..0000000 --- a/deploy/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -# Keep the monorepo directory structure in Git. diff --git a/deploy/compose.dev.yml b/deploy/compose.dev.yml new file mode 100644 index 0000000..593484b --- /dev/null +++ b/deploy/compose.dev.yml @@ -0,0 +1,44 @@ +name: vdi-cloud-dev +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_USER: vdi + POSTGRES_PASSWORD: vdi-local-demo + POSTGRES_DB: vdi + ports: + - "127.0.0.1:55438:5432" + volumes: + - vdi-postgres:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U vdi -d vdi"] + interval: 3s + timeout: 3s + retries: 20 + api: + build: + context: .. + dockerfile: platform/Dockerfile + environment: + VDI_MODE: demo + VDI_LISTEN: 0.0.0.0:8080 + VDI_JWT_SECRET: local-demo-only-change-before-any-production-use-2026 + DATABASE_URL: postgres://vdi:vdi-local-demo@postgres:5432/vdi?sslmode=disable + ports: + - "127.0.0.1:${VDI_API_PORT:-8088}:8080" + depends_on: + postgres: + condition: service_healthy + init: true + console: + build: + context: .. + dockerfile: console/Dockerfile + args: + VITE_DEMO_HINT: "true" + ports: + - "127.0.0.1:${VDI_CONSOLE_PORT:-5180}:80" + depends_on: + - api +volumes: + vdi-postgres: diff --git a/deploy/compose.live.yml b/deploy/compose.live.yml new file mode 100644 index 0000000..e2c10c4 --- /dev/null +++ b/deploy/compose.live.yml @@ -0,0 +1,59 @@ +name: vdi-cloud-live +services: + postgres: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_USER: vdi + POSTGRES_PASSWORD: ${VDI_DB_PASSWORD:?required} + POSTGRES_DB: vdi_live + volumes: + - vdi-live-postgres:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U vdi -d vdi_live"] + interval: 3s + timeout: 3s + retries: 20 + api: + image: vdi-cloud-live-api + build: + context: .. + dockerfile: platform/Dockerfile + restart: unless-stopped + init: true + extra_hosts: + - "${PVE_HOSTNAME:?required}:${PVE_HOST_IP:?required}" + environment: + VDI_MODE: production + VDI_LISTEN: 0.0.0.0:8080 + VDI_JWT_SECRET: ${VDI_JWT_SECRET:?required} + VDI_ADMIN_USERNAME: ${VDI_ADMIN_USERNAME:?required} + VDI_ADMIN_PASSWORD: ${VDI_ADMIN_PASSWORD:?required} + DATABASE_URL: postgres://vdi:${VDI_DB_PASSWORD}@postgres:5432/vdi_live?sslmode=disable + PVE_URL: ${PVE_URL:?required} + PVE_TOKEN_ID: ${PVE_TOKEN_ID:?required} + PVE_TOKEN_SECRET: ${PVE_TOKEN_SECRET:?required} + PVE_CA_FILE: /run/secrets/pve-ca.pem + volumes: + - ../.local/live/pve-ca.pem:/run/secrets/pve-ca.pem:ro + depends_on: + postgres: + condition: service_healthy + console: + image: vdi-cloud-live-console + build: + context: .. + dockerfile: console/Dockerfile + args: + VITE_DEMO_HINT: "false" + restart: unless-stopped + ports: + - "127.0.0.1:${VDI_LIVE_CONSOLE_PORT:-5182}:80" + - "${VDI_LIVE_BIND_IP:?required}:${VDI_LIVE_HTTPS_PORT:-8443}:443" + volumes: + - ./nginx.live.conf:/etc/nginx/conf.d/default.conf:ro + - ../.local/live/tls:/run/secrets/tls:ro + depends_on: + - api +volumes: + vdi-live-postgres: diff --git a/deploy/nginx.conf b/deploy/nginx.conf new file mode 100644 index 0000000..e9580eb --- /dev/null +++ b/deploy/nginx.conf @@ -0,0 +1,13 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + client_max_body_size 1m; + add_header X-Content-Type-Options nosniff always; + add_header Referrer-Policy same-origin always; + add_header X-Frame-Options DENY always; + location /api/ { proxy_pass http://api:8080; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } + location /healthz { proxy_pass http://api:8080; } + location / { try_files $uri $uri/ /index.html; } +} diff --git a/deploy/nginx.live.conf b/deploy/nginx.live.conf new file mode 100644 index 0000000..70b4810 --- /dev/null +++ b/deploy/nginx.live.conf @@ -0,0 +1,17 @@ +server { + listen 80; + listen 443 ssl; + ssl_certificate /run/secrets/tls/server.pem; + ssl_certificate_key /run/secrets/tls/server-key.pem; + ssl_protocols TLSv1.2 TLSv1.3; + server_name _; + root /usr/share/nginx/html; + index index.html; + client_max_body_size 1m; + add_header X-Content-Type-Options nosniff always; + add_header Referrer-Policy same-origin always; + add_header X-Frame-Options DENY always; + location /api/ { proxy_pass http://api:8080; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; } + location /healthz { proxy_pass http://api:8080; } + location / { try_files $uri $uri/ /index.html; } +} diff --git a/docs/openapi/platform.v1.json b/docs/openapi/platform.v1.json new file mode 100644 index 0000000..2884244 --- /dev/null +++ b/docs/openapi/platform.v1.json @@ -0,0 +1,4824 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "VDI 管理平台 API", + "version": "0.1.0", + "description": "第一阶段接口。平台票据不替代 Sunshine 配对;HTTP 202 只代表已提交。" + }, + "servers": [ + { + "url": "/api/v1" + } + ], + "paths": { + "/auth/login": { + "post": { + "operationId": "login", + "security": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginInput" + } + } + } + }, + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginResult" + } + } + } + }, + "400": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "502": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/auth/me": { + "get": { + "operationId": "me", + "security": [ + { + "adminBearer": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Administrator" + } + } + } + }, + "400": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "502": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/auth/logout": { + "post": { + "operationId": "logout", + "security": [ + { + "adminBearer": [] + } + ], + "parameters": [], + "responses": { + "204": { + "description": "Successful response" + }, + "400": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "502": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/overview": { + "get": { + "operationId": "overview", + "security": [ + { + "adminBearer": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Overview" + } + } + } + }, + "400": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "502": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/settings": { + "get": { + "operationId": "settings", + "security": [ + { + "adminBearer": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Settings" + } + } + } + }, + "400": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "502": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/resources": { + "get": { + "operationId": "resources", + "security": [ + { + "adminBearer": [] + } + ], + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "pageSize", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "keyword", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "status", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceList" + } + } + } + }, + "400": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "502": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/resources/vms": { + "get": { + "operationId": "discoverVMs", + "security": [ + { + "adminBearer": [] + } + ], + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "pageSize", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "keyword", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "status", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DiscoveredVMList" + } + } + } + }, + "400": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "502": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/resources/sync": { + "post": { + "operationId": "syncResources", + "security": [ + { + "adminBearer": [] + } + ], + "parameters": [ + { + "name": "Idempotency-Key", + "in": "header", + "required": true, + "schema": { + "type": "string", + "maxLength": 100 + } + } + ], + "responses": { + "202": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Task" + } + } + } + }, + "400": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "502": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/desktops": { + "get": { + "operationId": "desktops", + "security": [ + { + "adminBearer": [] + } + ], + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "pageSize", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "keyword", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "status", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopList" + } + } + } + }, + "400": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "502": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/desktops/import": { + "post": { + "operationId": "importDesktop", + "security": [ + { + "adminBearer": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportInput" + } + } + } + }, + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Desktop" + } + } + } + }, + "400": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "502": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/desktops/{id}": { + "get": { + "operationId": "desktop", + "security": [ + { + "adminBearer": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Desktop" + } + } + } + }, + "400": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "502": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/desktops/{id}/actions": { + "post": { + "operationId": "desktopAction", + "security": [ + { + "adminBearer": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopAction" + } + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "Idempotency-Key", + "in": "header", + "required": true, + "schema": { + "type": "string", + "maxLength": 100 + } + } + ], + "responses": { + "202": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Task" + } + } + } + }, + "400": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "502": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/terminals": { + "get": { + "operationId": "terminals", + "security": [ + { + "adminBearer": [] + } + ], + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "pageSize", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "keyword", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "status", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TerminalList" + } + } + } + }, + "400": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "502": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "operationId": "enrollTerminal", + "security": [ + { + "adminBearer": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EnrollmentInput" + } + } + } + }, + "parameters": [], + "responses": { + "201": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Enrollment" + } + } + } + }, + "400": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "502": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/terminals/{id}/commands": { + "post": { + "operationId": "terminalCommand", + "security": [ + { + "adminBearer": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TerminalAction" + } + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "Idempotency-Key", + "in": "header", + "required": true, + "schema": { + "type": "string", + "maxLength": 100 + } + } + ], + "responses": { + "202": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Task" + } + } + } + }, + "400": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "502": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/users": { + "get": { + "operationId": "users", + "security": [ + { + "adminBearer": [] + } + ], + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "pageSize", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "keyword", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "status", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserList" + } + } + } + }, + "400": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "502": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "operationId": "createUser", + "security": [ + { + "adminBearer": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateUserInput" + } + } + } + }, + "parameters": [], + "responses": { + "201": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "400": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "502": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/users/{id}/bindings": { + "put": { + "operationId": "updateBindings", + "security": [ + { + "adminBearer": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BindingInput" + } + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "400": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "502": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/tasks": { + "get": { + "operationId": "tasks", + "security": [ + { + "adminBearer": [] + } + ], + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "pageSize", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "keyword", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "status", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TaskList" + } + } + } + }, + "400": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "502": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/tasks/{id}": { + "get": { + "operationId": "task", + "security": [ + { + "adminBearer": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Task" + } + } + } + }, + "400": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "502": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/tasks/{id}/diagnostics": { + "get": { + "operationId": "taskDiagnostics", + "security": [ + { + "adminBearer": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeviceDiagnostics" + } + } + } + }, + "400": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "502": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/audit-logs": { + "get": { + "operationId": "audits", + "security": [ + { + "adminBearer": [] + } + ], + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "pageSize", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "keyword", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "status", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuditList" + } + } + } + }, + "400": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "502": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/terminal/register": { + "post": { + "operationId": "registerDevice", + "security": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegisterInput" + } + } + } + }, + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegisterResult" + } + } + } + }, + "400": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "502": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/terminal/heartbeat": { + "post": { + "operationId": "heartbeat", + "security": [ + { + "deviceBearer": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeartbeatInput" + } + } + } + }, + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeartbeatResult" + } + } + } + }, + "400": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "502": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/terminal/config": { + "get": { + "operationId": "deviceConfig", + "security": [ + { + "deviceBearer": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeviceConfig" + } + } + } + }, + "400": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "502": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/terminal/desktops": { + "get": { + "operationId": "deviceDesktops", + "security": [ + { + "deviceBearer": [] + } + ], + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "pageSize", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "keyword", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "status", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DesktopList" + } + } + } + }, + "400": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "502": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/terminal/commands": { + "get": { + "operationId": "deviceCommands", + "security": [ + { + "deviceBearer": [] + } + ], + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "pageSize", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "keyword", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "status", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TaskList" + } + } + } + }, + "400": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "502": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/terminal/commands/{id}/ack": { + "post": { + "operationId": "deviceAck", + "security": [ + { + "deviceBearer": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AckInput" + } + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Accepted" + } + } + } + }, + "400": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "502": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/terminal/connect-ticket": { + "post": { + "operationId": "createTicket", + "security": [ + { + "deviceBearer": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TicketInput" + } + } + } + }, + "parameters": [], + "responses": { + "201": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ticket" + } + } + } + }, + "400": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "502": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/terminal/connect-ticket/consume": { + "post": { + "operationId": "consumeTicket", + "security": [ + { + "deviceBearer": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConsumeInput" + } + } + } + }, + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Connection" + } + } + } + }, + "400": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "502": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/terminal/session": { + "post": { + "operationId": "deviceSession", + "security": [ + { + "deviceBearer": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionInput" + } + } + } + }, + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Accepted" + } + } + } + }, + "400": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "502": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Request failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Error": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "requestId": { + "type": "string" + } + }, + "required": [ + "code", + "message", + "requestId" + ], + "additionalProperties": false + }, + "Administrator": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "username": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "role": { + "type": "string", + "enum": [ + "super_admin", + "operator", + "viewer" + ] + } + }, + "required": [ + "id", + "username", + "displayName", + "role" + ], + "additionalProperties": false + }, + "LoginInput": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "password": { + "type": "string" + } + }, + "required": [ + "username", + "password" + ], + "additionalProperties": false + }, + "LoginResult": { + "type": "object", + "properties": { + "accessToken": { + "type": "string" + }, + "expiresIn": { + "type": "integer" + }, + "user": { + "$ref": "#/components/schemas/Administrator" + } + }, + "required": [ + "accessToken", + "expiresIn", + "user" + ], + "additionalProperties": false + }, + "Desktop": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "os": { + "type": "string", + "enum": [ + "Windows", + "Ubuntu", + "麒麟" + ] + }, + "node": { + "type": "string" + }, + "vmid": { + "type": "integer" + }, + "pool": { + "type": "string" + }, + "powerState": { + "type": "string" + }, + "serviceState": { + "type": "string" + }, + "provider": { + "type": "string", + "enum": [ + "demo", + "proxmox" + ] + }, + "endpoint": { + "type": "string" + }, + "app": { + "type": "string" + }, + "width": { + "type": "integer" + }, + "height": { + "type": "integer" + }, + "fps": { + "type": "integer" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "name", + "os", + "node", + "vmid", + "pool", + "powerState", + "serviceState", + "provider", + "endpoint", + "app", + "width", + "height", + "fps", + "updatedAt" + ], + "additionalProperties": false + }, + "Terminal": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "sn": { + "type": "string" + }, + "name": { + "type": "string" + }, + "group": { + "type": "string" + }, + "status": { + "type": "string" + }, + "agentVersion": { + "type": "string" + }, + "lastSeen": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "sessionDesktopId": { + "type": "string", + "nullable": true + }, + "configVersion": { + "type": "integer" + }, + "fps": { + "type": "number", + "nullable": true + }, + "rtt": { + "type": "number", + "nullable": true + }, + "sessionState": { + "type": "string" + } + }, + "required": [ + "id", + "sn", + "name", + "group", + "status", + "agentVersion", + "lastSeen", + "sessionDesktopId", + "configVersion", + "fps", + "rtt" + ], + "additionalProperties": false + }, + "User": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "username": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "terminalId": { + "type": "string", + "nullable": true + }, + "desktopIds": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "id", + "username", + "displayName", + "enabled", + "terminalId", + "desktopIds" + ], + "additionalProperties": false + }, + "Resource": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string" + }, + "cpu": { + "type": "number" + }, + "memory": { + "type": "number" + }, + "storage": { + "type": "number" + }, + "provider": { + "type": "string" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "name", + "status", + "cpu", + "memory", + "storage", + "provider", + "updatedAt" + ], + "additionalProperties": false + }, + "Task": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "action": { + "type": "string" + }, + "targetId": { + "type": "string" + }, + "targetName": { + "type": "string" + }, + "actor": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "queued", + "running", + "success", + "failed", + "timeout" + ] + }, + "message": { + "type": "string" + }, + "providerRef": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "kind", + "action", + "targetId", + "targetName", + "actor", + "status", + "message", + "providerRef", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + }, + "Audit": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "actor": { + "type": "string" + }, + "action": { + "type": "string" + }, + "target": { + "type": "string" + }, + "result": { + "type": "string" + }, + "requestId": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "actor", + "action", + "target", + "result", + "requestId", + "createdAt" + ], + "additionalProperties": false + }, + "Overview": { + "type": "object", + "properties": { + "terminalCount": { + "type": "integer" + }, + "onlineTerminals": { + "type": "integer" + }, + "desktopCount": { + "type": "integer" + }, + "runningDesktops": { + "type": "integer" + }, + "activeSessions": { + "type": "integer" + }, + "failedTasks": { + "type": "integer" + }, + "mode": { + "type": "string" + } + }, + "required": [ + "terminalCount", + "onlineTerminals", + "desktopCount", + "runningDesktops", + "activeSessions", + "failedTasks", + "mode" + ], + "additionalProperties": false + }, + "Settings": { + "type": "object", + "properties": { + "mode": { + "type": "string", + "enum": [ + "demo", + "production" + ] + }, + "computeProvider": { + "type": "string" + }, + "pveConfigured": { + "type": "boolean" + }, + "terminalTransport": { + "type": "string" + }, + "capabilities": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "mode", + "computeProvider", + "pveConfigured", + "terminalTransport", + "capabilities" + ], + "additionalProperties": false + }, + "ImportInput": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "os": { + "type": "string", + "enum": [ + "Windows", + "Ubuntu", + "麒麟" + ] + }, + "node": { + "type": "string" + }, + "vmid": { + "type": "integer" + }, + "endpoint": { + "type": "string" + }, + "width": { + "type": "integer" + }, + "height": { + "type": "integer" + }, + "fps": { + "type": "integer" + } + }, + "required": [ + "name", + "os", + "node", + "vmid" + ], + "additionalProperties": false + }, + "DiscoveredVM": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "node": { + "type": "string" + }, + "vmid": { + "type": "integer" + }, + "status": { + "type": "string" + } + }, + "required": [ + "name", + "node", + "vmid", + "status" + ], + "additionalProperties": false + }, + "EnrollmentInput": { + "type": "object", + "properties": { + "sn": { + "type": "string" + }, + "name": { + "type": "string" + }, + "group": { + "type": "string" + } + }, + "required": [ + "sn", + "name", + "group" + ], + "additionalProperties": false + }, + "Enrollment": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "activationSecret": { + "type": "string" + } + }, + "required": [ + "id", + "activationSecret" + ], + "additionalProperties": false + }, + "CreateUserInput": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "displayName": { + "type": "string" + } + }, + "required": [ + "username", + "displayName" + ], + "additionalProperties": false + }, + "BindingInput": { + "type": "object", + "properties": { + "terminalId": { + "type": "string", + "nullable": true + }, + "desktopIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "terminalId", + "desktopIds", + "enabled" + ], + "additionalProperties": false + }, + "DesktopAction": { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": [ + "start", + "shutdown", + "reboot" + ] + } + }, + "required": [ + "action" + ], + "additionalProperties": false + }, + "TerminalAction": { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": [ + "reboot", + "reset-session", + "collect-logs" + ] + } + }, + "required": [ + "action" + ], + "additionalProperties": false + }, + "RegisterInput": { + "type": "object", + "properties": { + "sn": { + "type": "string" + }, + "activationSecret": { + "type": "string" + }, + "agentVersion": { + "type": "string" + } + }, + "required": [ + "sn", + "activationSecret", + "agentVersion" + ], + "additionalProperties": false + }, + "RegisterResult": { + "type": "object", + "properties": { + "terminalId": { + "type": "string" + }, + "accessToken": { + "type": "string" + }, + "heartbeatInterval": { + "type": "integer" + } + }, + "required": [ + "terminalId", + "accessToken", + "heartbeatInterval" + ], + "additionalProperties": false + }, + "HeartbeatInput": { + "type": "object", + "properties": { + "agentVersion": { + "type": "string" + }, + "fps": { + "type": "number", + "nullable": true + }, + "rtt": { + "type": "number", + "nullable": true + } + }, + "required": [ + "agentVersion" + ], + "additionalProperties": false + }, + "Accepted": { + "type": "object", + "properties": { + "accepted": { + "type": "boolean" + } + }, + "required": [ + "accepted" + ], + "additionalProperties": false + }, + "HeartbeatResult": { + "type": "object", + "properties": { + "accepted": { + "type": "boolean" + }, + "serverTime": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "accepted", + "serverTime" + ], + "additionalProperties": false + }, + "DeviceConfig": { + "type": "object", + "properties": { + "configVersion": { + "type": "integer" + }, + "identityMode": { + "type": "string" + }, + "commandTransport": { + "type": "string" + }, + "heartbeatInterval": { + "type": "integer" + } + }, + "required": [ + "configVersion", + "identityMode", + "commandTransport", + "heartbeatInterval" + ], + "additionalProperties": false + }, + "TicketInput": { + "type": "object", + "properties": { + "desktopId": { + "type": "string" + } + }, + "required": [ + "desktopId" + ], + "additionalProperties": false + }, + "Ticket": { + "type": "object", + "properties": { + "ticket": { + "type": "string" + }, + "desktopId": { + "type": "string" + }, + "expiresAt": { + "type": "string", + "format": "date-time" + }, + "credentialMode": { + "type": "string" + } + }, + "required": [ + "ticket", + "desktopId", + "expiresAt", + "credentialMode" + ], + "additionalProperties": false + }, + "ConsumeInput": { + "type": "object", + "properties": { + "ticket": { + "type": "string" + } + }, + "required": [ + "ticket" + ], + "additionalProperties": false + }, + "Connection": { + "type": "object", + "properties": { + "desktopId": { + "type": "string" + }, + "sessionId": { + "type": "string" + }, + "protocol": { + "type": "string" + }, + "endpoint": { + "type": "string" + }, + "app": { + "type": "string" + }, + "width": { + "type": "integer" + }, + "height": { + "type": "integer" + }, + "fps": { + "type": "integer" + }, + "credentialMode": { + "type": "string" + } + }, + "required": [ + "desktopId", + "sessionId", + "protocol", + "endpoint", + "app", + "width", + "height", + "fps", + "credentialMode" + ], + "additionalProperties": false + }, + "DeviceDiagnostics": { + "type": "object", + "properties": { + "agentVersion": { + "type": "string" + }, + "bootId": { + "type": "string" + }, + "uptimeSeconds": { + "type": "number" + }, + "kioskState": { + "type": "string" + }, + "logTail": { + "type": "string" + } + }, + "required": [ + "agentVersion", + "bootId", + "uptimeSeconds", + "kioskState", + "logTail" + ], + "additionalProperties": false + }, + "SessionInput": { + "type": "object", + "properties": { + "sessionId": { + "type": "string" + }, + "state": { + "type": "string", + "enum": [ + "connected", + "ended" + ] + } + }, + "required": [ + "sessionId", + "state" + ], + "additionalProperties": false + }, + "AckInput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "success", + "failed" + ] + }, + "message": { + "type": "string" + }, + "diagnostics": { + "$ref": "#/components/schemas/DeviceDiagnostics" + } + }, + "required": [ + "status" + ], + "additionalProperties": false + }, + "DesktopList": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Desktop" + } + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "requestId": { + "type": "string" + } + }, + "required": [ + "items", + "page", + "pageSize", + "total", + "requestId" + ], + "additionalProperties": false + }, + "TerminalList": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Terminal" + } + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "requestId": { + "type": "string" + } + }, + "required": [ + "items", + "page", + "pageSize", + "total", + "requestId" + ], + "additionalProperties": false + }, + "UserList": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "requestId": { + "type": "string" + } + }, + "required": [ + "items", + "page", + "pageSize", + "total", + "requestId" + ], + "additionalProperties": false + }, + "ResourceList": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Resource" + } + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "requestId": { + "type": "string" + } + }, + "required": [ + "items", + "page", + "pageSize", + "total", + "requestId" + ], + "additionalProperties": false + }, + "TaskList": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Task" + } + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "requestId": { + "type": "string" + } + }, + "required": [ + "items", + "page", + "pageSize", + "total", + "requestId" + ], + "additionalProperties": false + }, + "AuditList": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Audit" + } + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "requestId": { + "type": "string" + } + }, + "required": [ + "items", + "page", + "pageSize", + "total", + "requestId" + ], + "additionalProperties": false + }, + "DiscoveredVMList": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DiscoveredVM" + } + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "requestId": { + "type": "string" + } + }, + "required": [ + "items", + "page", + "pageSize", + "total", + "requestId" + ], + "additionalProperties": false + } + }, + "securitySchemes": { + "adminBearer": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + }, + "deviceBearer": { + "type": "http", + "scheme": "bearer", + "description": "设备激活后获得的独立令牌,不是管理员 JWT" + } + } + } +} diff --git a/platform/.env.example b/platform/.env.example new file mode 100644 index 0000000..402a027 --- /dev/null +++ b/platform/.env.example @@ -0,0 +1,11 @@ +VDI_MODE=production +VDI_LISTEN=127.0.0.1:8080 +DATABASE_URL=postgres://vdi:replace-me@127.0.0.1:5432/vdi?sslmode=require +VDI_JWT_SECRET=replace-with-at-least-32-random-characters +VDI_ADMIN_USERNAME=admin +VDI_ADMIN_PASSWORD=replace-with-strong-initial-password +# PVE_URL is the HTTPS origin. CA verification is always enabled. +# PVE_URL=https://pve.example.internal:8006 +# PVE_TOKEN_ID=vdi@pve!console +# PVE_TOKEN_SECRET=replace-me +# PVE_CA_FILE=/run/secrets/pve-ca.pem diff --git a/platform/.gitkeep b/platform/.gitkeep deleted file mode 100644 index 728945f..0000000 --- a/platform/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -# Keep the monorepo directory structure in Git. diff --git a/platform/Dockerfile b/platform/Dockerfile new file mode 100644 index 0000000..c073d87 --- /dev/null +++ b/platform/Dockerfile @@ -0,0 +1,13 @@ +FROM golang:1.27-alpine AS build +WORKDIR /src +COPY platform/go.mod platform/go.sum ./ +RUN go mod download +COPY platform/ ./ +RUN CGO_ENABLED=0 go build -trimpath -o /vdi-api ./cmd/api-gateway + +FROM alpine:3.23 +RUN apk add --no-cache ca-certificates && adduser -D -u 10001 vdi +COPY --from=build /vdi-api /usr/local/bin/vdi-api +USER vdi +EXPOSE 8080 +ENTRYPOINT ["vdi-api"] diff --git a/platform/README.md b/platform/README.md new file mode 100644 index 0000000..c8f8671 --- /dev/null +++ b/platform/README.md @@ -0,0 +1,14 @@ +# VDI 云端服务 + +Go API 网关和第一阶段业务模块。运行、接口、真实 PVE 接入与当前限制见[开发说明](../docs/cloud-platform-development.md)。 + +```text +cmd/api-gateway/ 服务入口与优雅退出 +internal/config/ 环境配置校验 +internal/model/ 对外数据类型 +internal/store/ PostgreSQL 初始化、账号与业务存储 +internal/server/ 认证、资源/桌面、终端、授权、任务与审计 +pkg/proxmox/ 真实 PVE API 适配与异步任务跟踪 +``` + +配置参考 `.env.example`,变量由运行环境注入;程序不会自动读取 `.env`。不要提交访问凭据。 diff --git a/platform/cmd/api-gateway/main.go b/platform/cmd/api-gateway/main.go new file mode 100644 index 0000000..7091b42 --- /dev/null +++ b/platform/cmd/api-gateway/main.go @@ -0,0 +1,58 @@ +package main + +import ( + "context" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + "vdi-cloud/platform/internal/config" + "vdi-cloud/platform/internal/server" + "vdi-cloud/platform/internal/store" + "vdi-cloud/platform/pkg/proxmox" +) + +func main() { + cfg, err := config.Load() + if err != nil { + log.Fatal(err) + } + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + startup, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + db, err := store.Open(startup, cfg.DatabaseURL) + if err != nil { + log.Fatal("database initialization failed: ", err) + } + defer db.Close() + if err = store.Bootstrap(startup, db, cfg.Mode, cfg.AdminUsername, cfg.AdminPassword); err != nil { + log.Fatal("bootstrap failed: ", err) + } + var pve *proxmox.Client + if cfg.PVEURL != "" { + pve, err = proxmox.New(cfg.PVEURL, cfg.PVETokenID, cfg.PVETokenSecret, cfg.PVECAFile) + if err != nil { + log.Fatal(err) + } + } + s := server.New(cfg, db, pve) + go s.RunWorker(ctx) + if pve != nil { + go s.RunSync(ctx) + } + httpServer := &http.Server{Addr: cfg.Address, Handler: s.Router, ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 20 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 60 * time.Second} + go func() { + log.Printf("VDI API listening on %s (mode=%s)", cfg.Address, cfg.Mode) + if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Print(err) + stop() + } + }() + <-ctx.Done() + shutdown, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second) + defer shutdownCancel() + _ = httpServer.Shutdown(shutdown) +} diff --git a/platform/go.mod b/platform/go.mod new file mode 100644 index 0000000..0346f59 --- /dev/null +++ b/platform/go.mod @@ -0,0 +1,47 @@ +module vdi-cloud/platform + +go 1.25.0 + +require ( + github.com/gin-gonic/gin v1.11.0 + github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/jackc/pgx/v5 v5.7.6 + github.com/jmoiron/sqlx v1.4.0 + golang.org/x/crypto v0.43.0 +) + +require ( + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.54.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + go.uber.org/mock v0.5.0 // indirect + golang.org/x/arch v0.20.0 // indirect + golang.org/x/mod v0.28.0 // indirect + golang.org/x/net v0.45.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/tools v0.37.0 // indirect + google.golang.org/protobuf v1.36.9 // indirect +) diff --git a/platform/go.sum b/platform/go.sum new file mode 100644 index 0000000..1aa5e4f --- /dev/null +++ b/platform/go.sum @@ -0,0 +1,109 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= +github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk= +github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= +github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= +golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= +golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/platform/internal/config/config.go b/platform/internal/config/config.go new file mode 100644 index 0000000..57c1b4f --- /dev/null +++ b/platform/internal/config/config.go @@ -0,0 +1,39 @@ +package config + +import ( + "fmt" + "os" + "strings" +) + +type Config struct { + Mode, Address, DatabaseURL, JWTSecret, AdminUsername, AdminPassword string + PVEURL, PVETokenID, PVETokenSecret, PVECAFile string +} + +func Load() (Config, error) { + c := Config{Mode: env("VDI_MODE", "production"), Address: env("VDI_LISTEN", "127.0.0.1:8080"), DatabaseURL: os.Getenv("DATABASE_URL"), JWTSecret: os.Getenv("VDI_JWT_SECRET"), AdminUsername: os.Getenv("VDI_ADMIN_USERNAME"), AdminPassword: os.Getenv("VDI_ADMIN_PASSWORD"), PVEURL: os.Getenv("PVE_URL"), PVETokenID: os.Getenv("PVE_TOKEN_ID"), PVETokenSecret: os.Getenv("PVE_TOKEN_SECRET"), PVECAFile: os.Getenv("PVE_CA_FILE")} + if c.Mode != "demo" && c.Mode != "production" { + return c, fmt.Errorf("VDI_MODE must be demo or production") + } + if c.DatabaseURL == "" { + return c, fmt.Errorf("DATABASE_URL is required") + } + if len(c.JWTSecret) < 32 { + return c, fmt.Errorf("VDI_JWT_SECRET must contain at least 32 characters") + } + if c.Mode == "production" && strings.TrimSpace(c.AdminUsername) == "" { + return c, fmt.Errorf("production requires VDI_ADMIN_USERNAME") + } + if c.PVEURL != "" && (c.PVETokenID == "" || c.PVETokenSecret == "") { + return c, fmt.Errorf("PVE_TOKEN_ID and PVE_TOKEN_SECRET are required with PVE_URL") + } + return c, nil +} + +func env(key, fallback string) string { + if v := os.Getenv(key); v != "" { + return v + } + return fallback +} diff --git a/platform/internal/model/model.go b/platform/internal/model/model.go new file mode 100644 index 0000000..1fbb6a1 --- /dev/null +++ b/platform/internal/model/model.go @@ -0,0 +1,91 @@ +package model + +import "time" + +type Administrator struct { + ID string `json:"id" db:"id"` + Username string `json:"username" db:"username"` + DisplayName string `json:"displayName" db:"display_name"` + Role string `json:"role" db:"role"` + PasswordHash string `json:"-" db:"password_hash"` +} +type Desktop struct { + ID string `json:"id" db:"id"` + Name string `json:"name" db:"name"` + OS string `json:"os" db:"os"` + Node string `json:"node" db:"node"` + VMID int `json:"vmid" db:"vmid"` + Pool string `json:"pool" db:"pool"` + PowerState string `json:"powerState" db:"power_state"` + ServiceState string `json:"serviceState" db:"service_state"` + Provider string `json:"provider" db:"provider"` + Endpoint string `json:"endpoint" db:"endpoint"` + App string `json:"app" db:"app"` + Width int `json:"width" db:"width"` + Height int `json:"height" db:"height"` + FPS int `json:"fps" db:"fps"` + UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` +} +type Terminal struct { + ID string `json:"id" db:"id"` + SN string `json:"sn" db:"sn"` + Name string `json:"name" db:"name"` + Group string `json:"group" db:"group_name"` + Status string `json:"status" db:"status"` + AgentVersion string `json:"agentVersion" db:"agent_version"` + LastSeen *time.Time `json:"lastSeen" db:"last_seen"` + SessionDesktopID *string `json:"sessionDesktopId" db:"session_desktop_id"` + SessionState string `json:"sessionState" db:"session_state"` + ConfigVersion int `json:"configVersion" db:"config_version"` + FPS *float64 `json:"fps" db:"fps"` + RTT *float64 `json:"rtt" db:"rtt"` +} +type User struct { + ID string `json:"id" db:"id"` + Username string `json:"username" db:"username"` + DisplayName string `json:"displayName" db:"display_name"` + Enabled bool `json:"enabled" db:"enabled"` + TerminalID *string `json:"terminalId" db:"terminal_id"` + DesktopIDs []string `json:"desktopIds"` +} +type Resource struct { + ID string `json:"id" db:"id"` + Name string `json:"name" db:"name"` + Status string `json:"status" db:"status"` + CPU float64 `json:"cpu" db:"cpu"` + Memory float64 `json:"memory" db:"memory"` + Storage float64 `json:"storage" db:"storage"` + Provider string `json:"provider" db:"provider"` + UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` +} +type Task struct { + ID string `json:"id" db:"id"` + Kind string `json:"kind" db:"kind"` + Action string `json:"action" db:"action"` + TargetID string `json:"targetId" db:"target_id"` + TargetName string `json:"targetName" db:"target_name"` + Actor string `json:"actor" db:"actor"` + Status string `json:"status" db:"status"` + Message string `json:"message" db:"message"` + ProviderRef string `json:"providerRef" db:"provider_ref"` + CreatedAt time.Time `json:"createdAt" db:"created_at"` + UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` + IdempotencyKey string `json:"-" db:"idempotency_key"` +} +type Audit struct { + ID string `json:"id" db:"id"` + Actor string `json:"actor" db:"actor"` + Action string `json:"action" db:"action"` + Target string `json:"target" db:"target"` + Result string `json:"result" db:"result"` + RequestID string `json:"requestId" db:"request_id"` + CreatedAt time.Time `json:"createdAt" db:"created_at"` +} + +type DeviceDiagnostics struct { + AgentVersion string `json:"agentVersion" binding:"required,max=80"` + BootID string `json:"bootId" binding:"required,max=80"` + UptimeSeconds float64 `json:"uptimeSeconds" binding:"min=0"` + KioskState string `json:"kioskState" binding:"required,max=80"` + LogTail string `json:"logTail" binding:"max=16384"` +} diff --git a/platform/internal/server/admin.go b/platform/internal/server/admin.go new file mode 100644 index 0000000..9b32bcd --- /dev/null +++ b/platform/internal/server/admin.go @@ -0,0 +1,553 @@ +package server + +import ( + "context" + "database/sql" + "errors" + "fmt" + "github.com/gin-gonic/gin" + "github.com/jackc/pgx/v5/pgconn" + "log" + "net" + "strings" + "time" + "vdi-cloud/platform/internal/model" + "vdi-cloud/platform/internal/store" + "vdi-cloud/platform/pkg/proxmox" +) + +func (s *Server) overview(c *gin.Context) { + var counts struct { + Terminals int `db:"terminals"` + Online int `db:"online"` + Desktops int `db:"desktops"` + Running int `db:"running"` + Sessions int `db:"sessions"` + Failed int `db:"failed"` + } + err := s.DB.GetContext(c.Request.Context(), &counts, `SELECT (SELECT count(*) FROM terminals) terminals,(SELECT count(*) FROM terminals WHERE last_seen>now()-interval '90 seconds') online,(SELECT count(*) FROM desktops) desktops,(SELECT count(*) FROM desktops WHERE power_state='running') running,(SELECT count(*) FROM terminals WHERE session_state='connected' AND session_desktop_id IS NOT NULL AND last_seen>now()-interval '90 seconds') sessions,(SELECT count(*) FROM tasks WHERE status IN ('failed','timeout') AND created_at>now()-interval '24 hours') failed`) + if err != nil { + s.dbError(c, err) + return + } + c.JSON(200, gin.H{"terminalCount": counts.Terminals, "onlineTerminals": counts.Online, "desktopCount": counts.Desktops, "runningDesktops": counts.Running, "activeSessions": counts.Sessions, "failedTasks": counts.Failed, "mode": s.Config.Mode}) +} +func (s *Server) settings(c *gin.Context) { + provider := "unconfigured" + if s.Config.Mode == "demo" { + provider = "demo" + } + if s.PVE != nil { + provider = "proxmox" + } + c.JSON(200, gin.H{"mode": s.Config.Mode, "computeProvider": provider, "pveConfigured": s.PVE != nil, "terminalTransport": "https-poll", "capabilities": []string{"auth", "resources", "desktops", "terminals", "bindings", "tasks", "audit"}}) +} +func (s *Server) resources(c *gin.Context) { + items := []model.Resource{} + if err := s.DB.SelectContext(c.Request.Context(), &items, "SELECT * FROM resources ORDER BY name"); err != nil { + s.dbError(c, err) + return + } + list(c, items) +} +func (s *Server) desktops(c *gin.Context) { + items := []model.Desktop{} + q := "%" + c.Query("keyword") + "%" + err := s.DB.SelectContext(c.Request.Context(), &items, `SELECT * FROM desktops WHERE (name ILIKE $1 OR vmid::text ILIKE $1) AND ($2='' OR power_state=$2) ORDER BY name`, q, c.Query("status")) + if err != nil { + s.dbError(c, err) + return + } + list(c, items) +} +func (s *Server) desktop(c *gin.Context) { + var d model.Desktop + if err := s.DB.GetContext(c.Request.Context(), &d, "SELECT * FROM desktops WHERE id=$1", c.Param("id")); err != nil { + s.dbError(c, err) + return + } + c.JSON(200, d) +} +func (s *Server) terminals(c *gin.Context) { + items := []model.Terminal{} + err := s.DB.SelectContext(c.Request.Context(), &items, `SELECT id,sn,name,group_name,CASE WHEN last_seen>now()-interval '90 seconds' THEN 'online' WHEN status='unregistered' THEN 'unregistered' ELSE 'offline' END status,agent_version,last_seen,CASE WHEN last_seen>now()-interval '90 seconds' THEN session_desktop_id ELSE NULL END session_desktop_id,session_state,config_version,fps,rtt FROM terminals WHERE (name ILIKE $1 OR sn ILIKE $1) ORDER BY name`, "%"+c.Query("keyword")+"%") + if err != nil { + s.dbError(c, err) + return + } + list(c, items) +} +func (s *Server) users(c *gin.Context) { + items, err := store.Users(c.Request.Context(), s.DB) + if err != nil { + s.dbError(c, err) + return + } + list(c, items) +} +func (s *Server) createUser(c *gin.Context) { + var in struct { + Username string `json:"username" binding:"required,min=2,max=80"` + DisplayName string `json:"displayName" binding:"required,max=80"` + } + if !s.decode(c, &in) { + return + } + in.Username = strings.TrimSpace(in.Username) + in.DisplayName = strings.TrimSpace(in.DisplayName) + if in.Username == "" || in.DisplayName == "" { + s.fail(c, 400, "invalid_request", "账号和姓名不能为空") + return + } + id := store.ID("user_") + _, err := s.DB.ExecContext(c.Request.Context(), "INSERT INTO users(id,username,display_name) VALUES($1,$2,$3)", id, in.Username, in.DisplayName) + if err != nil { + if conflict(err) { + s.fail(c, 409, "conflict", "该账号已存在") + } else { + s.dbError(c, err) + } + return + } + s.log(c, "create_user", id, "success") + c.JSON(201, model.User{ID: id, Username: in.Username, DisplayName: in.DisplayName, Enabled: true, DesktopIDs: []string{}}) +} +func conflict(err error) bool { + var e *pgconn.PgError + return errors.As(err, &e) && (e.Code == "23505" || e.Code == "23503") +} +func (s *Server) bindings(c *gin.Context) { + var in struct { + TerminalID *string `json:"terminalId"` + DesktopIDs []string `json:"desktopIds" binding:"max=100"` + Enabled *bool `json:"enabled"` + } + if !s.decode(c, &in) { + return + } + if in.TerminalID != nil && *in.TerminalID == "" { + in.TerminalID = nil + } + ctx := c.Request.Context() + tx, err := s.DB.BeginTxx(ctx, nil) + if err != nil { + s.dbError(c, err) + return + } + defer tx.Rollback() + var user model.User + if err = tx.GetContext(ctx, &user, "SELECT * FROM users WHERE id=$1 FOR UPDATE", c.Param("id")); err != nil { + s.dbError(c, err) + return + } + if in.Enabled != nil { + user.Enabled = *in.Enabled + } + _, err = tx.ExecContext(ctx, "UPDATE users SET terminal_id=$1,enabled=$2 WHERE id=$3", in.TerminalID, user.Enabled, user.ID) + if err != nil { + if conflict(err) { + s.fail(c, 409, "binding_conflict", "终端不存在或已关联其他用户") + } else { + s.dbError(c, err) + } + return + } + if _, err = tx.ExecContext(ctx, "DELETE FROM bindings WHERE user_id=$1", user.ID); err != nil { + s.dbError(c, err) + return + } + seen := map[string]bool{} + for _, id := range in.DesktopIDs { + if seen[id] { + continue + } + seen[id] = true + if _, err = tx.ExecContext(ctx, "INSERT INTO bindings VALUES($1,$2)", user.ID, id); err != nil { + if conflict(err) { + s.fail(c, 400, "invalid_desktop", "所选桌面不存在") + } else { + s.dbError(c, err) + } + return + } + } + _, err = tx.ExecContext(ctx, "INSERT INTO audit_logs VALUES($1,$2,'update_bindings',$3,'success',$4,now())", store.ID("audit_"), s.admin(c).Username, user.ID, c.GetString("requestId")) + if err == nil { + err = tx.Commit() + } + if err != nil { + s.dbError(c, err) + return + } + user.TerminalID = in.TerminalID + user.DesktopIDs = in.DesktopIDs + if user.DesktopIDs == nil { + user.DesktopIDs = []string{} + } + c.JSON(200, user) +} +func (s *Server) tasks(c *gin.Context) { + items := []model.Task{} + if err := s.DB.SelectContext(c.Request.Context(), &items, "SELECT * FROM tasks ORDER BY created_at DESC LIMIT 1000"); err != nil { + s.dbError(c, err) + return + } + list(c, items) +} +func (s *Server) task(c *gin.Context) { + var t model.Task + if err := s.DB.GetContext(c.Request.Context(), &t, "SELECT * FROM tasks WHERE id=$1", c.Param("id")); err != nil { + s.dbError(c, err) + return + } + c.JSON(200, t) +} +func (s *Server) audits(c *gin.Context) { + items := []model.Audit{} + if err := s.DB.SelectContext(c.Request.Context(), &items, "SELECT * FROM audit_logs ORDER BY created_at DESC LIMIT 1000"); err != nil { + s.dbError(c, err) + return + } + list(c, items) +} +func (s *Server) discoverVMs(c *gin.Context) { + if s.PVE == nil { + s.fail(c, 503, "pve_unconfigured", "尚未配置 Proxmox 接入,演示环境不发现真实 VM") + return + } + items, err := s.PVE.Resources(c.Request.Context()) + if err != nil { + s.fail(c, 502, "pve_unavailable", "Proxmox 查询失败,请检查连接、证书和访问权限") + return + } + vms := []gin.H{} + for _, r := range items { + if r.Type == "qemu" && r.Template == 0 { + vms = append(vms, gin.H{"vmid": r.VMID, "node": r.Node, "name": r.Name, "status": r.Status}) + } + } + list(c, vms) +} +func (s *Server) syncResources(c *gin.Context) { + if s.PVE == nil { + s.fail(c, 503, "pve_unconfigured", "请通过服务端环境变量配置 Proxmox 接入") + return + } + s.submitTask(c, "resource", "sync", "cluster", "Proxmox 集群") +} +func (s *Server) importDesktop(c *gin.Context) { + var in struct { + Name string `json:"name" binding:"required,max=80"` + OS string `json:"os" binding:"required,oneof=Windows Ubuntu 麒麟"` + Node string `json:"node" binding:"required,max=100"` + VMID int `json:"vmid" binding:"min=100"` + Endpoint string `json:"endpoint" binding:"max=253"` + Width int `json:"width" binding:"omitempty,min=640,max=7680"` + Height int `json:"height" binding:"omitempty,min=480,max=4320"` + FPS int `json:"fps" binding:"omitempty,min=10,max=120"` + } + if !s.decode(c, &in) { + return + } + if s.PVE == nil { + s.fail(c, 503, "pve_unconfigured", "真实 VM 纳管需要先配置 Proxmox") + return + } + if in.Endpoint != "" && net.ParseIP(in.Endpoint) == nil { + for _, part := range strings.Split(in.Endpoint, ".") { + if part == "" || strings.ContainsAny(part, "/:@ \\") { + s.fail(c, 400, "invalid_endpoint", "连接地址应为 IP 或主机名") + return + } + } + } + power, err := s.PVE.Power(c.Request.Context(), in.Node, in.VMID) + if err != nil { + s.fail(c, 502, "pve_unavailable", "无法验证所选 VM,请检查节点和 VMID") + return + } + id := store.ID("desk_") + if in.Width == 0 { + in.Width = 2560 + } + if in.Height == 0 { + in.Height = 1440 + } + if in.FPS == 0 { + in.FPS = 30 + } + _, err = s.DB.ExecContext(c.Request.Context(), `INSERT INTO desktops(id,name,os,node,vmid,power_state,provider,endpoint,width,height,fps) VALUES($1,$2,$3,$4,$5,$6,'proxmox',$7,$8,$9,$10)`, id, in.Name, in.OS, in.Node, in.VMID, power, in.Endpoint, in.Width, in.Height, in.FPS) + if err != nil { + if conflict(err) { + s.fail(c, 409, "already_managed", "该 VM 已纳管") + } else { + s.dbError(c, err) + } + return + } + s.log(c, "import_desktop", id, "success") + c.Params = append(c.Params, gin.Param{Key: "id", Value: id}) + s.desktop(c) +} +func (s *Server) desktopAction(c *gin.Context) { + var in struct { + Action string `json:"action" binding:"required,oneof=start shutdown reboot"` + } + if !s.decode(c, &in) { + return + } + var d model.Desktop + if err := s.DB.GetContext(c.Request.Context(), &d, "SELECT * FROM desktops WHERE id=$1", c.Param("id")); err != nil { + s.dbError(c, err) + return + } + if d.Provider == "demo" && s.Config.Mode != "demo" { + s.fail(c, 409, "demo_disabled", "正式模式不执行演示桌面任务") + return + } + if d.Provider == "proxmox" && s.PVE == nil { + s.fail(c, 503, "pve_unconfigured", "Proxmox 尚未配置") + return + } + s.submitTask(c, "desktop", in.Action, d.ID, d.Name) +} +func (s *Server) terminalCommand(c *gin.Context) { + var in struct { + Action string `json:"action" binding:"required,oneof=reboot reset-session collect-logs"` + } + if !s.decode(c, &in) { + return + } + var name string + if err := s.DB.GetContext(c.Request.Context(), &name, "SELECT name FROM terminals WHERE id=$1", c.Param("id")); err != nil { + s.dbError(c, err) + return + } + s.submitTask(c, "terminal", in.Action, c.Param("id"), name) +} +func (s *Server) submitTask(c *gin.Context, kind, action, targetID, targetName string) { + key := c.GetHeader("Idempotency-Key") + if key == "" || len(key) > 100 { + s.fail(c, 400, "idempotency_required", "请提供不超过 100 字符的 Idempotency-Key") + return + } + actor := s.admin(c).Username + ctx := c.Request.Context() + var existing model.Task + err := s.DB.GetContext(ctx, &existing, "SELECT * FROM tasks WHERE actor=$1 AND idempotency_key=$2", actor, key) + if err == nil { + if existing.Kind != kind || existing.Action != action || existing.TargetID != targetID { + s.fail(c, 409, "idempotency_conflict", "相同请求标识不能用于不同操作") + return + } + c.JSON(202, existing) + return + } + if !errors.Is(err, sql.ErrNoRows) { + s.dbError(c, err) + return + } + tx, err := s.DB.BeginTxx(ctx, nil) + if err != nil { + s.dbError(c, err) + return + } + defer tx.Rollback() + var task model.Task + err = tx.GetContext(ctx, &task, `INSERT INTO tasks(id,kind,action,target_id,target_name,actor,status,message,idempotency_key) VALUES($1,$2,$3,$4,$5,$6,'queued','已提交,等待执行回执',$7) RETURNING *`, store.ID("task_"), kind, action, targetID, targetName, actor, key) + if err != nil { + if conflict(err) { + s.fail(c, 409, "task_conflict", "已有相同请求或该对象有任务执行中,请刷新任务列表") + } else { + s.dbError(c, err) + } + return + } + _, err = tx.ExecContext(ctx, "INSERT INTO audit_logs VALUES($1,$2,$3,$4,'submitted',$5,now())", store.ID("audit_"), actor, action, targetName, c.GetString("requestId")) + if err == nil { + err = tx.Commit() + } + if err != nil { + s.dbError(c, err) + return + } + c.JSON(202, task) +} + +// Complete persists the final result and its audit entry together. +func (s *Server) complete(ctx context.Context, t model.Task, status, message string) error { + tx, err := s.DB.BeginTxx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + result, err := tx.ExecContext(ctx, "UPDATE tasks SET status=$1,message=$2,updated_at=now() WHERE id=$3 AND status IN ('running','queued')", status, message, t.ID) + if err != nil { + return err + } + n, _ := result.RowsAffected() + if n == 0 { + return nil + } + // Session release is scoped to a session ID by the device event endpoint. + // A delayed command acknowledgement must not clear a newer connection. + _, err = tx.ExecContext(ctx, "INSERT INTO audit_logs VALUES($1,$2,$3,$4,$5,$6,now())", store.ID("audit_"), t.Actor, t.Action, t.TargetName, status, t.ID) + if err != nil { + return err + } + return tx.Commit() +} +func ratio(a, b float64) float64 { + if b <= 0 { + return 0 + } + return a / b +} +func (s *Server) sync(ctx context.Context) error { + items, err := s.PVE.Resources(ctx) + if err != nil { + return err + } + tx, err := s.DB.BeginTxx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + if _, err = tx.ExecContext(ctx, "DELETE FROM resources WHERE provider='proxmox'"); err != nil { + return err + } + for _, r := range items { + if r.Type == "node" { + if _, err = tx.ExecContext(ctx, `INSERT INTO resources VALUES($1,$1,$2,$3,$4,$5,'proxmox',now()) ON CONFLICT(id) DO UPDATE SET status=$2,cpu=$3,memory=$4,storage=$5,provider='proxmox',updated_at=now()`, r.Node, r.Status, r.CPU, ratio(r.Memory, r.MaxMemory), ratio(r.Disk, r.MaxDisk)); err != nil { + return err + } + } + if r.Type == "qemu" { + if _, err = tx.ExecContext(ctx, "UPDATE desktops SET power_state=$1,updated_at=now() WHERE provider='proxmox' AND node=$2 AND vmid=$3", r.Status, r.Node, r.VMID); err != nil { + return err + } + } + } + return tx.Commit() +} + +func (s *Server) RunWorker(ctx context.Context) { + ticker := time.NewTicker(300 * time.Millisecond) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + // Unknown outcomes are never automatically retried after a worker restart. + var expired []model.Task + if s.DB.SelectContext(ctx, &expired, "SELECT * FROM tasks WHERE status IN ('queued','running') AND created_at= 400 && httpError.Status < 500 { + uncertain = false + } + if err == nil { + _, err = s.DB.ExecContext(ctx, "UPDATE tasks SET provider_ref=$1 WHERE id=$2", upid, t.ID) + } + if err == nil { + err = s.PVE.Wait(ctx, d.Node, upid, time.Second) + } + if err == nil { + var power string + power, err = s.PVE.Power(ctx, d.Node, d.VMID) + if err == nil { + expected := "running" + if t.Action == "shutdown" { + expected = "stopped" + } + if power != expected { + err = fmt.Errorf("power result not confirmed") + } else { + _, err = s.DB.ExecContext(ctx, "UPDATE desktops SET power_state=$1,service_state='unknown',updated_at=now() WHERE id=$2", power, d.ID) + } + } + } + } + } + } + status := "success" + if err != nil { + log.Printf("task execution error taskId=%s error=%v", t.ID, err) + status = "failed" + message = "执行失败,请检查服务日志及 PVE 任务;未自动重试" + if (uncertain && !errors.Is(err, proxmox.ErrTaskFailed)) || errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) { + status = "timeout" + message = "结果未确认,请核对实际状态;未自动重试" + } + } + finalCtx, finalCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer finalCancel() + _ = s.complete(finalCtx, t, status, message) +} diff --git a/platform/internal/server/server.go b/platform/internal/server/server.go new file mode 100644 index 0000000..38ec61e --- /dev/null +++ b/platform/internal/server/server.go @@ -0,0 +1,214 @@ +package server + +import ( + "context" + "database/sql" + "errors" + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" + "github.com/jmoiron/sqlx" + "golang.org/x/crypto/bcrypt" + "log" + "net/http" + "strconv" + "strings" + "time" + "vdi-cloud/platform/internal/config" + "vdi-cloud/platform/internal/model" + "vdi-cloud/platform/internal/store" + "vdi-cloud/platform/pkg/proxmox" +) + +type Server struct { + Config config.Config + DB *sqlx.DB + PVE *proxmox.Client + Router *gin.Engine +} +type claims struct { + AdminID string `json:"adminId"` + jwt.RegisteredClaims +} + +func New(cfg config.Config, db *sqlx.DB, pve *proxmox.Client) *Server { + gin.SetMode(gin.ReleaseMode) + s := &Server{Config: cfg, DB: db, PVE: pve} + r := gin.New() + r.Use(gin.Recovery(), func(c *gin.Context) { + c.Set("requestId", store.ID("req_")) + c.Header("X-Request-ID", c.GetString("requestId")) + c.Header("Cache-Control", "no-store") + c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 1<<20) + c.Next() + }) + s.Router = r + r.GET("/healthz", func(c *gin.Context) { + ctx, cancel := context.WithTimeout(c.Request.Context(), 2*time.Second) + defer cancel() + if db.PingContext(ctx) != nil { + s.fail(c, 503, "dependency_unavailable", "数据库暂不可用") + return + } + c.JSON(200, gin.H{"status": "ok", "mode": cfg.Mode}) + }) + r.POST("/api/v1/auth/login", s.login) + r.POST("/api/v1/terminal/register", s.register) + t := r.Group("/api/v1/terminal", s.deviceAuth) + t.POST("/heartbeat", s.heartbeat) + t.GET("/config", s.deviceConfig) + t.GET("/desktops", s.deviceDesktops) + t.GET("/commands", s.deviceCommands) + t.POST("/commands/:id/ack", s.deviceAck) + t.POST("/connect-ticket", s.createTicket) + t.POST("/connect-ticket/consume", s.consumeTicket) + t.POST("/session", s.deviceSession) + a := r.Group("/api/v1", s.adminAuth) + a.GET("/auth/me", func(c *gin.Context) { c.JSON(200, c.MustGet("admin")) }) + a.POST("/auth/logout", s.logout) + a.GET("/overview", s.overview) + a.GET("/settings", s.settings) + a.GET("/resources", s.resources) + a.GET("/resources/vms", s.discoverVMs) + a.POST("/resources/sync", s.allow("super_admin"), s.syncResources) + a.GET("/desktops", s.desktops) + a.POST("/desktops/import", s.allow("super_admin"), s.importDesktop) + a.GET("/desktops/:id", s.desktop) + a.POST("/desktops/:id/actions", s.allow("super_admin", "operator"), s.desktopAction) + a.GET("/terminals", s.terminals) + a.POST("/terminals", s.allow("super_admin"), s.enroll) + a.POST("/terminals/:id/commands", s.allow("super_admin", "operator"), s.terminalCommand) + a.GET("/users", s.users) + a.POST("/users", s.allow("super_admin"), s.createUser) + a.PUT("/users/:id/bindings", s.allow("super_admin"), s.bindings) + a.GET("/tasks", s.tasks) + a.GET("/tasks/:id", s.task) + a.GET("/tasks/:id/diagnostics", s.allow("super_admin", "operator"), s.taskDiagnostics) + a.GET("/audit-logs", s.audits) + r.NoRoute(func(c *gin.Context) { s.fail(c, 404, "not_found", "接口不存在") }) + return s +} +func (s *Server) fail(c *gin.Context, status int, code, message string) { + c.AbortWithStatusJSON(status, gin.H{"code": code, "message": message, "requestId": c.GetString("requestId")}) +} +func (s *Server) dbError(c *gin.Context, err error) { + log.Printf("database request failed requestId=%s error=%v", c.GetString("requestId"), err) + if errors.Is(err, sql.ErrNoRows) { + s.fail(c, 404, "not_found", "记录不存在") + } else { + s.fail(c, 500, "database_error", "操作未完成,请根据请求 ID 检查服务日志") + } +} +func (s *Server) decode(c *gin.Context, v any) bool { + if err := c.ShouldBindJSON(v); err != nil { + s.fail(c, 400, "invalid_request", "请检查必填字段和数据格式") + return false + } + return true +} +func (s *Server) admin(c *gin.Context) model.Administrator { + return c.MustGet("admin").(model.Administrator) +} +func (s *Server) log(c *gin.Context, action, target, result string) { + actor := "device" + if a, ok := c.Get("admin"); ok { + actor = a.(model.Administrator).Username + } + _, err := s.DB.ExecContext(c.Request.Context(), "INSERT INTO audit_logs(id,actor,action,target,result,request_id) VALUES($1,$2,$3,$4,$5,$6)", store.ID("audit_"), actor, action, target, result, c.GetString("requestId")) + if err != nil { + c.Error(err) + } +} +func (s *Server) allow(roles ...string) gin.HandlerFunc { + return func(c *gin.Context) { + for _, role := range roles { + if role == s.admin(c).Role { + c.Next() + return + } + } + s.log(c, c.Request.Method+" "+c.FullPath(), c.Param("id"), "denied") + s.fail(c, 403, "forbidden", "当前账号没有该操作权限") + } +} +func (s *Server) adminAuth(c *gin.Context) { + raw := strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer ") + cl := &claims{} + token, err := jwt.ParseWithClaims(raw, cl, func(t *jwt.Token) (any, error) { return []byte(s.Config.JWTSecret), nil }, jwt.WithValidMethods([]string{"HS256"}), jwt.WithIssuer("vdi-platform"), jwt.WithAudience("vdi-console"), jwt.WithExpirationRequired()) + if err != nil || !token.Valid { + s.fail(c, 401, "unauthorized", "登录已失效,请重新登录") + return + } + var a model.Administrator + err = s.DB.GetContext(c.Request.Context(), &a, `SELECT a.* FROM administrators a JOIN admin_sessions x ON x.admin_id=a.id WHERE x.id=$1 AND a.id=$2 AND x.expires_at>now()`, cl.ID, cl.AdminID) + if err != nil { + s.fail(c, 401, "unauthorized", "登录已失效,请重新登录") + return + } + c.Set("admin", a) + c.Set("sessionId", cl.ID) + c.Next() +} +func (s *Server) login(c *gin.Context) { + var input struct { + Username string `json:"username" binding:"required,max=80"` + Password string `json:"password" binding:"required,max=200"` + } + if !s.decode(c, &input) { + return + } + var a model.Administrator + err := s.DB.GetContext(c.Request.Context(), &a, "SELECT * FROM administrators WHERE username=$1", input.Username) + if err != nil || bcrypt.CompareHashAndPassword([]byte(a.PasswordHash), []byte(input.Password)) != nil { + s.fail(c, 401, "invalid_credentials", "用户名或密码错误") + return + } + now := time.Now() + id := store.ID("sess_") + expires := now.Add(time.Hour) + token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims{AdminID: a.ID, RegisteredClaims: jwt.RegisteredClaims{Issuer: "vdi-platform", Audience: jwt.ClaimStrings{"vdi-console"}, ID: id, Subject: a.ID, IssuedAt: jwt.NewNumericDate(now), ExpiresAt: jwt.NewNumericDate(expires)}}).SignedString([]byte(s.Config.JWTSecret)) + if err != nil { + s.dbError(c, err) + return + } + if _, err = s.DB.ExecContext(c.Request.Context(), "INSERT INTO admin_sessions VALUES($1,$2,$3)", id, a.ID, expires); err != nil { + s.dbError(c, err) + return + } + c.Set("admin", a) + s.log(c, "login", a.Username, "success") + c.JSON(200, gin.H{"accessToken": token, "expiresIn": 3600, "user": a}) +} +func (s *Server) logout(c *gin.Context) { + if _, err := s.DB.ExecContext(c.Request.Context(), "DELETE FROM admin_sessions WHERE id=$1", c.GetString("sessionId")); err != nil { + s.dbError(c, err) + return + } + s.log(c, "logout", s.admin(c).Username, "success") + c.Status(204) +} +func list[T any](c *gin.Context, items []T) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + size, _ := strconv.Atoi(c.DefaultQuery("pageSize", "20")) + if page < 1 { + page = 1 + } + if size < 1 { + size = 20 + } + if size > 100 { + size = 100 + } + total := len(items) + start := (page - 1) * size + if start < 0 || start > total { + start = total + } + end := start + size + if end > total { + end = total + } + if items == nil { + items = []T{} + } + c.JSON(200, gin.H{"items": items[start:end], "page": page, "pageSize": size, "total": total, "requestId": c.GetString("requestId")}) +} diff --git a/platform/internal/server/server_test.go b/platform/internal/server/server_test.go new file mode 100644 index 0000000..f61e9c2 --- /dev/null +++ b/platform/internal/server/server_test.go @@ -0,0 +1,375 @@ +package server + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "github.com/jmoiron/sqlx" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + "time" + "vdi-cloud/platform/internal/config" + "vdi-cloud/platform/internal/model" + "vdi-cloud/platform/internal/store" + "vdi-cloud/platform/pkg/proxmox" +) + +type fixture struct { + server *Server + db *sqlx.DB + t *testing.T +} + +func newFixture(t *testing.T) *fixture { + t.Helper() + url := os.Getenv("VDI_TEST_DATABASE_URL") + if url == "" { + t.Skip("set VDI_TEST_DATABASE_URL to a dedicated empty test database") + } + ctx := context.Background() + db, err := store.Open(ctx, url) + if err != nil { + t.Fatal(err) + } + if _, err = db.Exec(`TRUNCATE connection_tickets,audit_logs,tasks,bindings,users,terminals,desktops,resources,admin_sessions,administrators CASCADE`); err != nil { + t.Fatal(err) + } + if err = store.Bootstrap(ctx, db, "demo", "", ""); err != nil { + t.Fatal(err) + } + s := New(config.Config{Mode: "demo", JWTSecret: strings.Repeat("test-", 10)}, db, nil) + ctx, cancel := context.WithCancel(ctx) + go s.RunWorker(ctx) + t.Cleanup(func() { cancel(); time.Sleep(30 * time.Millisecond); db.Close() }) + return &fixture{s, db, t} +} +func (f *fixture) request(method, path, token string, body any, key string) (int, map[string]any) { + f.t.Helper() + var encoded []byte + if body != nil { + encoded, _ = json.Marshal(body) + } + r := httptest.NewRequest(method, "/api/v1"+path, bytes.NewReader(encoded)) + r.Header.Set("Content-Type", "application/json") + if token != "" { + r.Header.Set("Authorization", "Bearer "+token) + } + if key != "" { + r.Header.Set("Idempotency-Key", key) + } + w := httptest.NewRecorder() + f.server.Router.ServeHTTP(w, r) + out := map[string]any{} + if w.Body.Len() > 0 { + if err := json.Unmarshal(w.Body.Bytes(), &out); err != nil { + f.t.Fatal(err) + } + } + return w.Code, out +} +func (f *fixture) login(user string) string { + f.t.Helper() + status, out := f.request("POST", "/auth/login", "", map[string]any{"username": user, "password": "Demo@2026"}, "") + if status != 200 { + f.t.Fatalf("login failed %d %v", status, out) + } + return out["accessToken"].(string) +} +func expect(t *testing.T, want, got int, out any) { + t.Helper() + if want != got { + t.Fatalf("want status %d got %d: %v", want, got, out) + } +} + +func TestRBACLogoutAndDurableTask(t *testing.T) { + f := newFixture(t) + viewer := f.login("viewer") + operator := f.login("operator") + admin := f.login("admin") + status, out := f.request("GET", "/desktops", viewer, nil, "") + expect(t, 200, status, out) + status, out = f.request("POST", "/desktops/desk_demo_kylin/actions", viewer, map[string]any{"action": "start"}, "viewer-1") + expect(t, 403, status, out) + status, out = f.request("PUT", "/users/user_demo_wang/bindings", operator, map[string]any{"desktopIds": []string{}}, "") + expect(t, 403, status, out) + status, out = f.request("POST", "/desktops/desk_demo_kylin/actions", operator, map[string]any{"action": "start"}, "start-1") + expect(t, 202, status, out) + id := out["id"].(string) + status, again := f.request("POST", "/desktops/desk_demo_kylin/actions", operator, map[string]any{"action": "start"}, "start-1") + expect(t, 202, status, again) + if again["id"] != id { + t.Fatal("duplicate task created") + } + status, out = f.request("POST", "/desktops/desk_demo_kylin/actions", operator, map[string]any{"action": "shutdown"}, "start-1") + expect(t, 409, status, out) + deadline := time.Now().Add(5 * time.Second) + for { + status, out = f.request("GET", "/tasks/"+id, admin, nil, "") + expect(t, 200, status, out) + if out["status"] == "success" { + break + } + if time.Now().After(deadline) { + t.Fatal("task never completed", out) + } + time.Sleep(50 * time.Millisecond) + } + var power, service string + if err := f.db.QueryRow("SELECT power_state,service_state FROM desktops WHERE id='desk_demo_kylin'").Scan(&power, &service); err != nil { + t.Fatal(err) + } + if power != "running" || service != "unknown" { + t.Fatal("power and service state confused") + } + status, out = f.request("POST", "/auth/logout", admin, nil, "") + expect(t, 204, status, out) + status, out = f.request("GET", "/auth/me", admin, nil, "") + expect(t, 401, status, out) + var count int + f.db.Get(&count, "SELECT count(*) FROM tasks WHERE id=$1", id) + if count != 1 { + t.Fatal("task not persisted") + } +} + +func TestDeviceIdentityBindingTicketAndAcknowledgement(t *testing.T) { + f := newFixture(t) + admin := f.login("admin") + enroll := func(sn string) (string, string) { + status, out := f.request("POST", "/terminals", admin, map[string]any{"sn": sn, "name": sn, "group": "测试"}, "") + expect(t, 201, status, out) + secret := out["activationSecret"].(string) + id := out["id"].(string) + status, out = f.request("POST", "/terminal/register", "", map[string]any{"sn": sn, "activationSecret": secret, "agentVersion": "test"}, "") + expect(t, 200, status, out) + token := out["accessToken"].(string) + status, out = f.request("POST", "/terminal/register", "", map[string]any{"sn": sn, "activationSecret": secret, "agentVersion": "test"}, "") + expect(t, 401, status, out) + return id, token + } + id, device := enroll("TEST-DEVICE-001") + otherID, other := enroll("TEST-DEVICE-002") + status, out := f.request("GET", "/users", device, nil, "") + expect(t, 401, status, out) + status, out = f.request("POST", "/terminal/connect-ticket", device, map[string]any{"desktopId": "desk_demo_windows"}, "") + expect(t, 403, status, out) + status, out = f.request("PUT", "/users/user_demo_wang/bindings", admin, map[string]any{"terminalId": id, "desktopIds": []string{"desk_demo_windows", "desk_demo_ubuntu"}, "enabled": true}, "") + expect(t, 200, status, out) + status, out = f.request("GET", "/terminal/desktops", device, nil, "") + expect(t, 200, status, out) + if len(out["items"].([]any)) != 2 { + t.Fatal("missing allowed desktops") + } + f.db.Exec("UPDATE desktops SET endpoint='192.0.2.100' WHERE id='desk_demo_windows'") + status, out = f.request("POST", "/terminal/connect-ticket", device, map[string]any{"desktopId": "desk_demo_windows"}, "") + expect(t, 201, status, out) + ticket := out["ticket"].(string) + status, out = f.request("POST", "/terminal/connect-ticket/consume", other, map[string]any{"ticket": ticket}, "") + expect(t, 401, status, out) + status, out = f.request("POST", "/terminal/connect-ticket/consume", device, map[string]any{"ticket": ticket}, "") + expect(t, 200, status, out) + if out["credentialMode"] != "existing-pairing" { + t.Fatal("must declare existing Sunshine pairing") + } + sessionID := out["sessionId"].(string) + status, out = f.request("GET", "/overview", admin, nil, "") + expect(t, 200, status, out) + if out["activeSessions"] != float64(0) { + t.Fatal("reservation counted as active stream") + } + status, out = f.request("POST", "/terminal/session", device, map[string]any{"sessionId": sessionID, "state": "connected"}, "") + expect(t, 200, status, out) + status, out = f.request("GET", "/overview", admin, nil, "") + if out["activeSessions"] != float64(1) { + t.Fatal("stream-ready event missing") + } + status, out = f.request("POST", "/terminal/session", other, map[string]any{"sessionId": sessionID, "state": "ended"}, "") + expect(t, 409, status, out) + status, out = f.request("POST", "/terminal/session", device, map[string]any{"sessionId": sessionID, "state": "ended"}, "") + expect(t, 200, status, out) + status, out = f.request("POST", "/terminal/session", device, map[string]any{"sessionId": sessionID, "state": "connected"}, "") + expect(t, 409, status, out) + status, out = f.request("POST", "/terminal/connect-ticket/consume", device, map[string]any{"ticket": ticket}, "") + expect(t, 401, status, out) + status, out = f.request("PUT", "/users/user_demo_wang/bindings", admin, map[string]any{"terminalId": id, "desktopIds": []string{}, "enabled": true}, "") + expect(t, 200, status, out) + status, out = f.request("POST", "/terminal/connect-ticket", device, map[string]any{"desktopId": "desk_demo_windows"}, "") + expect(t, 403, status, out) + status, out = f.request("POST", fmt.Sprintf("/terminals/%s/commands", id), admin, map[string]any{"action": "collect-logs"}, "cmd-1") + expect(t, 202, status, out) + taskID := out["id"].(string) + status, out = f.request("GET", "/terminal/commands", other, nil, "") + expect(t, 200, status, out) + if len(out["items"].([]any)) != 0 { + t.Fatal("command leaked to other terminal") + } + status, out = f.request("POST", "/terminal/commands/"+taskID+"/ack", other, map[string]any{"status": "success"}, "") + expect(t, 404, status, out) + status, out = f.request("GET", "/terminal/commands", device, nil, "") + expect(t, 200, status, out) + if len(out["items"].([]any)) != 1 { + t.Fatal("device did not receive command") + } + status, out = f.request("POST", "/terminal/commands/"+taskID+"/ack", device, map[string]any{"status": "failed", "message": "private device log"}, "") + expect(t, 200, status, out) + status, out = f.request("GET", "/tasks/"+taskID, admin, nil, "") + expect(t, 200, status, out) + if out["status"] != "failed" { + t.Fatal("must wait for device ack") + } + if strings.Contains(out["message"].(string), "private") { + t.Fatal("raw device detail leaked") + } + status, out = f.request("POST", "/terminals/"+id+"/commands", admin, map[string]any{"action": "collect-logs"}, "diagnostics-2") + expect(t, 202, status, out) + diagnosticID := out["id"].(string) + status, out = f.request("GET", "/terminal/commands", device, nil, "") + expect(t, 200, status, out) + status, out = f.request("POST", "/terminal/commands/"+diagnosticID+"/ack", device, map[string]any{"status": "success"}, "") + expect(t, 400, status, out) + status, out = f.request("POST", "/terminal/commands/"+diagnosticID+"/ack", device, map[string]any{"status": "success", "diagnostics": map[string]any{"agentVersion": "qt-test", "bootId": "test-boot", "kioskState": "DESKTOP_LIST", "uptimeSeconds": 100, "logTail": "private diagnostic"}}, "") + expect(t, 200, status, out) + viewer := f.login("viewer") + status, out = f.request("GET", "/tasks/"+diagnosticID+"/diagnostics", viewer, nil, "") + expect(t, 403, status, out) + status, out = f.request("GET", "/tasks/"+diagnosticID+"/diagnostics", admin, nil, "") + expect(t, 200, status, out) + if out["logTail"] != "private diagnostic" { + t.Fatal("diagnostic not retained") + } + _ = otherID +} + +func TestUnknownTerminalOutcomeAndBindingRollback(t *testing.T) { + f := newFixture(t) + admin := f.login("admin") + status, out := f.request("PUT", "/users/user_demo_wang/bindings", admin, map[string]any{"terminalId": "term_demo_001", "desktopIds": []string{"missing"}}, "") + expect(t, 400, status, out) + var n int + f.db.Get(&n, "SELECT count(*) FROM bindings WHERE user_id='user_demo_wang'") + if n != 2 { + t.Fatal("failed binding update lost original authorization") + } + status, out = f.request("POST", "/terminals/term_demo_001/commands", admin, map[string]any{"action": "reboot"}, "offline-1") + expect(t, 202, status, out) + id := out["id"].(string) + f.db.Exec("UPDATE tasks SET created_at=now()-interval '3 minutes' WHERE id=$1", id) + deadline := time.Now().Add(3 * time.Second) + for { + status, out = f.request("GET", "/tasks/"+id, admin, nil, "") + expect(t, 200, status, out) + if out["status"] == "timeout" { + break + } + if time.Now().After(deadline) { + t.Fatal("offline task was not timed out", out) + } + time.Sleep(50 * time.Millisecond) + } + status, out = f.request(http.MethodPost, "/resources/sync", admin, nil, "sync-1") + expect(t, 503, status, out) +} + +func TestRestartPreservesRevokedBindingsAndRejectsModeChange(t *testing.T) { + f := newFixture(t) + if _, err := f.db.Exec("DELETE FROM bindings WHERE user_id='user_demo_wang'"); err != nil { + t.Fatal(err) + } + if err := store.Bootstrap(context.Background(), f.db, "demo", "", ""); err != nil { + t.Fatal(err) + } + var count int + if err := f.db.Get(&count, "SELECT count(*) FROM bindings WHERE user_id='user_demo_wang'"); err != nil || count != 0 { + t.Fatalf("restart restored revoked bindings: count=%d err=%v", count, err) + } + if err := store.Bootstrap(context.Background(), f.db, "production", "prod-admin", "long-password-2026"); err == nil { + t.Fatal("demo database accepted production bootstrap") + } +} + +func TestInitialPasswordPolicyDoesNotRejectExistingAccountOnRestart(t *testing.T) { + f := newFixture(t) + if _, err := f.db.Exec("UPDATE platform_metadata SET mode='production'"); err != nil { + t.Fatal(err) + } + defer f.db.Exec("UPDATE platform_metadata SET mode='demo'") + ctx := context.Background() + if err := store.Bootstrap(ctx, f.db, "production", "new-admin", "short"); err == nil { + t.Fatal("weak initial password accepted") + } + if err := store.Bootstrap(ctx, f.db, "production", "new-admin", "strong-initial-password"); err != nil { + t.Fatal(err) + } + var before, after string + if err := f.db.Get(&before, "SELECT password_hash FROM administrators WHERE username='new-admin'"); err != nil { + t.Fatal(err) + } + if err := store.Bootstrap(ctx, f.db, "production", "new-admin", "short"); err != nil { + t.Fatal("existing account blocked startup", err) + } + if err := f.db.Get(&after, "SELECT password_hash FROM administrators WHERE username='new-admin'"); err != nil { + t.Fatal(err) + } + if before != after { + t.Fatal("restart changed the account password") + } +} + +func TestLateCommandAcknowledgementPreservesNewSession(t *testing.T) { + f := newFixture(t) + if _, err := f.db.Exec("UPDATE terminals SET session_id='new-session',session_state='connected',session_desktop_id='desk_demo_windows' WHERE id='term_demo_001'"); err != nil { + t.Fatal(err) + } + var task model.Task + if err := f.db.Get(&task, `INSERT INTO tasks(id,kind,action,target_id,target_name,actor,status,message,idempotency_key) VALUES('task-late','terminal','reset-session','term_demo_001','test','admin','running','','late') RETURNING *`); err != nil { + t.Fatal(err) + } + if err := f.server.complete(context.Background(), task, "success", "ack"); err != nil { + t.Fatal(err) + } + var session string + if err := f.db.Get(&session, "SELECT session_id FROM terminals WHERE id='term_demo_001'"); err != nil || session != "new-session" { + t.Fatalf("new session lost: %q %v", session, err) + } +} + +func TestPVEConfirmedFailureAndUncertainSubmission(t *testing.T) { + f := newFixture(t) + if _, err := f.db.Exec("UPDATE desktops SET provider='proxmox' WHERE id='desk_demo_kylin'"); err != nil { + t.Fatal(err) + } + for _, tc := range []struct { + name string + submission int + exit string + want string + }{{"submission unavailable", 503, "", "timeout"}, {"permission denied", 403, "", "failed"}, {"explicit PVE task failure", 200, "ERROR", "failed"}} { + t.Run(tc.name, func(t *testing.T) { + pve := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + if r.Method == http.MethodPost { + w.WriteHeader(tc.submission) + fmt.Fprint(w, `{"data":"UPID:test"}`) + return + } + fmt.Fprintf(w, `{"data":{"status":"stopped","exitstatus":%q}}`, tc.exit) + })) + defer pve.Close() + f.server.PVE = &proxmox.Client{BaseURL: pve.URL, HTTP: pve.Client()} + var task model.Task + if err := f.db.Get(&task, `INSERT INTO tasks(id,kind,action,target_id,target_name,actor,status,message,idempotency_key) VALUES($1,'desktop','start','desk_demo_kylin','test VM','admin','running','test',$1) RETURNING *`, store.ID("task_")); err != nil { + t.Fatal(err) + } + f.server.execute(context.Background(), task) + var status string + if err := f.db.Get(&status, "SELECT status FROM tasks WHERE id=$1", task.ID); err != nil || status != tc.want { + t.Fatalf("want %s, got %s err=%v", tc.want, status, err) + } + }) + } +} diff --git a/platform/internal/server/terminal.go b/platform/internal/server/terminal.go new file mode 100644 index 0000000..bf2e4fc --- /dev/null +++ b/platform/internal/server/terminal.go @@ -0,0 +1,291 @@ +package server + +import ( + "crypto/subtle" + "encoding/json" + "github.com/gin-gonic/gin" + "strings" + "time" + "vdi-cloud/platform/internal/model" + "vdi-cloud/platform/internal/store" +) + +func (s *Server) enroll(c *gin.Context) { + var in struct { + SN string `json:"sn" binding:"required,min=3,max=100"` + Name string `json:"name" binding:"required,max=80"` + Group string `json:"group" binding:"max=80"` + } + if !s.decode(c, &in) { + return + } + in.SN = strings.TrimSpace(in.SN) + in.Name = strings.TrimSpace(in.Name) + if in.SN == "" || in.Name == "" { + s.fail(c, 400, "invalid_request", "SN 和终端名称不能为空") + return + } + id, secret := store.ID("term_"), store.ID("activate_") + _, err := s.DB.ExecContext(c.Request.Context(), "INSERT INTO terminals(id,sn,name,group_name,bootstrap_hash) VALUES($1,$2,$3,$4,$5)", id, in.SN, in.Name, in.Group, store.Hash(secret)) + if err != nil { + if conflict(err) { + s.fail(c, 409, "sn_exists", "该 SN 已登记") + } else { + s.dbError(c, err) + } + return + } + s.log(c, "enroll_terminal", id, "success") + c.JSON(201, gin.H{"id": id, "activationSecret": secret}) +} +func (s *Server) register(c *gin.Context) { + var in struct { + SN string `json:"sn" binding:"required,max=100"` + ActivationSecret string `json:"activationSecret" binding:"required,max=200"` + AgentVersion string `json:"agentVersion" binding:"required,max=80"` + } + if !s.decode(c, &in) { + return + } + token := store.ID("device_") + var id string + err := s.DB.GetContext(c.Request.Context(), &id, `UPDATE terminals SET token_hash=$1,bootstrap_hash='',status='online',agent_version=$2,last_seen=now() WHERE sn=$3 AND bootstrap_hash=$4 AND bootstrap_hash!='' RETURNING id`, store.Hash(token), in.AgentVersion, in.SN, store.Hash(in.ActivationSecret)) + if err != nil { + s.fail(c, 401, "invalid_activation", "激活信息无效或已使用,请联系管理员") + return + } + c.JSON(200, gin.H{"terminalId": id, "accessToken": token, "heartbeatInterval": 30}) +} +func (s *Server) deviceAuth(c *gin.Context) { + token := strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer ") + var id string + if token == "" || s.DB.GetContext(c.Request.Context(), &id, "SELECT id FROM terminals WHERE token_hash=$1", store.Hash(token)) != nil { + s.fail(c, 401, "device_unauthorized", "终端身份无效") + return + } + c.Set("terminalId", id) + c.Next() +} +func (s *Server) heartbeat(c *gin.Context) { + var in struct { + AgentVersion string `json:"agentVersion" binding:"required,max=80"` + FPS *float64 `json:"fps" binding:"omitempty,min=0,max=1000"` + RTT *float64 `json:"rtt" binding:"omitempty,min=0,max=60000"` + } + if !s.decode(c, &in) { + return + } + _, err := s.DB.ExecContext(c.Request.Context(), "UPDATE terminals SET last_seen=now(),status='online',agent_version=$1,fps=$2,rtt=$3 WHERE id=$4", in.AgentVersion, in.FPS, in.RTT, c.GetString("terminalId")) + if err != nil { + s.dbError(c, err) + return + } + c.JSON(200, gin.H{"accepted": true, "serverTime": time.Now().UTC()}) +} +func (s *Server) deviceConfig(c *gin.Context) { + var version int + if err := s.DB.GetContext(c.Request.Context(), &version, "SELECT config_version FROM terminals WHERE id=$1", c.GetString("terminalId")); err != nil { + s.dbError(c, err) + return + } + c.JSON(200, gin.H{"configVersion": version, "identityMode": "device", "commandTransport": "https-poll", "heartbeatInterval": 30}) +} +func (s *Server) deviceDesktops(c *gin.Context) { + items := []model.Desktop{} + err := s.DB.SelectContext(c.Request.Context(), &items, `SELECT d.* FROM desktops d JOIN bindings b ON b.desktop_id=d.id JOIN users u ON u.id=b.user_id WHERE u.terminal_id=$1 AND u.enabled=true ORDER BY d.name`, c.GetString("terminalId")) + if err != nil { + s.dbError(c, err) + return + } + list(c, items) +} +func (s *Server) deviceCommands(c *gin.Context) { + items := []model.Task{} + err := s.DB.SelectContext(c.Request.Context(), &items, `UPDATE tasks SET status='running',updated_at=now(),message='终端已领取,等待执行回执' WHERE id IN (SELECT id FROM tasks WHERE kind='terminal' AND target_id=$1 AND status='queued' AND created_at>now()-interval '2 minutes' ORDER BY created_at FOR UPDATE SKIP LOCKED LIMIT 10) RETURNING *`, c.GetString("terminalId")) + if err != nil { + s.dbError(c, err) + return + } + list(c, items) +} +func (s *Server) deviceAck(c *gin.Context) { + var in struct { + Status string `json:"status" binding:"required,oneof=success failed"` + Message string `json:"message" binding:"max=300"` + Diagnostics *model.DeviceDiagnostics `json:"diagnostics"` + } + if !s.decode(c, &in) { + return + } + var task model.Task + err := s.DB.GetContext(c.Request.Context(), &task, "SELECT * FROM tasks WHERE id=$1 AND kind='terminal' AND target_id=$2", c.Param("id"), c.GetString("terminalId")) + if err != nil { + s.dbError(c, err) + return + } + if task.Status == in.Status { + c.JSON(200, gin.H{"accepted": true}) + return + } + if task.Status != "running" { + s.fail(c, 409, "invalid_task_state", "该命令尚未领取或已经结束") + return + } + if task.Action == "collect-logs" && in.Status == "success" { + if in.Diagnostics == nil { + s.fail(c, 400, "diagnostics_required", "收集日志成功需要上传诊断结果") + return + } + data, marshalErr := json.Marshal(in.Diagnostics) + if marshalErr != nil { + s.dbError(c, marshalErr) + return + } + if _, err = s.DB.ExecContext(c.Request.Context(), "INSERT INTO task_diagnostics(task_id,content) VALUES($1,$2) ON CONFLICT(task_id) DO NOTHING", task.ID, string(data)); err != nil { + s.dbError(c, err) + return + } + } + // Agent text can contain sensitive device details. Keep the public audit message fixed. + if err = s.complete(c.Request.Context(), task, in.Status, "已收到终端执行回执"); err != nil { + s.dbError(c, err) + return + } + c.JSON(200, gin.H{"accepted": true}) +} +func (s *Server) createTicket(c *gin.Context) { + var in struct { + DesktopID string `json:"desktopId" binding:"required,max=100"` + } + if !s.decode(c, &in) { + return + } + var allowed bool + err := s.DB.GetContext(c.Request.Context(), &allowed, `SELECT EXISTS(SELECT 1 FROM bindings b JOIN users u ON u.id=b.user_id WHERE b.desktop_id=$1 AND u.terminal_id=$2 AND u.enabled=true)`, in.DesktopID, c.GetString("terminalId")) + if err != nil { + s.dbError(c, err) + return + } + if !allowed { + s.fail(c, 403, "desktop_forbidden", "该终端没有桌面使用权限") + return + } + var d model.Desktop + if err = s.DB.GetContext(c.Request.Context(), &d, "SELECT * FROM desktops WHERE id=$1", in.DesktopID); err != nil { + s.dbError(c, err) + return + } + if d.PowerState != "running" || d.Endpoint == "" { + s.fail(c, 409, "desktop_not_ready", "桌面未开机或尚未配置连接地址") + return + } + token := store.ID("ticket_") + expires := time.Now().Add(45 * time.Second) + _, err = s.DB.ExecContext(c.Request.Context(), "INSERT INTO connection_tickets(token_hash,terminal_id,desktop_id,expires_at) VALUES($1,$2,$3,$4)", store.Hash(token), c.GetString("terminalId"), in.DesktopID, expires) + if err != nil { + s.dbError(c, err) + return + } + c.JSON(201, gin.H{"ticket": token, "desktopId": in.DesktopID, "expiresAt": expires, "credentialMode": "existing-pairing"}) +} +func (s *Server) consumeTicket(c *gin.Context) { + var in struct { + Ticket string `json:"ticket" binding:"required,max=200"` + } + if !s.decode(c, &in) { + return + } + ctx := c.Request.Context() + tx, err := s.DB.BeginTxx(ctx, nil) + if err != nil { + s.dbError(c, err) + return + } + defer tx.Rollback() + var row struct { + TokenHash string `db:"token_hash"` + TerminalID string `db:"terminal_id"` + DesktopID string `db:"desktop_id"` + } + err = tx.GetContext(ctx, &row, `SELECT token_hash,terminal_id,desktop_id FROM connection_tickets WHERE token_hash=$1 AND consumed_at IS NULL AND expires_at>now() FOR UPDATE`, store.Hash(in.Ticket)) + if err != nil || subtle.ConstantTimeCompare([]byte(row.TerminalID), []byte(c.GetString("terminalId"))) != 1 { + s.fail(c, 401, "invalid_ticket", "票据已失效或不属于当前终端") + return + } + var d model.Desktop + if err = tx.GetContext(ctx, &d, "SELECT * FROM desktops WHERE id=$1 FOR UPDATE", row.DesktopID); err != nil { + s.dbError(c, err) + return + } + var allowed bool + err = tx.GetContext(ctx, &allowed, `SELECT EXISTS(SELECT 1 FROM users u JOIN bindings b ON b.user_id=u.id WHERE u.terminal_id=$1 AND u.enabled AND b.desktop_id=$2)`, row.TerminalID, d.ID) + if err != nil { + s.dbError(c, err) + return + } + if !allowed { + s.fail(c, 403, "desktop_forbidden", "桌面授权已撤销") + return + } + if d.PowerState != "running" || d.Endpoint == "" { + s.fail(c, 409, "desktop_not_ready", "桌面当前不可连接") + return + } + var busy bool + if err = tx.GetContext(ctx, &busy, `SELECT EXISTS(SELECT 1 FROM terminals WHERE session_desktop_id=$1 AND id!=$2 AND last_seen>now()-interval '90 seconds')`, d.ID, row.TerminalID); err != nil { + s.dbError(c, err) + return + } + if busy { + s.fail(c, 409, "desktop_busy", "桌面已被其他终端占用") + return + } + sessionID := store.ID("session_") + _, err = tx.ExecContext(ctx, "UPDATE connection_tickets SET consumed_at=now() WHERE token_hash=$1", row.TokenHash) + if err == nil { + _, err = tx.ExecContext(ctx, "UPDATE terminals SET session_desktop_id=$1,last_seen=now(),session_id=$3,session_state='connecting' WHERE id=$2", d.ID, row.TerminalID, sessionID) + } + if err == nil { + err = tx.Commit() + } + if err != nil { + s.dbError(c, err) + return + } + c.JSON(200, gin.H{"desktopId": d.ID, "sessionId": sessionID, "protocol": "moonlight", "endpoint": d.Endpoint, "app": d.App, "width": d.Width, "height": d.Height, "fps": d.FPS, "credentialMode": "existing-pairing"}) +} + +func (s *Server) deviceSession(c *gin.Context) { + var in struct { + SessionID string `json:"sessionId" binding:"required,max=100"` + State string `json:"state" binding:"required,oneof=connected ended"` + } + if !s.decode(c, &in) { + return + } + query := "UPDATE terminals SET session_state='connected',last_seen=now() WHERE id=$1 AND session_id=$2 AND session_desktop_id IS NOT NULL" + if in.State == "ended" { + query = "UPDATE terminals SET session_state='idle',session_id='',session_desktop_id=NULL WHERE id=$1 AND session_id=$2" + } + result, err := s.DB.ExecContext(c.Request.Context(), query, c.GetString("terminalId"), in.SessionID) + if err != nil { + s.dbError(c, err) + return + } + n, _ := result.RowsAffected() + if n == 0 { + s.fail(c, 409, "stale_session", "连接预约已结束或已被新会话替代") + return + } + c.JSON(200, gin.H{"accepted": true}) +} + +func (s *Server) taskDiagnostics(c *gin.Context) { + var data []byte + if err := s.DB.GetContext(c.Request.Context(), &data, "SELECT content FROM task_diagnostics WHERE task_id=$1", c.Param("id")); err != nil { + s.dbError(c, err) + return + } + s.log(c, "read_diagnostics", c.Param("id"), "success") + c.Data(200, "application/json", data) +} diff --git a/platform/internal/store/schema.sql b/platform/internal/store/schema.sql new file mode 100644 index 0000000..02f7171 --- /dev/null +++ b/platform/internal/store/schema.sql @@ -0,0 +1,59 @@ +CREATE TABLE IF NOT EXISTS platform_metadata ( + singleton boolean PRIMARY KEY DEFAULT true CHECK(singleton), mode text NOT NULL CHECK(mode IN ('demo','production')) +); +CREATE TABLE IF NOT EXISTS administrators ( + id text PRIMARY KEY, username text NOT NULL UNIQUE, display_name text NOT NULL, + role text NOT NULL CHECK (role IN ('super_admin','operator','viewer')), password_hash text NOT NULL +); +CREATE TABLE IF NOT EXISTS admin_sessions ( + id text PRIMARY KEY, admin_id text NOT NULL REFERENCES administrators(id), expires_at timestamptz NOT NULL +); +CREATE TABLE IF NOT EXISTS desktops ( + id text PRIMARY KEY, name text NOT NULL, os text NOT NULL, node text NOT NULL, vmid integer NOT NULL, + pool text NOT NULL DEFAULT '', power_state text NOT NULL, service_state text NOT NULL DEFAULT 'unknown', + provider text NOT NULL CHECK (provider IN ('demo','proxmox')), endpoint text NOT NULL DEFAULT '', app text NOT NULL DEFAULT 'Desktop', + width integer NOT NULL DEFAULT 2560, height integer NOT NULL DEFAULT 1440, fps integer NOT NULL DEFAULT 30, + updated_at timestamptz NOT NULL DEFAULT now(), UNIQUE(provider,node,vmid) +); +CREATE TABLE IF NOT EXISTS terminals ( + id text PRIMARY KEY, sn text NOT NULL UNIQUE, name text NOT NULL, group_name text NOT NULL DEFAULT '', + status text NOT NULL DEFAULT 'unregistered', agent_version text NOT NULL DEFAULT '', last_seen timestamptz, + session_desktop_id text REFERENCES desktops(id), config_version integer NOT NULL DEFAULT 1, + bootstrap_hash text NOT NULL, token_hash text UNIQUE, fps double precision, rtt double precision +); +CREATE TABLE IF NOT EXISTS users ( + id text PRIMARY KEY, username text NOT NULL UNIQUE, display_name text NOT NULL, + enabled boolean NOT NULL DEFAULT true, terminal_id text UNIQUE REFERENCES terminals(id) +); +CREATE TABLE IF NOT EXISTS bindings ( + user_id text NOT NULL REFERENCES users(id) ON DELETE CASCADE, + desktop_id text NOT NULL REFERENCES desktops(id), PRIMARY KEY(user_id,desktop_id) +); +CREATE TABLE IF NOT EXISTS resources ( + id text PRIMARY KEY, name text NOT NULL, status text NOT NULL, cpu double precision NOT NULL, + memory double precision NOT NULL, storage double precision NOT NULL, provider text NOT NULL, + updated_at timestamptz NOT NULL DEFAULT now() +); +CREATE TABLE IF NOT EXISTS tasks ( + id text PRIMARY KEY, kind text NOT NULL, action text NOT NULL, target_id text NOT NULL, target_name text NOT NULL, + actor text NOT NULL, status text NOT NULL, message text NOT NULL DEFAULT '', provider_ref text NOT NULL DEFAULT '', + idempotency_key text NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), + UNIQUE(actor,idempotency_key) +); +CREATE UNIQUE INDEX IF NOT EXISTS tasks_active_target ON tasks(kind,target_id) WHERE status IN ('queued','running'); +CREATE TABLE IF NOT EXISTS audit_logs ( + id text PRIMARY KEY, actor text NOT NULL, action text NOT NULL, target text NOT NULL, result text NOT NULL, + request_id text NOT NULL, created_at timestamptz NOT NULL DEFAULT now() +); +CREATE TABLE IF NOT EXISTS connection_tickets ( + token_hash text PRIMARY KEY, terminal_id text NOT NULL REFERENCES terminals(id), desktop_id text NOT NULL REFERENCES desktops(id), + expires_at timestamptz NOT NULL, consumed_at timestamptz +); +CREATE INDEX IF NOT EXISTS audit_created ON audit_logs(created_at DESC); +CREATE INDEX IF NOT EXISTS tasks_created ON tasks(created_at DESC); +ALTER TABLE terminals ADD COLUMN IF NOT EXISTS session_id text NOT NULL DEFAULT ''; +ALTER TABLE terminals ADD COLUMN IF NOT EXISTS session_state text NOT NULL DEFAULT 'idle'; +CREATE TABLE IF NOT EXISTS task_diagnostics ( + task_id text PRIMARY KEY REFERENCES tasks(id) ON DELETE CASCADE, + content jsonb NOT NULL, created_at timestamptz NOT NULL DEFAULT now() +); diff --git a/platform/internal/store/store.go b/platform/internal/store/store.go new file mode 100644 index 0000000..87478da --- /dev/null +++ b/platform/internal/store/store.go @@ -0,0 +1,119 @@ +package store + +import ( + "context" + "crypto/rand" + "crypto/sha256" + _ "embed" + "encoding/hex" + "fmt" + _ "github.com/jackc/pgx/v5/stdlib" + "github.com/jmoiron/sqlx" + "golang.org/x/crypto/bcrypt" + "time" + "vdi-cloud/platform/internal/model" +) + +//go:embed schema.sql +var schema string + +func Open(ctx context.Context, url string) (*sqlx.DB, error) { + db, err := sqlx.ConnectContext(ctx, "pgx", url) + if err != nil { + return nil, err + } + db.SetMaxOpenConns(12) + db.SetMaxIdleConns(4) + db.SetConnMaxLifetime(30 * time.Minute) + if _, err = db.ExecContext(ctx, schema); err != nil { + db.Close() + return nil, err + } + return db, nil +} +func ID(prefix string) string { + b := make([]byte, 16) + if _, err := rand.Read(b); err != nil { + panic(err) + } + return prefix + hex.EncodeToString(b) +} +func Hash(secret string) string { h := sha256.Sum256([]byte(secret)); return hex.EncodeToString(h[:]) } +func Bootstrap(ctx context.Context, db *sqlx.DB, mode, username, password string) error { + if _, err := db.ExecContext(ctx, "INSERT INTO platform_metadata(singleton,mode) VALUES(true,$1) ON CONFLICT DO NOTHING", mode); err != nil { + return err + } + var existingMode string + if err := db.GetContext(ctx, &existingMode, "SELECT mode FROM platform_metadata WHERE singleton=true"); err != nil { + return err + } + if existingMode != mode { + return fmt.Errorf("database mode is %s; use a separate database for %s", existingMode, mode) + } + admins := []struct{ username, name, role, password string }{{username, "平台管理员", "super_admin", password}} + if mode == "demo" { + admins = []struct{ username, name, role, password string }{{"admin", "系统管理员", "super_admin", "Demo@2026"}, {"operator", "运维人员", "operator", "Demo@2026"}, {"viewer", "只读人员", "viewer", "Demo@2026"}} + } + for _, a := range admins { + var exists bool + if err := db.GetContext(ctx, &exists, "SELECT EXISTS(SELECT 1 FROM administrators WHERE username=$1)", a.username); err != nil { + return err + } + if exists { + continue + } + if mode == "production" && len(a.password) < 12 { + return fmt.Errorf("initial administrator password must contain at least 12 characters") + } + hash, err := bcrypt.GenerateFromPassword([]byte(a.password), bcrypt.DefaultCost) + if err != nil { + return err + } + if _, err = db.ExecContext(ctx, "INSERT INTO administrators VALUES($1,$2,$3,$4,$5) ON CONFLICT DO NOTHING", ID("adm_"), a.username, a.name, a.role, string(hash)); err != nil { + return err + } + } + if mode != "demo" { + return nil + } + // Seed the demo only once; restarting must not restore revoked bindings. + var seeded bool + if err := db.GetContext(ctx, &seeded, "SELECT EXISTS(SELECT 1 FROM desktops WHERE provider='demo')"); err != nil { + return err + } + if seeded { + return nil + } + tx, err := db.BeginTxx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + statements := []string{ + `INSERT INTO desktops(id,name,os,node,vmid,pool,power_state,service_state,provider) VALUES ('desk_demo_windows','设计专属桌面','Windows','demo-node-a',201,'设计专属池','running','unknown','demo'),('desk_demo_ubuntu','研发办公桌面','Ubuntu','demo-node-a',202,'研发专属池','running','unknown','demo'),('desk_demo_kylin','国产办公桌面','麒麟','demo-node-a',203,'通用办公池','stopped','unknown','demo') ON CONFLICT DO NOTHING`, + `INSERT INTO terminals(id,sn,name,group_name,status,agent_version,last_seen,bootstrap_hash) VALUES ('term_demo_001','DEMO-RK-001','设计工位 01','设计部','offline','演示版本',now()-interval '2 hours','disabled'),('term_demo_002','DEMO-RK-002','研发工位 02','研发部','offline','演示版本',now()-interval '3 hours','disabled') ON CONFLICT DO NOTHING`, + `INSERT INTO users(id,username,display_name,terminal_id) VALUES ('user_demo_wang','wang.demo','王工','term_demo_001'),('user_demo_chen','chen.demo','陈工','term_demo_002') ON CONFLICT DO NOTHING`, + `INSERT INTO bindings VALUES ('user_demo_wang','desk_demo_windows'),('user_demo_wang','desk_demo_ubuntu'),('user_demo_chen','desk_demo_ubuntu') ON CONFLICT DO NOTHING`, + `INSERT INTO resources VALUES ('demo-node-a','demo-node-a','online',0.24,0.48,0.35,'demo',now()) ON CONFLICT DO NOTHING`, + } + for _, q := range statements { + if _, err = tx.ExecContext(ctx, q); err != nil { + return fmt.Errorf("demo seed: %w", err) + } + } + return tx.Commit() +} + +func Users(ctx context.Context, db *sqlx.DB) ([]model.User, error) { + users := []model.User{} + if err := db.SelectContext(ctx, &users, "SELECT * FROM users ORDER BY username"); err != nil { + return nil, err + } + for i := range users { + users[i].DesktopIDs = []string{} + if err := db.SelectContext(ctx, &users[i].DesktopIDs, "SELECT desktop_id FROM bindings WHERE user_id=$1 ORDER BY desktop_id", users[i].ID); err != nil { + return nil, err + } + } + return users, nil +} diff --git a/platform/pkg/proxmox/client.go b/platform/pkg/proxmox/client.go new file mode 100644 index 0000000..8fe5e6e --- /dev/null +++ b/platform/pkg/proxmox/client.go @@ -0,0 +1,146 @@ +package proxmox + +import ( + "context" + "crypto/tls" + "crypto/x509" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "strings" + "time" +) + +type Client struct { + BaseURL, TokenID, TokenSecret string + HTTP *http.Client +} +type Resource struct { + Type string `json:"type"` + Node string `json:"node"` + Name string `json:"name"` + Status string `json:"status"` + VMID int `json:"vmid"` + CPU float64 `json:"cpu"` + Memory float64 `json:"mem"` + MaxMemory float64 `json:"maxmem"` + Disk float64 `json:"disk"` + MaxDisk float64 `json:"maxdisk"` + Template int `json:"template"` +} +type TaskStatus struct { + Status string `json:"status"` + ExitStatus string `json:"exitstatus"` +} + +var ErrTaskFailed = errors.New("PVE task failed; inspect task logs in PVE") + +type HTTPError struct{ Status int } + +func (e *HTTPError) Error() string { return fmt.Sprintf("PVE returned HTTP %d", e.Status) } + +func New(base, id, secret, caFile string) (*Client, error) { + u, err := url.Parse(base) + if err != nil || u.Scheme != "https" || u.Host == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" { + return nil, errors.New("PVE_URL must be an HTTPS origin without credentials or query") + } + if u.Path != "" && u.Path != "/" { + return nil, errors.New("PVE_URL must not include a path") + } + pool, err := x509.SystemCertPool() + if err != nil { + pool = x509.NewCertPool() + } + if caFile != "" { + data, err := os.ReadFile(caFile) + if err != nil { + return nil, err + } + if !pool.AppendCertsFromPEM(data) { + return nil, errors.New("invalid PVE CA file") + } + } + return &Client{BaseURL: strings.TrimRight(base, "/"), TokenID: id, TokenSecret: secret, HTTP: &http.Client{Timeout: 15 * time.Second, CheckRedirect: func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse }, Transport: &http.Transport{TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12, RootCAs: pool}}}}, nil +} +func (c *Client) request(ctx context.Context, method, path string, form url.Values, out any) error { + var body io.Reader + if form != nil { + body = strings.NewReader(form.Encode()) + } + req, err := http.NewRequestWithContext(ctx, method, c.BaseURL+"/api2/json"+path, body) + if err != nil { + return err + } + req.Header.Set("Authorization", "PVEAPIToken="+c.TokenID+"="+c.TokenSecret) + if form != nil { + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + } + res, err := c.HTTP.Do(req) + if err != nil { + return fmt.Errorf("PVE request failed: %w", err) + } + defer res.Body.Close() + if res.StatusCode < 200 || res.StatusCode >= 300 { + return &HTTPError{Status: res.StatusCode} + } + var envelope struct { + Data json.RawMessage `json:"data"` + } + if err = json.NewDecoder(io.LimitReader(res.Body, 4<<20)).Decode(&envelope); err != nil { + return err + } + return json.Unmarshal(envelope.Data, out) +} +func (c *Client) Resources(ctx context.Context) ([]Resource, error) { + var list []Resource + err := c.request(ctx, "GET", "/cluster/resources", nil, &list) + return list, err +} +func (c *Client) Action(ctx context.Context, node string, vmid int, action string) (string, error) { + if action != "start" && action != "shutdown" && action != "reboot" { + return "", errors.New("unsupported VM action") + } + var upid string + err := c.request(ctx, "POST", fmt.Sprintf("/nodes/%s/qemu/%d/status/%s", url.PathEscape(node), vmid, action), url.Values{}, &upid) + if err == nil && upid == "" { + err = errors.New("PVE did not return a task ID") + } + return upid, err +} +func (c *Client) Task(ctx context.Context, node, upid string) (TaskStatus, error) { + var status TaskStatus + err := c.request(ctx, "GET", "/nodes/"+url.PathEscape(node)+"/tasks/"+url.PathEscape(upid)+"/status", nil, &status) + return status, err +} +func (c *Client) Power(ctx context.Context, node string, vmid int) (string, error) { + var status struct { + Status string `json:"status"` + } + err := c.request(ctx, "GET", fmt.Sprintf("/nodes/%s/qemu/%d/status/current", url.PathEscape(node), vmid), nil, &status) + return status.Status, err +} +func (c *Client) Wait(ctx context.Context, node, upid string, interval time.Duration) error { + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + s, err := c.Task(ctx, node, upid) + if err != nil { + return err + } + if s.Status == "stopped" { + if s.ExitStatus != "OK" { + return ErrTaskFailed + } + return nil + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-ticker.C: + } + } +} diff --git a/platform/pkg/proxmox/client_test.go b/platform/pkg/proxmox/client_test.go new file mode 100644 index 0000000..9487e00 --- /dev/null +++ b/platform/pkg/proxmox/client_test.go @@ -0,0 +1,77 @@ +package proxmox + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" +) + +func TestTLSAndTaskLifecycle(t *testing.T) { + polls := 0 + ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Authorization") != "PVEAPIToken=vdi@pve!console=secret" { + t.Error("missing PVE token") + } + w.Header().Set("Content-Type", "application/json") + switch { + case strings.HasSuffix(r.URL.Path, "/status/start"): + if r.Method != "POST" { + t.Error("action should POST") + } + w.Write([]byte(`{"data":"UPID:node:123"}`)) + case strings.HasSuffix(r.URL.Path, "/status/current"): + w.Write([]byte(`{"data":{"status":"running"}}`)) + case strings.Contains(r.URL.Path, "/tasks/"): + polls++ + if polls < 2 { + w.Write([]byte(`{"data":{"status":"running"}}`)) + } else { + w.Write([]byte(`{"data":{"status":"stopped","exitstatus":"OK"}}`)) + } + default: + w.WriteHeader(404) + } + })) + defer ts.Close() + c, err := New(ts.URL, "vdi@pve!console", "secret", "") + if err != nil { + t.Fatal(err) + } + if _, err = c.Action(context.Background(), "node", 201, "start"); err == nil { + t.Fatal("untrusted PVE certificate must not be accepted") + } + c.HTTP = ts.Client() + upid, err := c.Action(context.Background(), "node", 201, "start") + if err != nil { + t.Fatal(err) + } + if err = c.Wait(context.Background(), "node", upid, time.Millisecond); err != nil { + t.Fatal(err) + } + if polls != 2 { + t.Fatalf("expected task polling; got %d", polls) + } + if _, err = c.Action(context.Background(), "node", 201, "restore"); err == nil { + t.Fatal("restore must never map to hard reset") + } +} +func TestFailedPVEJobIsNotSuccess(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"data":{"status":"stopped","exitstatus":"ERROR: disk full"}}`)) + })) + defer ts.Close() + c := &Client{BaseURL: ts.URL, HTTP: ts.Client()} + if err := c.Wait(context.Background(), "node", "task", time.Millisecond); err == nil { + t.Fatal("failed PVE job reported as success") + } +} +func TestRejectInsecureOrigin(t *testing.T) { + for _, base := range []string{"http://pve:8006", "https://user:password@pve:8006", "https://pve:8006/other"} { + if _, err := New(base, "id", "secret", ""); err == nil { + t.Fatalf("accepted %s", base) + } + } +} diff --git a/tools/generate-platform-openapi.mjs b/tools/generate-platform-openapi.mjs new file mode 100644 index 0000000..42734f2 --- /dev/null +++ b/tools/generate-platform-openapi.mjs @@ -0,0 +1,98 @@ +import { mkdirSync, writeFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +const string = { type: 'string' }, integer = { type: 'integer' }, number = { type: 'number' }, boolean = { type: 'boolean' } +const date = { type: 'string', format: 'date-time' } +const ref = name => ({ $ref: `#/components/schemas/${name}` }) +const array = items => ({ type: 'array', items }) +const object = (properties, required = Object.keys(properties)) => ({ type: 'object', properties, required, additionalProperties: false }) +const schemas = { + Error: object({ code: string, message: string, requestId: string }), + Administrator: object({ id: string, username: string, displayName: string, role: { type: 'string', enum: ['super_admin', 'operator', 'viewer'] } }), + LoginInput: object({ username: string, password: string }), + LoginResult: object({ accessToken: string, expiresIn: integer, user: ref('Administrator') }), + Desktop: object({ id: string, name: string, os: { type: 'string', enum: ['Windows', 'Ubuntu', '麒麟'] }, node: string, vmid: integer, pool: string, powerState: string, serviceState: string, provider: { type: 'string', enum: ['demo', 'proxmox'] }, endpoint: string, app: string, width: integer, height: integer, fps: integer, updatedAt: date }), + Terminal: object({ id: string, sn: string, name: string, group: string, status: string, agentVersion: string, lastSeen: { ...date, nullable: true }, sessionDesktopId: { ...string, nullable: true }, configVersion: integer, fps: { ...number, nullable: true }, rtt: { ...number, nullable: true } }), + User: object({ id: string, username: string, displayName: string, enabled: boolean, terminalId: { ...string, nullable: true }, desktopIds: array(string) }), + Resource: object({ id: string, name: string, status: string, cpu: number, memory: number, storage: number, provider: string, updatedAt: date }), + Task: object({ id: string, kind: string, action: string, targetId: string, targetName: string, actor: string, status: { type: 'string', enum: ['queued', 'running', 'success', 'failed', 'timeout'] }, message: string, providerRef: string, createdAt: date, updatedAt: date }), + Audit: object({ id: string, actor: string, action: string, target: string, result: string, requestId: string, createdAt: date }), + Overview: object({ terminalCount: integer, onlineTerminals: integer, desktopCount: integer, runningDesktops: integer, activeSessions: integer, failedTasks: integer, mode: string }), + Settings: object({ mode: { type: 'string', enum: ['demo', 'production'] }, computeProvider: string, pveConfigured: boolean, terminalTransport: string, capabilities: array(string) }), + ImportInput: object({ name: string, os: { type: 'string', enum: ['Windows', 'Ubuntu', '麒麟'] }, node: string, vmid: integer, endpoint: string, width: integer, height: integer, fps: integer }, ['name', 'os', 'node', 'vmid']), + DiscoveredVM: object({ name: string, node: string, vmid: integer, status: string }), + EnrollmentInput: object({ sn: string, name: string, group: string }), + Enrollment: object({ id: string, activationSecret: string }), + CreateUserInput: object({ username: string, displayName: string }), + BindingInput: object({ terminalId: { ...string, nullable: true }, desktopIds: array(string), enabled: boolean }), + DesktopAction: object({ action: { type: 'string', enum: ['start', 'shutdown', 'reboot'] } }), + TerminalAction: object({ action: { type: 'string', enum: ['reboot', 'reset-session', 'collect-logs'] } }), + RegisterInput: object({ sn: string, activationSecret: string, agentVersion: string }), + RegisterResult: object({ terminalId: string, accessToken: string, heartbeatInterval: integer }), + HeartbeatInput: object({ agentVersion: string, fps: { ...number, nullable: true }, rtt: { ...number, nullable: true } }, ['agentVersion']), + Accepted: object({ accepted: boolean }), + HeartbeatResult: object({ accepted: boolean, serverTime: date }), + DeviceConfig: object({ configVersion: integer, identityMode: string, commandTransport: string, heartbeatInterval: integer }), + TicketInput: object({ desktopId: string }), + Ticket: object({ ticket: string, desktopId: string, expiresAt: date, credentialMode: string }), + ConsumeInput: object({ ticket: string }), + Connection: object({ desktopId: string, sessionId: string, protocol: string, endpoint: string, app: string, width: integer, height: integer, fps: integer, credentialMode: string }), + DeviceDiagnostics: object({ agentVersion: string, bootId: string, uptimeSeconds: number, kioskState: string, logTail: string }), + SessionInput: object({ sessionId: string, state: { type: 'string', enum: ['connected', 'ended'] } }), + AckInput: object({ status: { type: 'string', enum: ['success', 'failed'] }, message: string, diagnostics: ref('DeviceDiagnostics') }, ['status']), +} +// Older frontends can omit this display field; the API always supplies it. +schemas.Terminal.properties.sessionState = string +for (const name of ['Desktop', 'Terminal', 'User', 'Resource', 'Task', 'Audit', 'DiscoveredVM']) { + schemas[`${name}List`] = object({ items: array(ref(name)), page: integer, pageSize: integer, total: integer, requestId: string }) +} +const json = schema => ({ 'application/json': { schema } }) +const operation = (id, output, { input, status = 200, list = false, param = false, command = false, security = 'adminBearer' } = {}) => ({ + operationId: id, + security: security ? [{ [security]: [] }] : [], + ...(input ? { requestBody: { required: true, content: json(ref(input)) } } : {}), + parameters: [ + ...(param ? [{ name: 'id', in: 'path', required: true, schema: string }] : []), + ...(list ? ['page', 'pageSize', 'keyword', 'status'].map(name => ({ name, in: 'query', schema: name.startsWith('page') ? integer : string })) : []), + ...(command ? [{ name: 'Idempotency-Key', in: 'header', required: true, schema: { type: 'string', maxLength: 100 } }] : []), + ], + responses: { + [status]: { description: 'Successful response', ...(output ? { content: json(ref(output)) } : {}) }, + ...Object.fromEntries([400, 401, 403, 404, 409, 500, 502, 503].map(code => [code, { description: 'Request failed', content: json(ref('Error')) }])), + }, +}) +const paths = { + '/auth/login': { post: operation('login', 'LoginResult', { input: 'LoginInput', security: null }) }, + '/auth/me': { get: operation('me', 'Administrator') }, + '/auth/logout': { post: operation('logout', null, { status: 204 }) }, + '/overview': { get: operation('overview', 'Overview') }, + '/settings': { get: operation('settings', 'Settings') }, + '/resources': { get: operation('resources', 'ResourceList', { list: true }) }, + '/resources/vms': { get: operation('discoverVMs', 'DiscoveredVMList', { list: true }) }, + '/resources/sync': { post: operation('syncResources', 'Task', { status: 202, command: true }) }, + '/desktops': { get: operation('desktops', 'DesktopList', { list: true }) }, + '/desktops/import': { post: operation('importDesktop', 'Desktop', { input: 'ImportInput' }) }, + '/desktops/{id}': { get: operation('desktop', 'Desktop', { param: true }) }, + '/desktops/{id}/actions': { post: operation('desktopAction', 'Task', { input: 'DesktopAction', param: true, status: 202, command: true }) }, + '/terminals': { get: operation('terminals', 'TerminalList', { list: true }), post: operation('enrollTerminal', 'Enrollment', { input: 'EnrollmentInput', status: 201 }) }, + '/terminals/{id}/commands': { post: operation('terminalCommand', 'Task', { input: 'TerminalAction', param: true, status: 202, command: true }) }, + '/users': { get: operation('users', 'UserList', { list: true }), post: operation('createUser', 'User', { input: 'CreateUserInput', status: 201 }) }, + '/users/{id}/bindings': { put: operation('updateBindings', 'User', { input: 'BindingInput', param: true }) }, + '/tasks': { get: operation('tasks', 'TaskList', { list: true }) }, + '/tasks/{id}': { get: operation('task', 'Task', { param: true }) }, + '/tasks/{id}/diagnostics': { get: operation('taskDiagnostics', 'DeviceDiagnostics', { param: true }) }, + '/audit-logs': { get: operation('audits', 'AuditList', { list: true }) }, + '/terminal/register': { post: operation('registerDevice', 'RegisterResult', { input: 'RegisterInput', security: null }) }, + '/terminal/heartbeat': { post: operation('heartbeat', 'HeartbeatResult', { input: 'HeartbeatInput', security: 'deviceBearer' }) }, + '/terminal/config': { get: operation('deviceConfig', 'DeviceConfig', { security: 'deviceBearer' }) }, + '/terminal/desktops': { get: operation('deviceDesktops', 'DesktopList', { security: 'deviceBearer', list: true }) }, + '/terminal/commands': { get: operation('deviceCommands', 'TaskList', { security: 'deviceBearer', list: true }) }, + '/terminal/commands/{id}/ack': { post: operation('deviceAck', 'Accepted', { input: 'AckInput', param: true, security: 'deviceBearer' }) }, + '/terminal/connect-ticket': { post: operation('createTicket', 'Ticket', { input: 'TicketInput', status: 201, security: 'deviceBearer' }) }, + '/terminal/connect-ticket/consume': { post: operation('consumeTicket', 'Connection', { input: 'ConsumeInput', security: 'deviceBearer' }) }, + '/terminal/session': { post: operation('deviceSession', 'Accepted', { input: 'SessionInput', security: 'deviceBearer' }) }, +} +const output = { openapi: '3.0.3', info: { title: 'VDI 管理平台 API', version: '0.1.0', description: '第一阶段接口。平台票据不替代 Sunshine 配对;HTTP 202 只代表已提交。' }, servers: [{ url: '/api/v1' }], paths, components: { schemas, securitySchemes: { adminBearer: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, deviceBearer: { type: 'http', scheme: 'bearer', description: '设备激活后获得的独立令牌,不是管理员 JWT' } } } } +const directory = fileURLToPath(new URL('../docs/openapi/', import.meta.url)) +mkdirSync(directory, { recursive: true }) +writeFileSync(`${directory}/platform.v1.json`, `${JSON.stringify(output, null, 2)}\n`) +console.log('Updated docs/openapi/platform.v1.json') diff --git a/tools/platform-terminal-simulator.py b/tools/platform-terminal-simulator.py new file mode 100644 index 0000000..5c68328 --- /dev/null +++ b/tools/platform-terminal-simulator.py @@ -0,0 +1,53 @@ +"""Exercise device registration/heartbeat/command receipts without controlling real hardware.""" +import argparse +import getpass +import json +import os +import time +from urllib.request import Request, urlopen +from urllib.error import HTTPError + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--server', default='http://127.0.0.1:8088') + parser.add_argument('--sn', required=True) + args = parser.parse_args() + server = args.server.rstrip('/') + token = '' + + def request(path, body=None): + headers = {'Content-Type': 'application/json'} + if token: + headers['Authorization'] = 'Bearer ' + token + req = Request(server + path, data=json.dumps(body).encode() if body is not None else None, headers=headers) + try: + with urlopen(req, timeout=15) as response: + return json.load(response) + except HTTPError as error: + detail = json.load(error) + raise RuntimeError(detail.get('message', 'API request failed')) from None + + if request('/healthz').get('mode') != 'demo': + raise SystemExit('This simulator only runs against VDI_MODE=demo.') + secret = os.environ.get('VDI_DEVICE_ACTIVATION_SECRET') or getpass.getpass('One-time activation secret: ') + result = request('/api/v1/terminal/register', {'sn': args.sn, 'activationSecret': secret, 'agentVersion': 'simulator-0.1'}) + token = result['accessToken'] + print('Registered terminal:', result['terminalId']) + print('Simulator sends heartbeat only; all requested device operations return failed.') + print('Press Ctrl+C to stop. No reboot, session change, or log collection is performed.') + while True: + request('/api/v1/terminal/heartbeat', {'agentVersion': 'simulator-0.1', 'fps': None, 'rtt': None}) + desktops = request('/api/v1/terminal/desktops')['items'] + print('Authorized desktops:', ', '.join(d['name'] for d in desktops) or '(none)') + for command in request('/api/v1/terminal/commands')['items']: + request('/api/v1/terminal/commands/' + command['id'] + '/ack', {'status': 'failed', 'message': 'Device simulator does not execute operations'}) + print('Returned failed receipt for:', command['action'], command['id']) + time.sleep(10) + + +if __name__ == '__main__': + try: + main() + except KeyboardInterrupt: + print('\nSimulator stopped.') diff --git a/tools/prepare-live-config.py b/tools/prepare-live-config.py new file mode 100644 index 0000000..cd7867f --- /dev/null +++ b/tools/prepare-live-config.py @@ -0,0 +1,43 @@ +"""Generate private local configuration and a CA for the device HTTPS channel.""" +import argparse +import datetime +import ipaddress +import json +import pathlib +import secrets +import os +from cryptography import x509 +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.x509.oid import NameOID + +p = argparse.ArgumentParser() +p.add_argument('--ip', required=True) +p.add_argument('--pve-hostname', required=True, help='DNS name present in the PVE certificate') +p.add_argument('--pve-ip', required=True) +args = p.parse_args() +ip = ipaddress.ip_address(args.ip) +root = pathlib.Path(__file__).resolve().parent.parent +target = root / '.local/live' +target.mkdir(parents=True, exist_ok=True) +if (target / '.env').exists(): + raise RuntimeError('Existing configuration retained; edit it deliberately instead of regenerating secrets') +access = json.loads((target / 'pve-access.json').read_text()) +access['PVE_URL']=f'https://{args.pve_hostname}:8006' +now = datetime.datetime.now(datetime.timezone.utc) +key = rsa.generate_private_key(public_exponent=65537, key_size=3072) +name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, 'VDI local device CA')]) +ca = x509.CertificateBuilder().subject_name(name).issuer_name(name).public_key(key.public_key()).serial_number(x509.random_serial_number()).not_valid_before(now-datetime.timedelta(minutes=5)).not_valid_after(now+datetime.timedelta(days=3650)).add_extension(x509.BasicConstraints(ca=True,path_length=0),critical=True).add_extension(x509.KeyUsage(True,False,False,False,False,True,True,False,False),critical=True).sign(key,hashes.SHA256()) +server_key = rsa.generate_private_key(public_exponent=65537, key_size=3072) +server = x509.CertificateBuilder().subject_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME,str(ip))])).issuer_name(name).public_key(server_key.public_key()).serial_number(x509.random_serial_number()).not_valid_before(now-datetime.timedelta(minutes=5)).not_valid_after(now+datetime.timedelta(days=365)).add_extension(x509.BasicConstraints(ca=False,path_length=None),critical=True).add_extension(x509.SubjectAlternativeName([x509.IPAddress(ip),x509.DNSName('localhost')]),critical=False).add_extension(x509.ExtendedKeyUsage([x509.oid.ExtendedKeyUsageOID.SERVER_AUTH]),critical=False).sign(key,hashes.SHA256()) +tls = target / 'tls'; tls.mkdir(exist_ok=True) +def save_private(path, data): + path.write_bytes(data); os.chmod(path, 0o600) +for name, obj in [('ca',key),('server',server_key)]: + save_private(tls/f'{name}-key.pem', obj.private_bytes(serialization.Encoding.PEM,serialization.PrivateFormat.PKCS8,serialization.NoEncryption())) +(tls/'ca.pem').write_bytes(ca.public_bytes(serialization.Encoding.PEM)) +(tls/'server.pem').write_bytes(server.public_bytes(serialization.Encoding.PEM)+ca.public_bytes(serialization.Encoding.PEM)) +env = dict(VDI_DB_PASSWORD=secrets.token_hex(24),VDI_JWT_SECRET=secrets.token_hex(48),VDI_ADMIN_USERNAME='admin',VDI_ADMIN_PASSWORD=secrets.token_urlsafe(24),VDI_LIVE_BIND_IP=str(ip),PVE_HOSTNAME=args.pve_hostname,PVE_HOST_IP=str(ipaddress.ip_address(args.pve_ip)),**access) +save_private(target/'.env', ''.join(f'{k}={v}\n' for k,v in env.items()).encode()) +save_private(target/'admin-login.txt', f'Console: http://127.0.0.1:5182\nUsername: {env["VDI_ADMIN_USERNAME"]}\nPassword: {env["VDI_ADMIN_PASSWORD"]}\nDevice HTTPS: https://{ip}:8443\n'.encode()) +print('Private environment, admin login, CA and HTTPS certificate generated in .local/live') diff --git a/tools/provision-pve-access.py b/tools/provision-pve-access.py new file mode 100644 index 0000000..b744b9c --- /dev/null +++ b/tools/provision-pve-access.py @@ -0,0 +1,62 @@ +"""Create scoped VDI API access through a known-host-verified SSH connection. + +Requires paramiko. Password is prompted or read from VDI_PVE_SSH_PASSWORD. +The one-time API secret is written only to the requested private output file. +""" +import argparse +import getpass +import json +import os +import pathlib +import shlex +import paramiko + + +def main(): + p = argparse.ArgumentParser() + p.add_argument('--host', required=True) + p.add_argument('--vmids', nargs='+', type=int, required=True) + p.add_argument('--output', required=True) + args = p.parse_args() + target = pathlib.Path(args.output) + if target.exists(): + raise RuntimeError('Output already exists; reuse the existing token rather than replace it') + ssh = paramiko.SSHClient() + ssh.load_system_host_keys(str(pathlib.Path.home() / '.ssh/known_hosts')) + ssh.set_missing_host_key_policy(paramiko.RejectPolicy()) + password = os.environ.get('VDI_PVE_SSH_PASSWORD') or getpass.getpass('PVE SSH password: ') + ssh.connect(args.host, username='root', password=password, look_for_keys=False, allow_agent=False, timeout=10) + def run(*parts): + _, out, err = ssh.exec_command(shlex.join(parts), timeout=30) + body = out.read().decode() + if out.channel.recv_exit_status() != 0: + raise RuntimeError(err.read().decode()) + return body + try: + user, role, token = 'vdi-cloud@pve', 'VDICloudPower', 'console' + users = json.loads(run('pveum', 'user', 'list', '--output-format', 'json')) + if any(u['userid'] == user for u in users): + raise RuntimeError('VDI account already exists. Inspect it and recover the existing token first') + roles = json.loads(run('pveum', 'role', 'list', '--output-format', 'json')) + if not any(r['roleid'] == role for r in roles): + run('pveum', 'role', 'add', role, '--privs', 'VM.Audit VM.PowerMgmt') + elif set(next(r for r in roles if r['roleid'] == role)['privs'].split(',')) != {'VM.Audit','VM.PowerMgmt'}: + raise RuntimeError('Existing VDICloudPower role has unexpected privileges; inspect it first') + run('pveum', 'user', 'add', user, '--comment', 'VDI cloud API; scoped power management') + run('pveum', 'acl', 'modify', '/', '--users', user, '--roles', 'PVEAuditor') + for vmid in args.vmids: + run('pveum', 'acl', 'modify', f'/vms/{vmid}', '--users', user, '--roles', role) + result = json.loads(run('pveum', 'user', 'token', 'add', user, token, '--privsep', '0', '--output-format', 'json')) + target.parent.mkdir(parents=True, exist_ok=True) + with target.open('x', encoding='utf-8') as f: + json.dump({'PVE_URL': f'https://{args.host}:8006', 'PVE_TOKEN_ID': result['full-tokenid'], 'PVE_TOKEN_SECRET': result['value']}, f) + os.chmod(target, 0o600) + with ssh.open_sftp() as sftp: + sftp.get('/etc/pve/pve-root-ca.pem', str(target.parent / 'pve-ca.pem')) + print(f'Created dedicated account and VM power scopes {args.vmids}; credentials saved privately') + finally: + ssh.close() + + +if __name__ == '__main__': + main() diff --git a/tools/start-cloud-dev.ps1 b/tools/start-cloud-dev.ps1 new file mode 100644 index 0000000..14329bc --- /dev/null +++ b/tools/start-cloud-dev.ps1 @@ -0,0 +1,42 @@ +param([switch]$SkipInstall, [int]$ApiPort=8088, [int]$ConsolePort=5180) +$ErrorActionPreference = 'Stop' +$vdiWorkspace = Split-Path -Parent $PSScriptRoot +$vdiLocalDir = Join-Path $vdiWorkspace '.local' +New-Item -ItemType Directory -Force -Path $vdiLocalDir | Out-Null +$vdiGoCommand = Get-Command go -ErrorAction SilentlyContinue +$vdiGoPath = if ($vdiGoCommand) { $vdiGoCommand.Source } else { Join-Path $env:LOCALAPPDATA 'vdi-cloud-dev\go\bin\go.exe' } +if (!(Test-Path -LiteralPath $vdiGoPath)) { throw 'Go 1.25+ is required. Install Go and add it to PATH.' } +foreach ($vdiPort in @($ApiPort,$ConsolePort)) { + if (Get-NetTCPConnection -State Listen -LocalPort $vdiPort -ErrorAction SilentlyContinue) { throw "Port $vdiPort is already in use. Stop the existing development instance before starting another." } +} +Push-Location $vdiWorkspace +try { + docker compose -f deploy/compose.dev.yml up -d postgres + if ($LASTEXITCODE -ne 0) { throw 'PostgreSQL startup failed' } + $vdiDatabaseReady = $false + for ($vdiAttempt = 0; $vdiAttempt -lt 40; $vdiAttempt++) { + docker exec vdi-cloud-dev-postgres-1 pg_isready -U vdi -d vdi 2>$null | Out-Null + if ($LASTEXITCODE -eq 0) { $vdiDatabaseReady = $true; break } + Start-Sleep -Milliseconds 500 + } + if (!$vdiDatabaseReady) { throw 'PostgreSQL did not become ready' } + Push-Location platform + try { & $vdiGoPath build -o (Join-Path $vdiLocalDir 'vdi-api.exe') ./cmd/api-gateway; if ($LASTEXITCODE -ne 0) { throw 'API build failed' } } finally { Pop-Location } + if (!$SkipInstall) { Push-Location console; try { npm.cmd ci; if ($LASTEXITCODE -ne 0) { throw 'Console install failed' } } finally { Pop-Location } } + $vdiEnvironment = @{ + VDI_MODE = 'demo'; VDI_LISTEN = "127.0.0.1:$ApiPort"; + DATABASE_URL = 'postgres://vdi:vdi-local-demo@127.0.0.1:55438/vdi?sslmode=disable'; + VDI_JWT_SECRET = 'local-demo-only-change-before-any-production-use-2026'; + PVE_URL = ''; PVE_TOKEN_ID = ''; PVE_TOKEN_SECRET = ''; PVE_CA_FILE = ''; VITE_API_MODE = 'api'; VDI_API_PROXY = "http://127.0.0.1:$ApiPort" + } + $vdiOriginalEnvironment = @{} + try { + foreach ($vdiKey in $vdiEnvironment.Keys) { $vdiOriginalEnvironment[$vdiKey] = [Environment]::GetEnvironmentVariable($vdiKey,'Process'); [Environment]::SetEnvironmentVariable($vdiKey,$vdiEnvironment[$vdiKey],'Process') } + $vdiAPIProcess = Start-Process -FilePath (Join-Path $vdiLocalDir 'vdi-api.exe') -WorkingDirectory $vdiWorkspace -WindowStyle Hidden -RedirectStandardOutput (Join-Path $vdiLocalDir 'api.stdout.log') -RedirectStandardError (Join-Path $vdiLocalDir 'api.stderr.log') -PassThru + $vdiConsoleProcess = Start-Process -FilePath 'cmd.exe' -ArgumentList '/d','/c',"npm.cmd run dev -- --port $ConsolePort" -WorkingDirectory (Join-Path $vdiWorkspace 'console') -WindowStyle Hidden -RedirectStandardOutput (Join-Path $vdiLocalDir 'console.stdout.log') -RedirectStandardError (Join-Path $vdiLocalDir 'console.stderr.log') -PassThru + @{ apiPid = $vdiAPIProcess.Id; consolePid = $vdiConsoleProcess.Id; startedAt = (Get-Date).ToString('o') } | ConvertTo-Json | Set-Content (Join-Path $vdiLocalDir 'cloud-dev.json') + } finally { foreach ($vdiKey in $vdiOriginalEnvironment.Keys) { [Environment]::SetEnvironmentVariable($vdiKey,$vdiOriginalEnvironment[$vdiKey],'Process') } } + Write-Host "Console: http://127.0.0.1:$ConsolePort" + Write-Host 'Demo users: admin / operator / viewer; password: Demo@2026' + Write-Host 'Development process IDs and logs: .local/' +} finally { Pop-Location } diff --git a/tools/start-cloud-live.ps1 b/tools/start-cloud-live.ps1 new file mode 100644 index 0000000..b2cd4a0 --- /dev/null +++ b/tools/start-cloud-live.ps1 @@ -0,0 +1,13 @@ +param([switch]$Build) +$ErrorActionPreference='Stop' +$vdiRoot=Split-Path -Parent $PSScriptRoot +Push-Location $vdiRoot +try { + if (!(Test-Path -LiteralPath '.local/live/.env')) { throw 'Configure PVE access and run tools/prepare-live-config.py first. See docs/cloud-live-integration.md.' } + $vdiArgs=@('compose','--env-file','.local/live/.env','-f','deploy/compose.live.yml','up','-d') + if ($Build) { $vdiArgs+='--build' } + & docker @vdiArgs + if ($LASTEXITCODE -ne 0) { throw 'Live platform startup failed' } + Write-Host 'Real platform: http://127.0.0.1:5182' + Write-Host 'Private initial administrator credentials: .local/live/admin-login.txt' +} finally { Pop-Location }