From f70a9a85c89129929d34fd4f2153acee1c0932f6 Mon Sep 17 00:00:00 2001 From: liuyuanchuang Date: Fri, 6 Mar 2026 14:30:30 +0800 Subject: [PATCH] feat: add google oauth --- deploy_dev.sh | 40 +- e2e/auth-email.spec.ts | 57 + e2e/auth-oauth.spec.ts | 80 + package-lock.json | 2294 ++++++++++++++++- package.json | 12 +- playwright.config.ts | 24 + src/App.tsx | 57 +- src/components/AuthModal.tsx | 172 +- src/components/LeftSidebar.tsx | 45 +- src/components/UserGuide.tsx | 21 +- src/components/__tests__/App.test.tsx | 112 + src/components/__tests__/AuthModal.test.tsx | 103 + src/contexts/AuthContext.tsx | 239 +- .../__tests__/AuthContext.oauth.test.tsx | 207 ++ src/contexts/authMachine.ts | 135 + src/lib/api.ts | 6 +- src/lib/authService.ts | 168 +- src/lib/translations.ts | 18 + src/main.tsx | 15 +- src/pages/AuthCallbackPage.tsx | 81 + src/routes/AppRouter.tsx | 12 + src/test/setup.ts | 1 + src/types/api.ts | 16 + vitest.config.ts | 12 + 24 files changed, 3593 insertions(+), 334 deletions(-) create mode 100644 e2e/auth-email.spec.ts create mode 100644 e2e/auth-oauth.spec.ts create mode 100644 playwright.config.ts create mode 100644 src/components/__tests__/App.test.tsx create mode 100644 src/components/__tests__/AuthModal.test.tsx create mode 100644 src/contexts/__tests__/AuthContext.oauth.test.tsx create mode 100644 src/contexts/authMachine.ts create mode 100644 src/pages/AuthCallbackPage.tsx create mode 100644 src/routes/AppRouter.tsx create mode 100644 src/test/setup.ts create mode 100644 vitest.config.ts diff --git a/deploy_dev.sh b/deploy_dev.sh index af8605f..4abb93a 100755 --- a/deploy_dev.sh +++ b/deploy_dev.sh @@ -66,57 +66,57 @@ deploy_to_server() { print_info "在 ${server} 上执行部署操作..." print_info "部署路径: ${DEPLOY_PATH}/${DEPLOY_NAME}" # 注意:密码通过环境变量传递,避免在命令行中暴露 - ssh_output=$(SSH_SUDO_PASSWORD="${SUDO_PASSWORD}" ssh ${server} bash << SSH_EOF + ssh_output=$(ssh "${server}" "SSH_SUDO_PASSWORD='${SUDO_PASSWORD}' SSH_DEPLOY_PATH='${DEPLOY_PATH}' SSH_DEPLOY_NAME='${DEPLOY_NAME}' bash -s" << 'SSH_EOF' set -e - DEPLOY_PATH="${DEPLOY_PATH}" - DEPLOY_NAME="${DEPLOY_NAME}" - SUDO_PASSWORD="\${SSH_SUDO_PASSWORD}" + DEPLOY_PATH="${SSH_DEPLOY_PATH}" + DEPLOY_NAME="${SSH_DEPLOY_NAME}" + SUDO_PASSWORD="${SSH_SUDO_PASSWORD}" # 检查部署目录是否存在 - if [ ! -d "\${DEPLOY_PATH}" ]; then - echo "错误:部署目录 \${DEPLOY_PATH} 不存在,请检查路径是否正确" + if [ ! -d "${DEPLOY_PATH}" ]; then + echo "错误:部署目录 ${DEPLOY_PATH} 不存在,请检查路径是否正确" exit 1 fi # 检查是否有权限写入(尝试创建测试文件) - if ! touch "\${DEPLOY_PATH}/.deploy_test" 2>/dev/null; then + if ! touch "${DEPLOY_PATH}/.deploy_test" 2>/dev/null; then echo "提示:没有直接写入权限,将使用 sudo 执行操作" USE_SUDO=1 else - rm -f "\${DEPLOY_PATH}/.deploy_test" + rm -f "${DEPLOY_PATH}/.deploy_test" USE_SUDO=0 fi # 备份旧版本(如果存在) - if [ -d "\${DEPLOY_PATH}/\${DEPLOY_NAME}" ]; then + if [ -d "${DEPLOY_PATH}/${DEPLOY_NAME}" ]; then echo "备份旧版本..." - if [ "\$USE_SUDO" = "1" ]; then - echo "\${SUDO_PASSWORD}" | sudo -S rm -rf "\${DEPLOY_PATH}/\${DEPLOY_NAME}_bak" 2>/dev/null || true - echo "\${SUDO_PASSWORD}" | sudo -S mv "\${DEPLOY_PATH}/\${DEPLOY_NAME}" "\${DEPLOY_PATH}/\${DEPLOY_NAME}_bak" || { echo "错误:备份失败,权限不足"; exit 1; } + if [ "$USE_SUDO" = "1" ]; then + echo "${SUDO_PASSWORD}" | sudo -S rm -rf "${DEPLOY_PATH}/${DEPLOY_NAME}_bak" 2>/dev/null || true + echo "${SUDO_PASSWORD}" | sudo -S mv "${DEPLOY_PATH}/${DEPLOY_NAME}" "${DEPLOY_PATH}/${DEPLOY_NAME}_bak" || { echo "错误:备份失败,权限不足"; exit 1; } else - rm -rf "\${DEPLOY_PATH}/\${DEPLOY_NAME}_bak" 2>/dev/null || true - mv "\${DEPLOY_PATH}/\${DEPLOY_NAME}" "\${DEPLOY_PATH}/\${DEPLOY_NAME}_bak" || { echo "错误:备份失败"; exit 1; } + rm -rf "${DEPLOY_PATH}/${DEPLOY_NAME}_bak" 2>/dev/null || true + mv "${DEPLOY_PATH}/${DEPLOY_NAME}" "${DEPLOY_PATH}/${DEPLOY_NAME}_bak" || { echo "错误:备份失败"; exit 1; } fi fi # 移动新版本到部署目录(覆盖现有目录) - if [ -d ~/\${DEPLOY_NAME} ]; then + if [ -d ~/${DEPLOY_NAME} ]; then echo "移动新版本到部署目录..." - if [ "\$USE_SUDO" = "1" ]; then - echo "\${SUDO_PASSWORD}" | sudo -S mv ~/\${DEPLOY_NAME} "\${DEPLOY_PATH}/" || { echo "错误:移动文件失败,权限不足"; exit 1; } + if [ "$USE_SUDO" = "1" ]; then + echo "${SUDO_PASSWORD}" | sudo -S mv ~/${DEPLOY_NAME} "${DEPLOY_PATH}/" || { echo "错误:移动文件失败,权限不足"; exit 1; } else - mv ~/\${DEPLOY_NAME} "\${DEPLOY_PATH}/" || { echo "错误:移动文件失败"; exit 1; } + mv ~/${DEPLOY_NAME} "${DEPLOY_PATH}/" || { echo "错误:移动文件失败"; exit 1; } fi echo "部署完成!" else - echo "错误:找不到 ~/\${DEPLOY_NAME} 目录" + echo "错误:找不到 ~/${DEPLOY_NAME} 目录" exit 1 fi # 重新加载 nginx(如果配置了) if command -v nginx &> /dev/null; then echo "重新加载 nginx..." - echo "\${SUDO_PASSWORD}" | sudo -S nginx -t && echo "\${SUDO_PASSWORD}" | sudo -S nginx -s reload || echo "警告:nginx 重新加载失败,请手动检查" + echo "${SUDO_PASSWORD}" | sudo -S nginx -t && echo "${SUDO_PASSWORD}" | sudo -S nginx -s reload || echo "警告:nginx 重新加载失败,请手动检查" fi SSH_EOF ) diff --git a/e2e/auth-email.spec.ts b/e2e/auth-email.spec.ts new file mode 100644 index 0000000..6497a6f --- /dev/null +++ b/e2e/auth-email.spec.ts @@ -0,0 +1,57 @@ +import { expect, test } from '@playwright/test'; + +const jwtPayload = Buffer.from( + JSON.stringify({ user_id: 7, email: 'user@example.com', exp: 1999999999, iat: 1111111 }) +) + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/g, ''); + +const token = `header.${jwtPayload}.sig`; + +test('email login should authenticate and display user email', async ({ page }) => { + await page.route('**/user/login', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + request_id: 'req_login', + code: 200, + message: 'ok', + data: { + token, + expires_at: 1999999999, + }, + }), + }); + }); + + await page.route('**/task/list**', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + request_id: 'req_tasks', + code: 200, + message: 'ok', + data: { + task_list: [], + total: 0, + }, + }), + }); + }); + + await page.goto('/'); + + const loginButton = page.getByRole('button', { name: /Login|登录/ }).first(); + await loginButton.click(); + + await page.fill('#auth-email', 'user@example.com'); + await page.fill('#auth-password', '123456'); + + await page.locator('button[type="submit"]').click(); + + await expect(page.getByText('user@example.com')).toBeVisible(); +}); diff --git a/e2e/auth-oauth.spec.ts b/e2e/auth-oauth.spec.ts new file mode 100644 index 0000000..9d9aa9a --- /dev/null +++ b/e2e/auth-oauth.spec.ts @@ -0,0 +1,80 @@ +import { expect, test } from '@playwright/test'; + +const jwtPayload = Buffer.from( + JSON.stringify({ user_id: 9, email: 'oauth@example.com', exp: 1999999999, iat: 1111111 }) +) + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/g, ''); + +const token = `header.${jwtPayload}.sig`; + +test('google oauth callback with valid state should complete login', async ({ page }) => { + await page.route('**/user/oauth/google/url**', async (route, request) => { + const url = new URL(request.url()); + const state = url.searchParams.get('state') ?? ''; + + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + request_id: 'req_oauth_url', + code: 200, + message: 'ok', + data: { + auth_url: `http://127.0.0.1:4173/auth/google/callback?code=oauth_code&state=${state}`, + }, + }), + }); + }); + + await page.route('**/user/oauth/google/callback', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + request_id: 'req_oauth_callback', + code: 200, + message: 'ok', + data: { + token, + expires_at: 1999999999, + }, + }), + }); + }); + + await page.route('**/task/list**', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + request_id: 'req_tasks', + code: 200, + message: 'ok', + data: { + task_list: [], + total: 0, + }, + }), + }); + }); + + await page.goto('/'); + await page.getByRole('button', { name: /Login|登录/ }).first().click(); + await page.getByRole('button', { name: /Google/ }).click(); + + await expect(page.getByText('oauth@example.com')).toBeVisible(); +}); + +test('google oauth callback with invalid state should show error', async ({ page }) => { + await page.goto('/'); + + await page.evaluate(() => { + sessionStorage.setItem('texpixel_oauth_state', 'expected_state'); + }); + + await page.goto('/auth/google/callback?code=fake_code&state=wrong_state'); + await expect(page.getByText('OAuth state 校验失败')).toBeVisible(); +}); diff --git a/package-lock.json b/package-lock.json index 56ad008..68bfc95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "react-dom": "^18.3.1", "react-hot-toast": "^2.6.0", "react-markdown": "^10.1.0", + "react-router-dom": "^7.13.1", "rehype-katex": "^7.0.1", "remark-breaks": "^4.0.0", "remark-math": "^6.0.0", @@ -28,7 +29,11 @@ }, "devDependencies": { "@eslint/js": "^9.9.1", + "@playwright/test": "^1.58.2", "@tailwindcss/typography": "^0.5.19", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", @@ -37,13 +42,29 @@ "eslint-plugin-react-hooks": "^5.1.0-rc.0", "eslint-plugin-react-refresh": "^0.4.11", "globals": "^15.9.0", + "jsdom": "^28.1.0", "postcss": "^8.4.35", "tailwindcss": "^3.4.1", "typescript": "^5.5.3", "typescript-eslint": "^8.3.0", - "vite": "^5.4.2" + "vite": "^5.4.2", + "vitest": "^4.0.18" } }, + "node_modules/@acemir/cssom": { + "version": "0.9.31", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", + "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -69,6 +90,64 @@ "node": ">=6.0.0" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz", + "integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.1.1", + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", + "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.6" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -296,6 +375,16 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", @@ -352,6 +441,151 @@ "node": ">=6.9.0" } }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.0.tgz", + "integrity": "sha512-H4tuz2nhWgNKLt1inYpoVCfbJbMwX/lQKp3g69rrrIMIYlFD9+zTykOKhNR8uGrAmbS/kT9n6hTFkmDkxLgeTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0" + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -624,6 +858,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/netbsd-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", @@ -640,6 +891,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/openbsd-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", @@ -656,6 +924,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", @@ -859,6 +1144,24 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@humanfs/core": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.0.tgz", @@ -957,10 +1260,11 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", @@ -1017,214 +1321,379 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", - "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", - "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", - "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", - "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", - "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", - "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", - "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", - "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", - "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", - "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", - "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", - "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", - "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", - "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", - "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", - "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@supabase/auth-js": { "version": "2.71.1", "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.71.1.tgz", @@ -1319,6 +1788,104 @@ "node": ">=4" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1360,6 +1927,17 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@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/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -1369,10 +1947,18 @@ "@types/ms": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" }, "node_modules/@types/estree-jsx": { "version": "1.0.5", @@ -1731,6 +2317,90 @@ "vite": "^4.2.0 || ^5.0.0" } }, + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", @@ -1752,6 +2422,16 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/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.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1780,6 +2460,20 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -1811,6 +2505,26 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/autoprefixer": { "version": "10.4.20", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", @@ -1864,6 +2578,16 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -1988,6 +2712,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/character-entities": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", @@ -2104,6 +2838,19 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2119,6 +2866,27 @@ "node": ">= 8" } }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -2131,11 +2899,89 @@ "node": ">=4" } }, + "node_modules/cssstyle": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.2.0.tgz", + "integrity": "sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.0.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.28", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.6" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cssstyle/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", @@ -2152,6 +2998,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/decode-named-character-reference": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", @@ -2205,6 +3058,14 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "dev": true }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2235,6 +3096,13 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -2545,6 +3413,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/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.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2554,6 +3432,16 @@ "node": ">=0.10.0" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -3000,6 +3888,19 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/html-to-image": { "version": "1.11.13", "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz", @@ -3016,6 +3917,34 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/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.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3050,6 +3979,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/inline-style-parser": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", @@ -3178,6 +4117,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3226,6 +4172,98 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "28.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz", + "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.31", + "@asamuzakjp/dom-selector": "^6.8.1", + "@bramus/specificity": "^2.4.2", + "@exodus/bytes": "^1.11.0", + "cssstyle": "^6.0.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "undici": "^7.21.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/jsesc": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", @@ -3392,6 +4430,27 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/mathml2omml": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/mathml2omml/-/mathml2omml-0.5.0.tgz", @@ -3600,6 +4659,13 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -4083,6 +5149,16 @@ "node": ">=8.6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4187,6 +5263,17 @@ "node": ">= 6" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -4335,6 +5422,13 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -4371,10 +5465,57 @@ "node": ">= 6" } }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { - "version": "8.4.47", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", - "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "dev": true, "funding": [ { @@ -4390,9 +5531,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.1.0", + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, "engines": { @@ -4523,6 +5665,33 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/property-information": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", @@ -4602,6 +5771,14 @@ "react-dom": ">=16" } }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/react-markdown": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", @@ -4638,6 +5815,44 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz", + "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz", + "integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==", + "license": "MIT", + "dependencies": { + "react-router": "7.13.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -4659,6 +5874,20 @@ "node": ">=8.10.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/rehype-katex": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.1.tgz", @@ -4742,6 +5971,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/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": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -4779,12 +6018,13 @@ } }, "node_modules/rollup": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", - "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, + "license": "MIT", "dependencies": { - "@types/estree": "1.0.6" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -4794,22 +6034,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.24.0", - "@rollup/rollup-android-arm64": "4.24.0", - "@rollup/rollup-darwin-arm64": "4.24.0", - "@rollup/rollup-darwin-x64": "4.24.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", - "@rollup/rollup-linux-arm-musleabihf": "4.24.0", - "@rollup/rollup-linux-arm64-gnu": "4.24.0", - "@rollup/rollup-linux-arm64-musl": "4.24.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", - "@rollup/rollup-linux-riscv64-gnu": "4.24.0", - "@rollup/rollup-linux-s390x-gnu": "4.24.0", - "@rollup/rollup-linux-x64-gnu": "4.24.0", - "@rollup/rollup-linux-x64-musl": "4.24.0", - "@rollup/rollup-win32-arm64-msvc": "4.24.0", - "@rollup/rollup-win32-ia32-msvc": "4.24.0", - "@rollup/rollup-win32-x64-msvc": "4.24.0", + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" } }, @@ -4836,6 +6085,19 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -4853,6 +6115,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -4874,6 +6142,13 @@ "node": ">=8" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -4911,6 +6186,20 @@ "integrity": "sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==", "license": "(WTFPL OR MIT)" }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -5021,6 +6310,19 @@ "node": ">=8" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -5085,6 +6387,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwind-merge": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", @@ -5159,6 +6468,101 @@ "node": ">=0.8" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/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/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.24", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.24.tgz", + "integrity": "sha512-1r6vQTTt1rUiJkI5vX7KG8PR342Ru/5Oh13kEQP2SMbRSZpOey9SrBe27IDxkoWulx8ShWu4K6C0BkctP8Z1bQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.24" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.24", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.24.tgz", + "integrity": "sha512-pj7yygNMoMRqG7ML2SDQ0xNIOfN3IBDUcPVM2Sg6hP96oFNN2nqnzHreT3z9xLq85IWJyNTvD38O002DdOrPMw==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5171,6 +6575,19 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -5262,6 +6679,16 @@ } } }, + "node_modules/undici": { + "version": "7.22.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", + "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "7.10.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", @@ -5535,6 +6962,663 @@ } } }, + "node_modules/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "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/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/vitest/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/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/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.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/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", @@ -5550,6 +7634,16 @@ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -5574,6 +7668,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -5724,6 +7835,23 @@ } } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/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/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 877f2be..7ca436f 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,9 @@ "build:dev": "VITE_ENV=development vite build", "build:prod": "VITE_ENV=production vite build", "lint": "eslint .", + "test": "vitest run", + "test:watch": "vitest", + "test:e2e": "playwright test", "preview": "vite preview", "typecheck": "tsc --noEmit -p tsconfig.app.json" }, @@ -25,6 +28,7 @@ "react-dom": "^18.3.1", "react-hot-toast": "^2.6.0", "react-markdown": "^10.1.0", + "react-router-dom": "^7.13.1", "rehype-katex": "^7.0.1", "remark-breaks": "^4.0.0", "remark-math": "^6.0.0", @@ -33,7 +37,11 @@ }, "devDependencies": { "@eslint/js": "^9.9.1", + "@playwright/test": "^1.58.2", "@tailwindcss/typography": "^0.5.19", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", @@ -42,10 +50,12 @@ "eslint-plugin-react-hooks": "^5.1.0-rc.0", "eslint-plugin-react-refresh": "^0.4.11", "globals": "^15.9.0", + "jsdom": "^28.1.0", "postcss": "^8.4.35", "tailwindcss": "^3.4.1", "typescript": "^5.5.3", "typescript-eslint": "^8.3.0", - "vite": "^5.4.2" + "vite": "^5.4.2", + "vitest": "^4.0.18" } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..53e1fc5 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,24 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './e2e', + fullyParallel: true, + retries: 0, + reporter: 'list', + use: { + baseURL: 'http://127.0.0.1:4173', + trace: 'on-first-retry', + }, + webServer: { + command: 'npm run dev -- --host 127.0.0.1 --port 4173', + url: 'http://127.0.0.1:4173', + reuseExistingServer: true, + timeout: 120000, + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}); diff --git a/src/App.tsx b/src/App.tsx index 3b1ce93..9b0dbd5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,8 +10,11 @@ import FilePreview from './components/FilePreview'; import ResultPanel from './components/ResultPanel'; import UploadModal from './components/UploadModal'; import UserGuide from './components/UserGuide'; +import AuthModal from './components/AuthModal'; const PAGE_SIZE = 6; +const GUEST_USAGE_LIMIT = 3; +const GUEST_USAGE_COUNT_KEY = 'texpixel_guest_usage_count'; function App() { const { user, initializing } = useAuth(); @@ -21,7 +24,13 @@ function App() { const [selectedResult, setSelectedResult] = useState(null); const [showUploadModal, setShowUploadModal] = useState(false); const [showUserGuide, setShowUserGuide] = useState(false); + const [showAuthModal, setShowAuthModal] = useState(false); const [loading, setLoading] = useState(false); + const [guestUsageCount, setGuestUsageCount] = useState(() => { + const storedCount = localStorage.getItem(GUEST_USAGE_COUNT_KEY); + const parsedCount = storedCount ? Number.parseInt(storedCount, 10) : 0; + return Number.isFinite(parsedCount) ? parsedCount : 0; + }); // Pagination state const [currentPage, setCurrentPage] = useState(1); @@ -44,6 +53,19 @@ function App() { const hasLoadedFiles = useRef(false); const selectedFile = files.find((f) => f.id === selectedFileId) || null; + const canUploadAnonymously = !user && guestUsageCount < GUEST_USAGE_LIMIT; + + const openAuthModal = useCallback(() => { + setShowAuthModal(true); + }, []); + + const incrementGuestUsage = useCallback(() => { + setGuestUsageCount((prev) => { + const nextCount = prev + 1; + localStorage.setItem(GUEST_USAGE_COUNT_KEY, String(nextCount)); + return nextCount; + }); + }, []); useEffect(() => { const handleStartGuide = () => setShowUserGuide(true); @@ -97,6 +119,10 @@ function App() { const handlePaste = (e: ClipboardEvent) => { // If modal is open, let the modal handle paste events to avoid double upload if (showUploadModal) return; + if (!user && guestUsageCount >= GUEST_USAGE_LIMIT) { + openAuthModal(); + return; + } const items = e.clipboardData?.items; if (!items) return; @@ -116,7 +142,7 @@ function App() { document.addEventListener('paste', handlePaste); return () => document.removeEventListener('paste', handlePaste); - }, [user, showUploadModal]); + }, [guestUsageCount, openAuthModal, showUploadModal, user]); const startResizing = useCallback((mouseDownEvent: React.MouseEvent) => { mouseDownEvent.preventDefault(); @@ -391,8 +417,15 @@ function App() { }; const handleUpload = async (uploadFiles: File[]) => { + if (!user && guestUsageCount >= GUEST_USAGE_LIMIT) { + openAuthModal(); + return; + } + setLoading(true); try { + let successfulUploads = 0; + for (const file of uploadFiles) { // 1. Upload file to OSS (or check duplicate) const fileHash = await uploadService.calculateMD5(file); @@ -430,6 +463,12 @@ function App() { if (taskData.task_no) { startPolling(taskData.task_no, fileId); } + + successfulUploads += 1; + } + + if (!user && successfulUploads > 0) { + incrementGuestUsage(); } } catch (error) { console.error('Error uploading files:', error); @@ -464,7 +503,15 @@ function App() { files={files} selectedFileId={selectedFileId} onFileSelect={setSelectedFileId} - onUploadClick={() => setShowUploadModal(true)} + onUploadClick={() => { + if (!user && guestUsageCount >= GUEST_USAGE_LIMIT) { + openAuthModal(); + return; + } + setShowUploadModal(true); + }} + canUploadAnonymously={canUploadAnonymously} + onRequireAuth={openAuthModal} isCollapsed={sidebarCollapsed} onToggleCollapse={() => setSidebarCollapsed(!sidebarCollapsed)} onUploadFiles={handleUpload} @@ -502,6 +549,12 @@ function App() { /> )} + {showAuthModal && ( + setShowAuthModal(false)} + /> + )} + setShowUserGuide(false)} diff --git a/src/components/AuthModal.tsx b/src/components/AuthModal.tsx index c0665e1..590f5d7 100644 --- a/src/components/AuthModal.tsx +++ b/src/components/AuthModal.tsx @@ -1,41 +1,55 @@ -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import { X } from 'lucide-react'; import { useAuth } from '../contexts/AuthContext'; import { useLanguage } from '../contexts/LanguageContext'; interface AuthModalProps { onClose: () => void; + mandatory?: boolean; } -export default function AuthModal({ onClose }: AuthModalProps) { - const { signIn, signUp } = useAuth(); +export default function AuthModal({ onClose, mandatory = false }: AuthModalProps) { + const { signIn, signUp, beginGoogleOAuth, authPhase, authError } = useAuth(); const { t } = useLanguage(); - const [isSignUp, setIsSignUp] = useState(false); + + const [mode, setMode] = useState<'signin' | 'signup'>('signin'); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [localError, setLocalError] = useState(''); + + const isBusy = useMemo( + () => ['email_signing_in', 'email_signing_up', 'oauth_redirecting', 'oauth_exchanging'].includes(authPhase), + [authPhase] + ); + + const submitText = mode === 'signup' ? t.auth.signUp : t.auth.signIn; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - setError(''); - setLoading(true); + setLocalError(''); - try { - const { error } = isSignUp - ? await signUp(email, password) - : await signIn(email, password); - - if (error) { - setError(error.message); - } else { - onClose(); + if (mode === 'signup') { + if (password.length < 6) { + setLocalError(t.auth.passwordHint); + return; + } + + if (password !== confirmPassword) { + setLocalError(t.auth.passwordMismatch); + return; } - } catch (err) { - setError('发生错误,请重试'); - } finally { - setLoading(false); } + + const result = mode === 'signup' ? await signUp(email, password) : await signIn(email, password); + + if (!result.error) { + onClose(); + } + }; + + const handleGoogleOAuth = async () => { + await beginGoogleOAuth(); }; return ( @@ -43,36 +57,72 @@ export default function AuthModal({ onClose }: AuthModalProps) {

- {isSignUp ? t.auth.signUpTitle : t.auth.signInTitle} + {mode === 'signup' ? t.auth.signUpTitle : t.auth.signInTitle}

+ {!mandatory && ( + + )} +
+ +
+
-
-
- {error && ( -
- {t.auth.error}: {error} + {mode === 'signup' && ( +
+ + setConfirmPassword(e.target.value)} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="••••••••" + required + minLength={6} + disabled={isBusy} + /> +
+ )} + + {(localError || authError) && ( +
+ {t.auth.error}: {localError || authError}
)} + +
+ + + - -
- -
); diff --git a/src/components/LeftSidebar.tsx b/src/components/LeftSidebar.tsx index b447cdd..83f99e5 100644 --- a/src/components/LeftSidebar.tsx +++ b/src/components/LeftSidebar.tsx @@ -1,5 +1,5 @@ import React, { useState, useRef, useCallback, useEffect } from 'react'; -import { Upload, LogIn, LogOut, FileText, Clock, ChevronLeft, ChevronRight, Settings, History, MousePointerClick, FileUp, ClipboardPaste, Loader2 } from 'lucide-react'; +import { Upload, LogIn, LogOut, FileText, Clock, ChevronLeft, ChevronRight, History, MousePointerClick, FileUp, ClipboardPaste, Loader2 } from 'lucide-react'; import { useAuth } from '../contexts/AuthContext'; import { useLanguage } from '../contexts/LanguageContext'; import { FileRecord } from '../types'; @@ -10,6 +10,8 @@ interface LeftSidebarProps { selectedFileId: string | null; onFileSelect: (fileId: string) => void; onUploadClick: () => void; + canUploadAnonymously: boolean; + onRequireAuth: () => void; isCollapsed: boolean; onToggleCollapse: () => void; onUploadFiles: (files: File[]) => void; @@ -23,6 +25,8 @@ export default function LeftSidebar({ selectedFileId, onFileSelect, onUploadClick, + canUploadAnonymously, + onRequireAuth, isCollapsed, onToggleCollapse, onUploadFiles, @@ -30,12 +34,19 @@ export default function LeftSidebar({ loadingMore, onLoadMore, }: LeftSidebarProps) { - const { user, signOut } = useAuth(); + const { user, signOut, isAuthenticated } = useAuth(); const { t } = useLanguage(); const [showAuthModal, setShowAuthModal] = useState(false); const [isDragging, setIsDragging] = useState(false); const fileInputRef = useRef(null); const listRef = useRef(null); + const displayName = user?.username?.trim() || user?.email || ''; + + useEffect(() => { + if (isAuthenticated) { + setShowAuthModal(false); + } + }, [isAuthenticated]); // ... (rest of the logic remains the same) // Handle scroll to load more @@ -84,6 +95,10 @@ export default function LeftSidebar({ const handleDrop = (e: React.DragEvent) => { e.preventDefault(); + if (!user && !canUploadAnonymously) { + onRequireAuth(); + return; + } setIsDragging(false); const droppedFiles = Array.from(e.dataTransfer.files); @@ -97,6 +112,10 @@ export default function LeftSidebar({ }; const handleFileChange = (e: React.ChangeEvent) => { + if (!user && !canUploadAnonymously) { + onRequireAuth(); + return; + } if (e.target.files && e.target.files.length > 0) { onUploadFiles(Array.from(e.target.files)); } @@ -117,7 +136,13 @@ export default function LeftSidebar({ + + {canUploadAnonymously ? 'guest-allowed' : 'guest-blocked'} +
+ ), +})); + +vi.mock('../../components/FilePreview', () => ({ + default: () =>
preview
, +})); + +vi.mock('../../components/ResultPanel', () => ({ + default: () =>
result
, +})); + +vi.mock('../../components/UploadModal', () => ({ + default: () =>
upload-modal
, +})); + +vi.mock('../../components/UserGuide', () => ({ + default: () => null, +})); + +vi.mock('../../components/AuthModal', () => ({ + default: () =>
auth-modal
, +})); + +describe('App anonymous usage limit', () => { + beforeEach(() => { + vi.clearAllMocks(); + localStorage.clear(); + localStorage.setItem('hasSeenGuide', 'true'); + useAuthMock.mockReturnValue({ + user: null, + initializing: false, + }); + }); + + it('allows anonymous upload before the limit', () => { + localStorage.setItem('texpixel_guest_usage_count', '2'); + + render(); + + expect(screen.getByText('guest-allowed')).toBeInTheDocument(); + fireEvent.click(screen.getByText('open-upload')); + + expect(screen.getByText('upload-modal')).toBeInTheDocument(); + }); + + it('forces login after three anonymous uses', () => { + localStorage.setItem('texpixel_guest_usage_count', '3'); + + render(); + + expect(screen.getByText('guest-blocked')).toBeInTheDocument(); + fireEvent.click(screen.getByText('open-upload')); + + expect(screen.getByText('auth-modal')).toBeInTheDocument(); + expect(screen.queryByText('upload-modal')).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/__tests__/AuthModal.test.tsx b/src/components/__tests__/AuthModal.test.tsx new file mode 100644 index 0000000..c973246 --- /dev/null +++ b/src/components/__tests__/AuthModal.test.tsx @@ -0,0 +1,103 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; + +import AuthModal from '../AuthModal'; + +const useAuthMock = vi.fn(); + +vi.mock('../../contexts/AuthContext', () => ({ + useAuth: () => useAuthMock(), +})); + +vi.mock('../../contexts/LanguageContext', () => ({ + useLanguage: () => ({ + t: { + auth: { + signIn: '登录', + signUp: '注册', + signInTitle: '登录账号', + signUpTitle: '注册账号', + email: '邮箱', + password: '密码', + error: '错误', + genericError: '发生错误,请重试', + hasAccount: '已有账号?去登录', + noAccount: '没有账号?去注册', + continueWithGoogle: 'Google', + emailHint: '仅用于登录和同步记录。', + passwordHint: '密码至少 6 位,建议使用字母和数字组合。', + confirmPassword: '确认密码', + passwordMismatch: '两次输入的密码不一致。', + oauthRedirecting: '正在跳转 Google...', + }, + }, + }), +})); + +describe('AuthModal', () => { + it('renders google oauth button', () => { + useAuthMock.mockReturnValue({ + signIn: vi.fn().mockResolvedValue({ error: null }), + signUp: vi.fn().mockResolvedValue({ error: null }), + beginGoogleOAuth: vi.fn().mockResolvedValue({ error: null }), + authPhase: 'idle', + authError: null, + }); + + render(); + + expect(screen.getByRole('button', { name: 'Google' })).toBeInTheDocument(); + }); + + it('disables inputs and submit while oauth redirecting', () => { + useAuthMock.mockReturnValue({ + signIn: vi.fn().mockResolvedValue({ error: null }), + signUp: vi.fn().mockResolvedValue({ error: null }), + beginGoogleOAuth: vi.fn().mockResolvedValue({ error: null }), + authPhase: 'oauth_redirecting', + authError: null, + }); + + render(); + + const emailInput = screen.getByLabelText('邮箱'); + const submitButton = document.querySelector('button[type="submit"]') as HTMLButtonElement; + + expect(emailInput).toBeDisabled(); + expect(submitButton).toBeDisabled(); + }); + + it('switches between signin and signup with segmented tabs', () => { + useAuthMock.mockReturnValue({ + signIn: vi.fn().mockResolvedValue({ error: null }), + signUp: vi.fn().mockResolvedValue({ error: null }), + beginGoogleOAuth: vi.fn().mockResolvedValue({ error: null }), + authPhase: 'idle', + authError: null, + }); + + render(); + + const signupTab = screen.getByRole('button', { name: '注册', pressed: false }); + fireEvent.click(signupTab); + + expect(screen.getByRole('button', { name: '注册', pressed: true })).toBeInTheDocument(); + }); + + it('shows friendlier signup guidance', () => { + useAuthMock.mockReturnValue({ + signIn: vi.fn().mockResolvedValue({ error: null }), + signUp: vi.fn().mockResolvedValue({ error: null }), + beginGoogleOAuth: vi.fn().mockResolvedValue({ error: null }), + authPhase: 'idle', + authError: null, + }); + + render(); + + fireEvent.click(screen.getByRole('button', { name: '注册', pressed: false })); + + expect(screen.getByText(/密码至少 6 位/i)).toBeInTheDocument(); + expect(screen.getByText(/仅用于登录和同步记录/i)).toBeInTheDocument(); + }); +}); diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index 78267ce..455e80e 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -1,122 +1,207 @@ -import { createContext, useContext, useEffect, useState, ReactNode, useCallback } from 'react'; +import { createContext, useContext, useMemo, ReactNode, useCallback, useEffect, useReducer } from 'react'; import { authService } from '../lib/authService'; import { ApiErrorMessages } from '../types/api'; -import type { UserInfo } from '../types/api'; +import type { GoogleOAuthCallbackRequest, UserInfo } from '../types/api'; +import { authReducer, createInitialAuthState, type AuthPhase } from './authMachine'; + +export const OAUTH_STATE_KEY = 'texpixel_oauth_state'; +export const OAUTH_POST_LOGIN_REDIRECT_KEY = 'texpixel_post_login_redirect'; interface AuthContextType { user: UserInfo | null; token: string | null; loading: boolean; - initializing: boolean; // 新增初始化状态 + initializing: boolean; + authPhase: AuthPhase; + authError: string | null; signIn: (email: string, password: string) => Promise<{ error: Error | null }>; signUp: (email: string, password: string) => Promise<{ error: Error | null }>; + beginGoogleOAuth: () => Promise<{ error: Error | null }>; + completeGoogleOAuth: (params: GoogleOAuthCallbackRequest) => Promise<{ error: Error | null }>; signOut: () => Promise; isAuthenticated: boolean; } const AuthContext = createContext(undefined); +const oauthExchangeInFlight = new Map>(); + +function getErrorMessage(error: unknown, fallback: string): string { + if (error && typeof error === 'object' && 'code' in error) { + const apiError = error as { code: number; message?: string }; + return ApiErrorMessages[apiError.code] || apiError.message || fallback; + } + if (error instanceof Error) { + return error.message; + } + return fallback; +} + +function createOAuthState(): string { + if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { + return crypto.randomUUID(); + } + + return `${Date.now()}_${Math.random().toString(36).slice(2)}`; +} + +function mergeUserProfile(user: UserInfo, profile: { username: string; email: string }): UserInfo { + return { + ...user, + username: profile.username || user.username || '', + email: profile.email || user.email, + }; +} export function AuthProvider({ children }: { children: ReactNode }) { - // 直接在 useState 初始化函数中同步恢复会话 - const [user, setUser] = useState(() => { - try { - const session = authService.restoreSession(); - return session ? session.user : null; - } catch { - return null; - } - }); + const restoredSession = authService.restoreSession(); - const [token, setToken] = useState(() => { - try { - const session = authService.restoreSession(); - return session ? session.token : null; - } catch { - return null; - } - }); + const [state, dispatch] = useReducer( + authReducer, + createInitialAuthState(restoredSession ? { user: restoredSession.user, token: restoredSession.token } : null) + ); - const [loading, setLoading] = useState(false); - const [initializing, setInitializing] = useState(false); // 不需要初始化过程了,因为是同步的 - - // 不再需要 useEffect 里的 restoreSession - - - /** - * 从错误对象中提取用户友好的错误消息 - */ - const getErrorMessage = (error: unknown, fallback: string): string => { - // 检查是否是 ApiError(通过 code 属性判断,避免 instanceof 在热更新时失效) - if (error && typeof error === 'object' && 'code' in error) { - const apiError = error as { code: number; message: string }; - return ApiErrorMessages[apiError.code] || apiError.message || fallback; - } - if (error instanceof Error) { - return error.message; - } - return fallback; - }; - - /** - * 登录 - */ const signIn = useCallback(async (email: string, password: string) => { - setLoading(true); + dispatch({ type: 'EMAIL_SIGNIN_START' }); + try { const result = await authService.login({ email, password }); - setUser(result.user); - setToken(result.token); + dispatch({ type: 'EMAIL_SIGNIN_SUCCESS', payload: { user: result.user, token: result.token } }); return { error: null }; } catch (error) { const message = getErrorMessage(error, '登录失败'); + dispatch({ type: 'EMAIL_SIGNIN_FAIL', payload: { error: message } }); return { error: new Error(message) }; - } finally { - setLoading(false); } }, []); - /** - * 注册 - */ const signUp = useCallback(async (email: string, password: string) => { - setLoading(true); + dispatch({ type: 'EMAIL_SIGNUP_START' }); + try { const result = await authService.register({ email, password }); - setUser(result.user); - setToken(result.token); + dispatch({ type: 'EMAIL_SIGNUP_SUCCESS', payload: { user: result.user, token: result.token } }); return { error: null }; } catch (error) { const message = getErrorMessage(error, '注册失败'); + dispatch({ type: 'EMAIL_SIGNUP_FAIL', payload: { error: message } }); return { error: new Error(message) }; - } finally { - setLoading(false); } }, []); - /** - * 登出 - */ - const signOut = useCallback(async () => { - setLoading(true); + const beginGoogleOAuth = useCallback(async () => { + dispatch({ type: 'OAUTH_REDIRECT_START' }); + try { - authService.logout(); - setUser(null); - setToken(null); - } finally { - setLoading(false); + const stateToken = createOAuthState(); + const redirectUri = `${window.location.origin}/auth/google/callback`; + sessionStorage.setItem(OAUTH_STATE_KEY, stateToken); + sessionStorage.setItem(OAUTH_POST_LOGIN_REDIRECT_KEY, window.location.href); + + const { authUrl } = await authService.getGoogleOAuthUrl(redirectUri, stateToken); + window.location.assign(authUrl); + return { error: null }; + } catch (error) { + const message = getErrorMessage(error, 'Google 登录失败'); + dispatch({ type: 'OAUTH_EXCHANGE_FAIL', payload: { error: message } }); + return { error: new Error(message) }; } }, []); - const value: AuthContextType = { - user, - token, - loading, - initializing, - signIn, - signUp, - signOut, - isAuthenticated: !!user && !!token, - }; + const completeGoogleOAuth = useCallback(async (params: GoogleOAuthCallbackRequest) => { + const requestKey = params.code; + const existing = oauthExchangeInFlight.get(requestKey); + if (existing) { + return existing; + } + + const promise = (async () => { + dispatch({ type: 'OAUTH_EXCHANGE_START' }); + + try { + const expectedState = sessionStorage.getItem(OAUTH_STATE_KEY); + if (!expectedState || expectedState !== params.state) { + const invalidStateMessage = 'OAuth state 校验失败'; + dispatch({ type: 'OAUTH_EXCHANGE_FAIL', payload: { error: invalidStateMessage } }); + return { error: new Error(invalidStateMessage) }; + } + + const result = await authService.exchangeGoogleCode(params); + sessionStorage.removeItem(OAUTH_STATE_KEY); + dispatch({ type: 'OAUTH_EXCHANGE_SUCCESS', payload: { user: result.user, token: result.token } }); + return { error: null }; + } catch (error) { + const message = getErrorMessage(error, 'Google 登录失败'); + dispatch({ type: 'OAUTH_EXCHANGE_FAIL', payload: { error: message } }); + return { error: new Error(message) }; + } finally { + oauthExchangeInFlight.delete(requestKey); + } + })(); + + oauthExchangeInFlight.set(requestKey, promise); + return promise; + }, []); + + const signOut = useCallback(async () => { + authService.logout(); + dispatch({ type: 'SIGN_OUT' }); + }, []); + + useEffect(() => { + let cancelled = false; + + const syncUserProfile = async () => { + if (!state.user || !state.token) { + return; + } + + try { + const profile = await authService.getUserInfo(); + if (cancelled) { + return; + } + + dispatch({ + type: 'UPDATE_USER', + payload: { + user: mergeUserProfile(state.user, profile), + }, + }); + } catch { + // Keep token-derived identity if profile sync fails. + } + }; + + void syncUserProfile(); + + return () => { + cancelled = true; + }; + }, [state.token, state.user]); + + const value = useMemo(() => { + const loadingPhases: AuthPhase[] = [ + 'email_signing_in', + 'email_signing_up', + 'oauth_redirecting', + 'oauth_exchanging', + ]; + + return { + user: state.user, + token: state.token, + loading: loadingPhases.includes(state.authPhase), + initializing: state.initializing, + authPhase: state.authPhase, + authError: state.authError, + signIn, + signUp, + beginGoogleOAuth, + completeGoogleOAuth, + signOut, + isAuthenticated: !!state.user && !!state.token, + }; + }, [beginGoogleOAuth, completeGoogleOAuth, signIn, signOut, signUp, state]); return {children}; } diff --git a/src/contexts/__tests__/AuthContext.oauth.test.tsx b/src/contexts/__tests__/AuthContext.oauth.test.tsx new file mode 100644 index 0000000..747f15d --- /dev/null +++ b/src/contexts/__tests__/AuthContext.oauth.test.tsx @@ -0,0 +1,207 @@ +import { useEffect } from 'react'; +import { act, render, waitFor } from '@testing-library/react'; +import { describe, expect, it, vi, beforeEach } from 'vitest'; + +import { AuthProvider, useAuth } from '../AuthContext'; + +const { + loginMock, + registerMock, + logoutMock, + restoreSessionMock, + getGoogleOAuthUrlMock, + exchangeGoogleCodeMock, +} = vi.hoisted(() => ({ + loginMock: vi.fn(), + registerMock: vi.fn(), + logoutMock: vi.fn(), + restoreSessionMock: vi.fn(() => null), + getGoogleOAuthUrlMock: vi.fn(), + exchangeGoogleCodeMock: vi.fn(), +})); + +vi.mock('../../lib/authService', () => ({ + authService: { + login: loginMock, + register: registerMock, + logout: logoutMock, + restoreSession: restoreSessionMock, + getGoogleOAuthUrl: getGoogleOAuthUrlMock, + exchangeGoogleCode: exchangeGoogleCodeMock, + }, +})); + +function Harness({ onReady }: { onReady: (ctx: ReturnType) => void }) { + const auth = useAuth(); + + useEffect(() => { + onReady(auth); + }, [auth, onReady]); + + return null; +} + +function renderWithProvider(onReady: (ctx: ReturnType) => void) { + return render( + + + + ); +} + +describe('AuthContext OAuth flow', () => { + beforeEach(() => { + vi.clearAllMocks(); + sessionStorage.clear(); + localStorage.clear(); + restoreSessionMock.mockReturnValue(null); + }); + + it('beginGoogleOAuth writes state and redirect then redirects browser', async () => { + getGoogleOAuthUrlMock.mockResolvedValue({ authUrl: 'https://accounts.google.com/o/oauth2/v2/auth' }); + let ctxRef: ReturnType | null = null; + + renderWithProvider((ctx) => { + ctxRef = ctx; + }); + + await waitFor(() => { + expect(ctxRef).toBeTruthy(); + }); + + await act(async () => { + await (ctxRef as ReturnType).beginGoogleOAuth(); + }); + + expect(sessionStorage.getItem('texpixel_oauth_state')).toBeTruthy(); + expect(sessionStorage.getItem('texpixel_post_login_redirect')).toBe(window.location.href); + expect(getGoogleOAuthUrlMock).toHaveBeenCalledTimes(1); + }); + + it('completeGoogleOAuth rejects when state mismatches', async () => { + sessionStorage.setItem('texpixel_oauth_state', 'expected_state'); + let ctxRef: ReturnType | null = null; + + renderWithProvider((ctx) => { + ctxRef = ctx; + }); + + await waitFor(() => { + expect(ctxRef).toBeTruthy(); + }); + + let result: { error: Error | null } = { error: null }; + await act(async () => { + result = await (ctxRef as ReturnType).completeGoogleOAuth({ + code: 'abc', + state: 'wrong_state', + redirect_uri: 'http://localhost:5173/auth/google/callback', + }); + }); + + expect(result.error).toBeTruthy(); + expect(exchangeGoogleCodeMock).not.toHaveBeenCalled(); + expect(localStorage.getItem('texpixel_token')).toBeNull(); + }); + + it('completeGoogleOAuth stores session on success', async () => { + sessionStorage.setItem('texpixel_oauth_state', 'state_ok'); + exchangeGoogleCodeMock.mockImplementation(async () => { + localStorage.setItem('texpixel_token', 'Bearer header.payload.sig'); + return { + token: 'Bearer header.payload.sig', + expiresAt: 1999999999, + user: { + user_id: 7, + id: '7', + email: 'oauth@example.com', + exp: 1999999999, + iat: 1111111, + }, + }; + }); + + let ctxRef: ReturnType | null = null; + + renderWithProvider((ctx) => { + ctxRef = ctx; + }); + + await waitFor(() => { + expect(ctxRef).toBeTruthy(); + }); + + let result: { error: Error | null } = { error: null }; + await act(async () => { + result = await (ctxRef as ReturnType).completeGoogleOAuth({ + code: 'code_ok', + state: 'state_ok', + redirect_uri: 'http://localhost:5173/auth/google/callback', + }); + }); + + expect(result.error).toBeNull(); + + await waitFor(() => { + expect((ctxRef as ReturnType).isAuthenticated).toBe(true); + }); + expect(localStorage.getItem('texpixel_token')).toBe('Bearer header.payload.sig'); + }); + + it('completeGoogleOAuth deduplicates same code requests', async () => { + sessionStorage.setItem('texpixel_oauth_state', 'state_ok'); + + exchangeGoogleCodeMock.mockImplementation( + () => + new Promise((resolve) => { + setTimeout(() => { + localStorage.setItem('texpixel_token', 'Bearer header.payload.sig'); + resolve({ + token: 'Bearer header.payload.sig', + expiresAt: 1999999999, + user: { + user_id: 7, + id: '7', + email: 'oauth@example.com', + exp: 1999999999, + iat: 1111111, + }, + }); + }, 20); + }) + ); + + let ctxRef: ReturnType | null = null; + + renderWithProvider((ctx) => { + ctxRef = ctx; + }); + + await waitFor(() => { + expect(ctxRef).toBeTruthy(); + }); + + let result1: { error: Error | null } = { error: null }; + let result2: { error: Error | null } = { error: null }; + + await act(async () => { + const p1 = (ctxRef as ReturnType).completeGoogleOAuth({ + code: 'same_code', + state: 'state_ok', + redirect_uri: 'http://localhost:5173/auth/google/callback', + }); + + const p2 = (ctxRef as ReturnType).completeGoogleOAuth({ + code: 'same_code', + state: 'state_ok', + redirect_uri: 'http://localhost:5173/auth/google/callback', + }); + + [result1, result2] = await Promise.all([p1, p2]); + }); + + expect(result1.error).toBeNull(); + expect(result2.error).toBeNull(); + expect(exchangeGoogleCodeMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/contexts/authMachine.ts b/src/contexts/authMachine.ts new file mode 100644 index 0000000..46d4205 --- /dev/null +++ b/src/contexts/authMachine.ts @@ -0,0 +1,135 @@ +import type { UserInfo } from '../types/api'; + +export type AuthPhase = + | 'idle' + | 'email_signing_in' + | 'email_signing_up' + | 'oauth_redirecting' + | 'oauth_exchanging' + | 'authenticated' + | 'error'; + +export interface AuthState { + user: UserInfo | null; + token: string | null; + authPhase: AuthPhase; + authError: string | null; + initializing: boolean; +} + +export type AuthAction = + | { type: 'RESTORE_SESSION'; payload: { user: UserInfo | null; token: string | null } } + | { type: 'EMAIL_SIGNIN_START' } + | { type: 'EMAIL_SIGNIN_SUCCESS'; payload: { user: UserInfo; token: string } } + | { type: 'EMAIL_SIGNIN_FAIL'; payload: { error: string } } + | { type: 'EMAIL_SIGNUP_START' } + | { type: 'EMAIL_SIGNUP_SUCCESS'; payload: { user: UserInfo; token: string } } + | { type: 'EMAIL_SIGNUP_FAIL'; payload: { error: string } } + | { type: 'OAUTH_REDIRECT_START' } + | { type: 'OAUTH_EXCHANGE_START' } + | { type: 'OAUTH_EXCHANGE_SUCCESS'; payload: { user: UserInfo; token: string } } + | { type: 'OAUTH_EXCHANGE_FAIL'; payload: { error: string } } + | { type: 'UPDATE_USER'; payload: { user: UserInfo } } + | { type: 'SIGN_OUT' }; + +export function createInitialAuthState(session: { user: UserInfo; token: string } | null): AuthState { + if (session) { + return { + user: session.user, + token: session.token, + authPhase: 'authenticated', + authError: null, + initializing: false, + }; + } + + return { + user: null, + token: null, + authPhase: 'idle', + authError: null, + initializing: false, + }; +} + +export function authReducer(state: AuthState, action: AuthAction): AuthState { + switch (action.type) { + case 'RESTORE_SESSION': { + if (action.payload.user && action.payload.token) { + return { + ...state, + user: action.payload.user, + token: action.payload.token, + authPhase: 'authenticated', + authError: null, + initializing: false, + }; + } + + return { + ...state, + user: null, + token: null, + authPhase: 'idle', + authError: null, + initializing: false, + }; + } + + case 'EMAIL_SIGNIN_START': + return { ...state, authPhase: 'email_signing_in', authError: null }; + case 'EMAIL_SIGNIN_SUCCESS': + return { + ...state, + user: action.payload.user, + token: action.payload.token, + authPhase: 'authenticated', + authError: null, + }; + case 'EMAIL_SIGNIN_FAIL': + return { ...state, authPhase: 'error', authError: action.payload.error }; + + case 'EMAIL_SIGNUP_START': + return { ...state, authPhase: 'email_signing_up', authError: null }; + case 'EMAIL_SIGNUP_SUCCESS': + return { + ...state, + user: action.payload.user, + token: action.payload.token, + authPhase: 'authenticated', + authError: null, + }; + case 'EMAIL_SIGNUP_FAIL': + return { ...state, authPhase: 'error', authError: action.payload.error }; + + case 'OAUTH_REDIRECT_START': + return { ...state, authPhase: 'oauth_redirecting', authError: null }; + case 'OAUTH_EXCHANGE_START': + return { ...state, authPhase: 'oauth_exchanging', authError: null }; + case 'OAUTH_EXCHANGE_SUCCESS': + return { + ...state, + user: action.payload.user, + token: action.payload.token, + authPhase: 'authenticated', + authError: null, + }; + case 'OAUTH_EXCHANGE_FAIL': + return { ...state, authPhase: 'error', authError: action.payload.error }; + + case 'UPDATE_USER': + return { ...state, user: action.payload.user }; + + case 'SIGN_OUT': + return { + ...state, + user: null, + token: null, + authPhase: 'idle', + authError: null, + }; + + default: + return state; + } +} diff --git a/src/lib/api.ts b/src/lib/api.ts index 8cf2b0f..5426b11 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -73,6 +73,7 @@ export class ApiError extends Error { */ interface RequestConfig extends RequestInit { skipAuth?: boolean; + successCodes?: number[]; } /** @@ -82,7 +83,7 @@ async function request( endpoint: string, config: RequestConfig = {} ): Promise> { - const { skipAuth = false, headers: customHeaders, ...restConfig } = config; + const { skipAuth = false, successCodes = [200], headers: customHeaders, ...restConfig } = config; const headers: HeadersInit = { 'Content-Type': 'application/json', @@ -108,7 +109,7 @@ async function request( const data: ApiResponse = await response.json(); // 统一处理业务错误 - if (data.code !== 200) { + if (!successCodes.includes(data.code)) { throw new ApiError(data.code, data.message, data.request_id); } @@ -153,4 +154,3 @@ export const http = { }; export default http; - diff --git a/src/lib/authService.ts b/src/lib/authService.ts index a2e4732..b312bd8 100644 --- a/src/lib/authService.ts +++ b/src/lib/authService.ts @@ -1,36 +1,61 @@ /** * 认证服务 - * 处理用户登录、注册、登出等认证相关操作 + * 处理用户登录、注册、OAuth、登出等认证相关操作 */ import { http, tokenManager, ApiError } from './api'; -import type { AuthData, LoginRequest, RegisterRequest, UserInfo } from '../types/api'; +import type { + AuthData, + GoogleAuthUrlData, + GoogleOAuthCallbackRequest, + LoginRequest, + RegisterRequest, + UserInfoData, + UserInfo, +} from '../types/api'; -// 重新导出 ApiErrorMessages 以便使用 export { ApiErrorMessages } from '../types/api'; -/** - * 从 JWT Token 解析用户信息 - */ -function parseJwtPayload(token: string): UserInfo | null { +function decodeJwtPayload(token: string): UserInfo | null { try { - // 移除 Bearer 前缀 const actualToken = token.replace('Bearer ', ''); const base64Payload = actualToken.split('.')[1]; - const payload = JSON.parse(atob(base64Payload)); + const normalized = base64Payload.replace(/-/g, '+').replace(/_/g, '/'); + const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '='); + const payload = JSON.parse(atob(padded)); return payload as UserInfo; } catch { return null; } } -/** - * 认证服务 - */ +function normalizeUser(payload: UserInfo, emailHint?: string): UserInfo { + return { + ...payload, + email: payload.email || emailHint || '', + id: String(payload.user_id), + }; +} + +function buildSession(authData: AuthData, emailHint?: string): { user: UserInfo; token: string; expiresAt: number } { + const { token, expires_at } = authData; + const parsedUser = decodeJwtPayload(token); + + if (!parsedUser) { + throw new ApiError(-1, 'Token 解析失败'); + } + + const user = normalizeUser(parsedUser, emailHint); + tokenManager.setToken(token, expires_at, user.email || undefined); + + return { + user, + token, + expiresAt: expires_at, + }; +} + export const authService = { - /** - * 用户登录 - */ async login(credentials: LoginRequest): Promise<{ user: UserInfo; token: string; expiresAt: number }> { const response = await http.post('/user/login', credentials, { skipAuth: true }); @@ -38,35 +63,9 @@ export const authService = { throw new ApiError(-1, '登录失败,请重试'); } - const { token, expires_at } = response.data; - - // 存储 Token 和 email - tokenManager.setToken(token, expires_at, credentials.email); - - // 解析用户信息 - const user = parseJwtPayload(token); - if (!user) { - throw new ApiError(-1, 'Token 解析失败'); - } - - // 补充 email 和 id 兼容字段 - const userWithEmail: UserInfo = { - ...user, - email: credentials.email, - id: String(user.user_id), - }; - - return { - user: userWithEmail, - token, - expiresAt: expires_at, - }; + return buildSession(response.data, credentials.email); }, - /** - * 用户注册 - * 注意:业务错误码 (code !== 200) 已在 api.ts 中统一处理,会抛出 ApiError - */ async register(credentials: RegisterRequest): Promise<{ user: UserInfo; token: string; expiresAt: number }> { const response = await http.post('/user/register', credentials, { skipAuth: true }); @@ -74,55 +73,58 @@ export const authService = { throw new ApiError(-1, '注册失败,请重试'); } - const { token, expires_at } = response.data; - - // 存储 Token 和 email - tokenManager.setToken(token, expires_at, credentials.email); - - // 解析用户信息 - const user = parseJwtPayload(token); - if (!user) { - throw new ApiError(-1, 'Token 解析失败'); - } - - // 补充 email 和 id 兼容字段 - const userWithEmail: UserInfo = { - ...user, - email: credentials.email, - id: String(user.user_id), - }; - - return { - user: userWithEmail, - token, - expiresAt: expires_at, - }; + return buildSession(response.data, credentials.email); + }, + + async getGoogleOAuthUrl(redirectUri: string, state: string): Promise<{ authUrl: string }> { + const query = new URLSearchParams({ redirect_uri: redirectUri, state }); + const response = await http.get(`/user/oauth/google/url?${query.toString()}`, { + skipAuth: true, + }); + + if (!response.data?.auth_url) { + throw new ApiError(-1, '获取 Google 授权地址失败'); + } + + return { authUrl: response.data.auth_url }; + }, + + async exchangeGoogleCode( + payload: GoogleOAuthCallbackRequest + ): Promise<{ user: UserInfo; token: string; expiresAt: number }> { + const response = await http.post('/user/oauth/google/callback', payload, { skipAuth: true }); + + if (!response.data) { + throw new ApiError(-1, 'Google 登录失败,请重试'); + } + + return buildSession(response.data); + }, + + async getUserInfo(): Promise { + const response = await http.get('/user/info', { + successCodes: [0, 200], + }); + + if (!response.data) { + throw new ApiError(-1, '获取用户信息失败'); + } + + return response.data; }, - /** - * 用户登出 - */ logout(): void { tokenManager.removeToken(); }, - /** - * 检查是否已登录 - */ isAuthenticated(): boolean { return tokenManager.isTokenValid(); }, - /** - * 获取当前存储的 Token - */ getToken(): string | null { return tokenManager.getToken(); }, - /** - * 从存储的 Token 恢复用户会话 - */ restoreSession(): { user: UserInfo; token: string; expiresAt: number } | null { const token = tokenManager.getToken(); const expiresAt = tokenManager.getExpiresAt(); @@ -133,21 +135,14 @@ export const authService = { return null; } - const parsedUser = parseJwtPayload(token); + const parsedUser = decodeJwtPayload(token); if (!parsedUser) { tokenManager.removeToken(); return null; } - // 补充 email 和 id 兼容字段 - const user: UserInfo = { - ...parsedUser, - email: email || '', - id: String(parsedUser.user_id), - }; - return { - user, + user: normalizeUser(parsedUser, email || ''), token, expiresAt, }; @@ -156,4 +151,3 @@ export const authService = { export { ApiError }; export default authService; - diff --git a/src/lib/translations.ts b/src/lib/translations.ts index c237125..c413c1d 100644 --- a/src/lib/translations.ts +++ b/src/lib/translations.ts @@ -60,6 +60,15 @@ export const translations = { genericError: 'An error occurred, please try again', hasAccount: 'Already have an account? Login', noAccount: 'No account? Register', + continueWithGoogle: 'Google', + emailHint: 'Used only for sign-in and history sync.', + passwordHint: 'Use at least 6 characters. Letters and numbers are recommended.', + confirmPassword: 'Confirm Password', + passwordMismatch: 'The two passwords do not match.', + oauthRedirecting: 'Redirecting to Google...', + oauthExchanging: 'Completing Google sign-in...', + invalidOAuthState: 'Invalid OAuth state, please retry.', + oauthFailed: 'Google sign-in failed, please retry.', }, export: { title: 'Export', @@ -156,6 +165,15 @@ export const translations = { genericError: '发生错误,请重试', hasAccount: '已有账号?去登录', noAccount: '没有账号?去注册', + continueWithGoogle: 'Google', + emailHint: '仅用于登录和同步记录。', + passwordHint: '密码至少 6 位,建议使用字母和数字组合。', + confirmPassword: '确认密码', + passwordMismatch: '两次输入的密码不一致。', + oauthRedirecting: '正在跳转 Google...', + oauthExchanging: '正在完成 Google 登录...', + invalidOAuthState: 'OAuth 状态校验失败,请重试', + oauthFailed: 'Google 登录失败,请重试', }, export: { title: '导出', diff --git a/src/main.tsx b/src/main.tsx index 13db444..65981b9 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,9 +1,10 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; -import App from './App.tsx'; import './index.css'; import { AuthProvider } from './contexts/AuthContext'; import { LanguageProvider } from './contexts/LanguageContext'; +import { BrowserRouter } from 'react-router-dom'; +import AppRouter from './routes/AppRouter'; // 错误处理:捕获未处理的错误 window.addEventListener('error', (event) => { @@ -22,11 +23,13 @@ if (!rootElement) { try { createRoot(rootElement).render( - - - - - + + + + + + + ); } catch (error) { diff --git a/src/pages/AuthCallbackPage.tsx b/src/pages/AuthCallbackPage.tsx new file mode 100644 index 0000000..ea8282b --- /dev/null +++ b/src/pages/AuthCallbackPage.tsx @@ -0,0 +1,81 @@ +import { useEffect, useMemo, useState } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import { useAuth, OAUTH_POST_LOGIN_REDIRECT_KEY } from '../contexts/AuthContext'; +import { useLanguage } from '../contexts/LanguageContext'; + +function toInternalPath(urlOrPath: string): string { + try { + const parsed = new URL(urlOrPath, window.location.origin); + if (parsed.origin !== window.location.origin) { + return '/'; + } + + return `${parsed.pathname}${parsed.search}${parsed.hash}`; + } catch { + return '/'; + } +} + +export default function AuthCallbackPage() { + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const { completeGoogleOAuth } = useAuth(); + const { t } = useLanguage(); + const [error, setError] = useState(null); + + const code = useMemo(() => searchParams.get('code') ?? '', [searchParams]); + const state = useMemo(() => searchParams.get('state') ?? '', [searchParams]); + + useEffect(() => { + let mounted = true; + + const run = async () => { + if (!code || !state) { + if (mounted) { + setError(t.auth.oauthFailed); + } + return; + } + + const redirectUri = `${window.location.origin}/auth/google/callback`; + const result = await completeGoogleOAuth({ code, state, redirect_uri: redirectUri }); + + if (result.error) { + if (mounted) { + setError(result.error.message || t.auth.oauthFailed); + } + return; + } + + const redirectTarget = sessionStorage.getItem(OAUTH_POST_LOGIN_REDIRECT_KEY) || '/'; + navigate(toInternalPath(redirectTarget), { replace: true }); + }; + + run(); + + return () => { + mounted = false; + }; + }, [code, completeGoogleOAuth, navigate, state, t.auth.oauthFailed]); + + return ( +
+
+

Google OAuth

+ {!error &&

{t.auth.oauthExchanging}

} + {error && ( + <> +

{error}

+ + + )} +
+
+ ); +} diff --git a/src/routes/AppRouter.tsx b/src/routes/AppRouter.tsx new file mode 100644 index 0000000..56fc65c --- /dev/null +++ b/src/routes/AppRouter.tsx @@ -0,0 +1,12 @@ +import { Routes, Route } from 'react-router-dom'; +import App from '../App'; +import AuthCallbackPage from '../pages/AuthCallbackPage'; + +export default function AppRouter() { + return ( + + } /> + } /> + + ); +} diff --git a/src/test/setup.ts b/src/test/setup.ts new file mode 100644 index 0000000..bb02c60 --- /dev/null +++ b/src/test/setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom/vitest'; diff --git a/src/types/api.ts b/src/types/api.ts index 13f7db3..5700326 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -14,10 +14,26 @@ export interface AuthData { expires_at: number; } +export interface UserInfoData { + username: string; + email: string; +} + +export interface GoogleAuthUrlData { + auth_url: string; +} + +export interface GoogleOAuthCallbackRequest { + code: string; + state: string; + redirect_uri: string; +} + // 用户信息(从 token 解析或 API 返回) export interface UserInfo { user_id: number; email: string; + username?: string; exp: number; iat: number; // 兼容字段,方便代码使用 diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..a8af831 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + test: { + environment: 'jsdom', + setupFiles: ['./src/test/setup.ts'], + globals: true, + exclude: ['e2e/**', 'node_modules/**', 'dist/**'], + }, +});