From eaf01cfc591dd72f9b093b533ced6c12a6c490a1 Mon Sep 17 00:00:00 2001 From: MasutaYunikon Date: Tue, 10 Mar 2026 22:36:34 +0100 Subject: [PATCH] feat: initial project --- .dockerignore | 8 + .github/workflows/containerize.yml | 40 + .github/workflows/release-please.yml | 16 + .gitignore | 24 + Dockerfile | 27 + README.md | 5 + docker-compose.yml | 0 index.html | 13 + package.json | 39 + pnpm-lock.yaml | 2716 +++++++++++++++++++++++++ server/auth.ts | 114 ++ server/index.ts | 312 +++ server/organize.ts | 248 +++ server/settings.ts | 33 + server/torrent-clients/index.ts | 181 ++ server/torrent-clients/qbittorrent.ts | 110 + src/App.vue | 14 + src/api/fankai.ts | 80 + src/assets/main.css | 1 + src/components/EpisodeRow.vue | 52 + src/components/ToastContainer.vue | 36 + src/composables/useToast.ts | 25 + src/main.ts | 10 + src/router/index.ts | 52 + src/stores/auth.ts | 57 + src/stores/catalog.ts | 33 + src/stores/series.ts | 114 ++ src/views/AuthView.vue | 115 ++ src/views/CatalogView.vue | 190 ++ src/views/DownloadsView.vue | 235 +++ src/views/ProductionView.vue | 135 ++ src/views/SerieView.vue | 275 +++ src/views/SettingsView.vue | 401 ++++ tsconfig.app.json | 16 + tsconfig.json | 20 + tsconfig.node.json | 26 + tsconfig.server.json | 15 + vite.config.ts | 19 + 38 files changed, 5807 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/workflows/containerize.yml create mode 100644 .github/workflows/release-please.yml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 index.html create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 server/auth.ts create mode 100644 server/index.ts create mode 100644 server/organize.ts create mode 100644 server/settings.ts create mode 100644 server/torrent-clients/index.ts create mode 100644 server/torrent-clients/qbittorrent.ts create mode 100644 src/App.vue create mode 100644 src/api/fankai.ts create mode 100644 src/assets/main.css create mode 100644 src/components/EpisodeRow.vue create mode 100644 src/components/ToastContainer.vue create mode 100644 src/composables/useToast.ts create mode 100644 src/main.ts create mode 100644 src/router/index.ts create mode 100644 src/stores/auth.ts create mode 100644 src/stores/catalog.ts create mode 100644 src/stores/series.ts create mode 100644 src/views/AuthView.vue create mode 100644 src/views/CatalogView.vue create mode 100644 src/views/DownloadsView.vue create mode 100644 src/views/ProductionView.vue create mode 100644 src/views/SerieView.vue create mode 100644 src/views/SettingsView.vue create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 tsconfig.server.json create mode 100644 vite.config.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..af73afd --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +node_modules +.git +.gitignore +*.md +data +.env +.env.* +!.env.example \ No newline at end of file diff --git a/.github/workflows/containerize.yml b/.github/workflows/containerize.yml new file mode 100644 index 0000000..55180c7 --- /dev/null +++ b/.github/workflows/containerize.yml @@ -0,0 +1,40 @@ +name: containerize +on: + release: + types: + - created + push: + branches: + - dev + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: masutayunikon/fankarr + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,prefix=dev-,enable=${{ github.ref == 'refs/heads/dev' }} + - name: Build and push + uses: docker/build-push-action@v6 + with: + push: true + platforms: linux/amd64,linux/arm64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..35866e9 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,16 @@ +on: + push: + branches: + - main +permissions: + contents: write + pull-requests: write +name: release-please +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - uses: googleapis/release-please-action@v4 + with: + token: ${{ secrets.FINED_TOKEN }} + release-type: simple \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a4277ef --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +FROM node:20-slim AS base +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable +COPY . /app +WORKDIR /app + +# ── Dépendances prod ─────────────────────────────────────────── +FROM base AS prod-deps +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile + +# ── Build ────────────────────────────────────────────────────── +FROM base AS build +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile +RUN pnpm run build + +# ── Production ───────────────────────────────────────────────── +FROM base +COPY --from=prod-deps /app/node_modules /app/node_modules +COPY --from=build /app/dist/server /app/dist/server +COPY --from=build /app/dist/client /app/public + +RUN mkdir -p /app/data + +EXPOSE 3001 +ENV NODE_ENV=production +CMD ["node", "dist/server/index.js"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..33895ab --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# Vue 3 + TypeScript + Vite + +This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..e4fe7bb --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "fankarr", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "concurrently \"vite\" \"tsx watch server/index.ts\"", + "dev:server": "tsx watch server/index.ts", + "dev:client": "vite", + "build": "vite build --outDir dist/client && tsc -p tsconfig.server.json", + "preview": "vite preview" + }, + "dependencies": { + "@tailwindcss/vite": "^4.2.1", + "bcrypt": "^6.0.0", + "cookie-parser": "^1.4.7", + "express": "^5.2.1", + "jsonwebtoken": "^9.0.3", + "node-fetch": "^3.3.2", + "pinia": "^3.0.4", + "tailwindcss": "^4.2.1", + "vue": "^3.5.25", + "vue-router": "^5.0.3" + }, + "devDependencies": { + "@types/bcrypt": "^5.0.2", + "@types/cookie-parser": "^1.4.8", + "@types/express": "^5.0.6", + "@types/jsonwebtoken": "^9.0.9", + "@types/node": "^24.10.1", + "@vitejs/plugin-vue": "^6.0.2", + "@vue/tsconfig": "^0.8.1", + "concurrently": "^9.2.1", + "tsx": "^4.21.0", + "typescript": "~5.9.3", + "vite": "^7.3.1", + "vue-tsc": "^3.1.5" + } +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..cdcd5a8 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,2716 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@tailwindcss/vite': + specifier: ^4.2.1 + version: 4.2.1(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + bcrypt: + specifier: ^6.0.0 + version: 6.0.0 + cookie-parser: + specifier: ^1.4.7 + version: 1.4.7 + express: + specifier: ^5.2.1 + version: 5.2.1 + jsonwebtoken: + specifier: ^9.0.3 + version: 9.0.3 + node-fetch: + specifier: ^3.3.2 + version: 3.3.2 + pinia: + specifier: ^3.0.4 + version: 3.0.4(typescript@5.9.3)(vue@3.5.29(typescript@5.9.3)) + tailwindcss: + specifier: ^4.2.1 + version: 4.2.1 + vue: + specifier: ^3.5.25 + version: 3.5.29(typescript@5.9.3) + vue-router: + specifier: ^5.0.3 + version: 5.0.3(@vue/compiler-sfc@3.5.29)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3)) + devDependencies: + '@types/bcrypt': + specifier: ^5.0.2 + version: 5.0.2 + '@types/cookie-parser': + specifier: ^1.4.8 + version: 1.4.10(@types/express@5.0.6) + '@types/express': + specifier: ^5.0.6 + version: 5.0.6 + '@types/jsonwebtoken': + specifier: ^9.0.9 + version: 9.0.10 + '@types/node': + specifier: ^24.10.1 + version: 24.12.0 + '@vitejs/plugin-vue': + specifier: ^6.0.2 + version: 6.0.4(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3)) + '@vue/tsconfig': + specifier: ^0.8.1 + version: 0.8.1(typescript@5.9.3)(vue@3.5.29(typescript@5.9.3)) + concurrently: + specifier: ^9.2.1 + version: 9.2.1 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + typescript: + specifier: ~5.9.3 + version: 5.9.3 + vite: + specifier: ^7.3.1 + version: 7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + vue-tsc: + specifier: ^3.1.5 + version: 3.2.5(typescript@5.9.3) + +packages: + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@rolldown/pluginutils@1.0.0-rc.2': + resolution: {integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==} + + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + cpu: [x64] + os: [win32] + + '@tailwindcss/node@4.2.1': + resolution: {integrity: sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==} + + '@tailwindcss/oxide-android-arm64@4.2.1': + resolution: {integrity: sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.2.1': + resolution: {integrity: sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.2.1': + resolution: {integrity: sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.2.1': + resolution: {integrity: sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': + resolution: {integrity: sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': + resolution: {integrity: sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.2.1': + resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.2.1': + resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.2.1': + resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.2.1': + resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': + resolution: {integrity: sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.2.1': + resolution: {integrity: sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.2.1': + resolution: {integrity: sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.2.1': + resolution: {integrity: sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + + '@types/bcrypt@5.0.2': + resolution: {integrity: sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==} + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/cookie-parser@1.4.10': + resolution: {integrity: sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==} + peerDependencies: + '@types/express': '*' + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/express-serve-static-core@5.1.1': + resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} + + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@24.12.0': + resolution: {integrity: sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==} + + '@types/qs@6.15.0': + resolution: {integrity: sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + + '@vitejs/plugin-vue@6.0.4': + resolution: {integrity: sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + vue: ^3.2.25 + + '@volar/language-core@2.4.28': + resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} + + '@volar/source-map@2.4.28': + resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==} + + '@volar/typescript@2.4.28': + resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} + + '@vue-macros/common@3.1.2': + resolution: {integrity: sha512-h9t4ArDdniO9ekYHAD95t9AZcAbb19lEGK+26iAjUODOIJKmObDNBSe4+6ELQAA3vtYiFPPBtHh7+cQCKi3Dng==} + engines: {node: '>=20.19.0'} + peerDependencies: + vue: ^2.7.0 || ^3.2.25 + peerDependenciesMeta: + vue: + optional: true + + '@vue/compiler-core@3.5.29': + resolution: {integrity: sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==} + + '@vue/compiler-dom@3.5.29': + resolution: {integrity: sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==} + + '@vue/compiler-sfc@3.5.29': + resolution: {integrity: sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==} + + '@vue/compiler-ssr@3.5.29': + resolution: {integrity: sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==} + + '@vue/devtools-api@7.7.9': + resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==} + + '@vue/devtools-api@8.0.7': + resolution: {integrity: sha512-tc1TXAxclsn55JblLkFVcIRG7MeSJC4fWsPjfM7qu/IcmPUYnQ5Q8vzWwBpyDY24ZjmZTUCCwjRSNbx58IhlAA==} + + '@vue/devtools-kit@7.7.9': + resolution: {integrity: sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==} + + '@vue/devtools-kit@8.0.7': + resolution: {integrity: sha512-H6esJGHGl5q0E9iV3m2EoBQHJ+V83WMW83A0/+Fn95eZ2iIvdsq4+UCS6yT/Fdd4cGZSchx/MdWDreM3WqMsDw==} + + '@vue/devtools-shared@7.7.9': + resolution: {integrity: sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==} + + '@vue/devtools-shared@8.0.7': + resolution: {integrity: sha512-CgAb9oJH5NUmbQRdYDj/1zMiaICYSLtm+B1kxcP72LBrifGAjUmt8bx52dDH1gWRPlQgxGPqpAMKavzVirAEhA==} + + '@vue/language-core@3.2.5': + resolution: {integrity: sha512-d3OIxN/+KRedeM5wQ6H6NIpwS3P5gC9nmyaHgBk+rO6dIsjY+tOh4UlPpiZbAh3YtLdCGEX4M16RmsBqPmJV+g==} + + '@vue/reactivity@3.5.29': + resolution: {integrity: sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA==} + + '@vue/runtime-core@3.5.29': + resolution: {integrity: sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg==} + + '@vue/runtime-dom@3.5.29': + resolution: {integrity: sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg==} + + '@vue/server-renderer@3.5.29': + resolution: {integrity: sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g==} + peerDependencies: + vue: 3.5.29 + + '@vue/shared@3.5.29': + resolution: {integrity: sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==} + + '@vue/tsconfig@0.8.1': + resolution: {integrity: sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g==} + peerDependencies: + typescript: 5.x + vue: ^3.4.0 + peerDependenciesMeta: + typescript: + optional: true + vue: + optional: true + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + alien-signals@3.1.2: + resolution: {integrity: sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ast-kit@2.2.0: + resolution: {integrity: sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==} + engines: {node: '>=20.19.0'} + + ast-walker-scope@0.8.3: + resolution: {integrity: sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg==} + engines: {node: '>=20.19.0'} + + bcrypt@6.0.0: + resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==} + engines: {node: '>= 18'} + + birpc@2.9.0: + resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concurrently@9.2.1: + resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==} + engines: {node: '>=18'} + hasBin: true + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-parser@1.4.7: + resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==} + engines: {node: '>= 0.8.0'} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + copy-anything@4.0.5: + resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} + engines: {node: '>=18'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + enhanced-resolve@5.20.0: + resolution: {integrity: sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==} + engines: {node: '>=10.13.0'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.13.6: + resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-what@5.5.0: + resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} + engines: {node: '>=18'} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + + lightningcss-android-arm64@1.31.1: + resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.31.1: + resolution: {integrity: sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.31.1: + resolution: {integrity: sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.31.1: + resolution: {integrity: sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.31.1: + resolution: {integrity: sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.31.1: + resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.31.1: + resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.31.1: + resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.31.1: + resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.31.1: + resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.31.1: + resolution: {integrity: sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.31.1: + resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==} + engines: {node: '>= 12.0.0'} + + local-pkg@1.1.2: + resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} + engines: {node: '>=14'} + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + magic-string-ast@1.0.3: + resolution: {integrity: sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==} + engines: {node: '>=20.19.0'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + mlly@1.8.1: + resolution: {integrity: sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + node-addon-api@8.6.0: + resolution: {integrity: sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==} + engines: {node: ^18 || ^20 || >= 21} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pinia@3.0.4: + resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==} + peerDependencies: + typescript: '>=4.5.0' + vue: ^3.5.11 + peerDependenciesMeta: + typescript: + optional: true + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + qs@6.15.0: + resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} + engines: {node: '>=0.6'} + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + superjson@2.2.6: + resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} + engines: {node: '>=16'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + tailwindcss@4.2.1: + resolution: {integrity: sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + unplugin-utils@0.3.1: + resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==} + engines: {node: '>=20.19.0'} + + unplugin@3.0.0: + resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==} + engines: {node: ^20.19.0 || >=22.12.0} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + 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 + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + vue-router@5.0.3: + resolution: {integrity: sha512-nG1c7aAFac7NYj8Hluo68WyWfc41xkEjaR0ViLHCa3oDvTQ/nIuLJlXJX1NUPw/DXzx/8+OKMng045HHQKQKWw==} + peerDependencies: + '@pinia/colada': '>=0.21.2' + '@vue/compiler-sfc': ^3.5.17 + pinia: ^3.0.4 + vue: ^3.5.0 + peerDependenciesMeta: + '@pinia/colada': + optional: true + '@vue/compiler-sfc': + optional: true + pinia: + optional: true + + vue-tsc@3.2.5: + resolution: {integrity: sha512-/htfTCMluQ+P2FISGAooul8kO4JMheOTCbCy4M6dYnYYjqLe3BExZudAua6MSIKSFYQtFOYAll7XobYwcpokGA==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.5.29: + resolution: {integrity: sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yaml@2.8.2: + resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + +snapshots: + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.27.3': + optional: true + + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-x64@0.27.3': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@rolldown/pluginutils@1.0.0-rc.2': {} + + '@rollup/rollup-android-arm-eabi@4.59.0': + optional: true + + '@rollup/rollup-android-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-x64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.59.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.59.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.59.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.59.0': + optional: true + + '@tailwindcss/node@4.2.1': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.20.0 + jiti: 2.6.1 + lightningcss: 1.31.1 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.2.1 + + '@tailwindcss/oxide-android-arm64@4.2.1': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.2.1': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.2.1': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.2.1': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.2.1': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.2.1': + optional: true + + '@tailwindcss/oxide@4.2.1': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.2.1 + '@tailwindcss/oxide-darwin-arm64': 4.2.1 + '@tailwindcss/oxide-darwin-x64': 4.2.1 + '@tailwindcss/oxide-freebsd-x64': 4.2.1 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.1 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.1 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.1 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.1 + '@tailwindcss/oxide-linux-x64-musl': 4.2.1 + '@tailwindcss/oxide-wasm32-wasi': 4.2.1 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.1 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.1 + + '@tailwindcss/vite@4.2.1(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@tailwindcss/node': 4.2.1 + '@tailwindcss/oxide': 4.2.1 + tailwindcss: 4.2.1 + vite: 7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + + '@types/bcrypt@5.0.2': + dependencies: + '@types/node': 24.12.0 + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 24.12.0 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 24.12.0 + + '@types/cookie-parser@1.4.10(@types/express@5.0.6)': + dependencies: + '@types/express': 5.0.6 + + '@types/estree@1.0.8': {} + + '@types/express-serve-static-core@5.1.1': + dependencies: + '@types/node': 24.12.0 + '@types/qs': 6.15.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@5.0.6': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.1 + '@types/serve-static': 2.2.0 + + '@types/http-errors@2.0.5': {} + + '@types/jsonwebtoken@9.0.10': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 24.12.0 + + '@types/ms@2.1.0': {} + + '@types/node@24.12.0': + dependencies: + undici-types: 7.16.0 + + '@types/qs@6.15.0': {} + + '@types/range-parser@1.2.7': {} + + '@types/send@1.2.1': + dependencies: + '@types/node': 24.12.0 + + '@types/serve-static@2.2.0': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 24.12.0 + + '@vitejs/plugin-vue@6.0.4(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))': + dependencies: + '@rolldown/pluginutils': 1.0.0-rc.2 + vite: 7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + vue: 3.5.29(typescript@5.9.3) + + '@volar/language-core@2.4.28': + dependencies: + '@volar/source-map': 2.4.28 + + '@volar/source-map@2.4.28': {} + + '@volar/typescript@2.4.28': + dependencies: + '@volar/language-core': 2.4.28 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@vue-macros/common@3.1.2(vue@3.5.29(typescript@5.9.3))': + dependencies: + '@vue/compiler-sfc': 3.5.29 + ast-kit: 2.2.0 + local-pkg: 1.1.2 + magic-string-ast: 1.0.3 + unplugin-utils: 0.3.1 + optionalDependencies: + vue: 3.5.29(typescript@5.9.3) + + '@vue/compiler-core@3.5.29': + dependencies: + '@babel/parser': 7.29.0 + '@vue/shared': 3.5.29 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.29': + dependencies: + '@vue/compiler-core': 3.5.29 + '@vue/shared': 3.5.29 + + '@vue/compiler-sfc@3.5.29': + dependencies: + '@babel/parser': 7.29.0 + '@vue/compiler-core': 3.5.29 + '@vue/compiler-dom': 3.5.29 + '@vue/compiler-ssr': 3.5.29 + '@vue/shared': 3.5.29 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.8 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.29': + dependencies: + '@vue/compiler-dom': 3.5.29 + '@vue/shared': 3.5.29 + + '@vue/devtools-api@7.7.9': + dependencies: + '@vue/devtools-kit': 7.7.9 + + '@vue/devtools-api@8.0.7': + dependencies: + '@vue/devtools-kit': 8.0.7 + + '@vue/devtools-kit@7.7.9': + dependencies: + '@vue/devtools-shared': 7.7.9 + birpc: 2.9.0 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.6 + + '@vue/devtools-kit@8.0.7': + dependencies: + '@vue/devtools-shared': 8.0.7 + birpc: 2.9.0 + hookable: 5.5.3 + perfect-debounce: 2.1.0 + + '@vue/devtools-shared@7.7.9': + dependencies: + rfdc: 1.4.1 + + '@vue/devtools-shared@8.0.7': {} + + '@vue/language-core@3.2.5': + dependencies: + '@volar/language-core': 2.4.28 + '@vue/compiler-dom': 3.5.29 + '@vue/shared': 3.5.29 + alien-signals: 3.1.2 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + picomatch: 4.0.3 + + '@vue/reactivity@3.5.29': + dependencies: + '@vue/shared': 3.5.29 + + '@vue/runtime-core@3.5.29': + dependencies: + '@vue/reactivity': 3.5.29 + '@vue/shared': 3.5.29 + + '@vue/runtime-dom@3.5.29': + dependencies: + '@vue/reactivity': 3.5.29 + '@vue/runtime-core': 3.5.29 + '@vue/shared': 3.5.29 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.29(vue@3.5.29(typescript@5.9.3))': + dependencies: + '@vue/compiler-ssr': 3.5.29 + '@vue/shared': 3.5.29 + vue: 3.5.29(typescript@5.9.3) + + '@vue/shared@3.5.29': {} + + '@vue/tsconfig@0.8.1(typescript@5.9.3)(vue@3.5.29(typescript@5.9.3))': + optionalDependencies: + typescript: 5.9.3 + vue: 3.5.29(typescript@5.9.3) + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + acorn@8.16.0: {} + + alien-signals@3.1.2: {} + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ast-kit@2.2.0: + dependencies: + '@babel/parser': 7.29.0 + pathe: 2.0.3 + + ast-walker-scope@0.8.3: + dependencies: + '@babel/parser': 7.29.0 + ast-kit: 2.2.0 + + bcrypt@6.0.0: + dependencies: + node-addon-api: 8.6.0 + node-gyp-build: 4.8.4 + + birpc@2.9.0: {} + + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.0 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + buffer-equal-constant-time@1.0.1: {} + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concurrently@9.2.1: + dependencies: + chalk: 4.1.2 + rxjs: 7.8.2 + shell-quote: 1.8.3 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + + confbox@0.1.8: {} + + confbox@0.2.4: {} + + content-disposition@1.0.1: {} + + content-type@1.0.5: {} + + cookie-parser@1.4.7: + dependencies: + cookie: 0.7.2 + cookie-signature: 1.0.6 + + cookie-signature@1.0.6: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + copy-anything@4.0.5: + dependencies: + is-what: 5.5.0 + + csstype@3.2.3: {} + + data-uri-to-buffer@4.0.1: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + depd@2.0.0: {} + + detect-libc@2.1.2: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + ee-first@1.1.1: {} + + emoji-regex@8.0.0: {} + + encodeurl@2.0.0: {} + + enhanced-resolve@5.20.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + entities@7.0.1: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + esbuild@0.27.3: + 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 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + estree-walker@2.0.2: {} + + etag@1.8.1: {} + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + exsolve@1.0.8: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-tsconfig@4.13.6: + dependencies: + resolve-pkg-maps: 1.0.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hookable@5.5.3: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + inherits@2.0.4: {} + + ipaddr.js@1.9.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-promise@4.0.0: {} + + is-what@5.5.0: {} + + jiti@2.6.1: {} + + jsesc@3.1.0: {} + + json5@2.2.3: {} + + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.4 + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + + lightningcss-android-arm64@1.31.1: + optional: true + + lightningcss-darwin-arm64@1.31.1: + optional: true + + lightningcss-darwin-x64@1.31.1: + optional: true + + lightningcss-freebsd-x64@1.31.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.31.1: + optional: true + + lightningcss-linux-arm64-gnu@1.31.1: + optional: true + + lightningcss-linux-arm64-musl@1.31.1: + optional: true + + lightningcss-linux-x64-gnu@1.31.1: + optional: true + + lightningcss-linux-x64-musl@1.31.1: + optional: true + + lightningcss-win32-arm64-msvc@1.31.1: + optional: true + + lightningcss-win32-x64-msvc@1.31.1: + optional: true + + lightningcss@1.31.1: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.31.1 + lightningcss-darwin-arm64: 1.31.1 + lightningcss-darwin-x64: 1.31.1 + lightningcss-freebsd-x64: 1.31.1 + lightningcss-linux-arm-gnueabihf: 1.31.1 + lightningcss-linux-arm64-gnu: 1.31.1 + lightningcss-linux-arm64-musl: 1.31.1 + lightningcss-linux-x64-gnu: 1.31.1 + lightningcss-linux-x64-musl: 1.31.1 + lightningcss-win32-arm64-msvc: 1.31.1 + lightningcss-win32-x64-msvc: 1.31.1 + + local-pkg@1.1.2: + dependencies: + mlly: 1.8.1 + pkg-types: 2.3.0 + quansync: 0.2.11 + + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.once@4.1.1: {} + + magic-string-ast@1.0.3: + dependencies: + magic-string: 0.30.21 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mitt@3.0.1: {} + + mlly@1.8.1: + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.3 + + ms@2.1.3: {} + + muggle-string@0.4.1: {} + + nanoid@3.3.11: {} + + negotiator@1.0.0: {} + + node-addon-api@8.6.0: {} + + node-domexception@1.0.0: {} + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + node-gyp-build@4.8.4: {} + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + parseurl@1.3.3: {} + + path-browserify@1.0.1: {} + + path-to-regexp@8.3.0: {} + + pathe@2.0.3: {} + + perfect-debounce@1.0.0: {} + + perfect-debounce@2.1.0: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + pinia@3.0.4(typescript@5.9.3)(vue@3.5.29(typescript@5.9.3)): + dependencies: + '@vue/devtools-api': 7.7.9 + vue: 3.5.29(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.1 + pathe: 2.0.3 + + pkg-types@2.3.0: + dependencies: + confbox: 0.2.4 + exsolve: 1.0.8 + pathe: 2.0.3 + + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + qs@6.15.0: + dependencies: + side-channel: 1.1.0 + + quansync@0.2.11: {} + + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + + readdirp@5.0.0: {} + + require-directory@2.1.1: {} + + resolve-pkg-maps@1.0.0: {} + + rfdc@1.4.1: {} + + rollup@4.59.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@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.3 + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + scule@1.3.0: {} + + semver@7.7.4: {} + + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + shell-quote@1.8.3: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + source-map-js@1.2.1: {} + + speakingurl@14.0.1: {} + + statuses@2.0.2: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + superjson@2.2.6: + dependencies: + copy-anything: 4.0.5 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + tailwindcss@4.2.1: {} + + tapable@2.3.0: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + toidentifier@1.0.1: {} + + tree-kill@1.2.2: {} + + tslib@2.8.1: {} + + tsx@4.21.0: + dependencies: + esbuild: 0.27.3 + get-tsconfig: 4.13.6 + optionalDependencies: + fsevents: 2.3.3 + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + + typescript@5.9.3: {} + + ufo@1.6.3: {} + + undici-types@7.16.0: {} + + unpipe@1.0.0: {} + + unplugin-utils@0.3.1: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.3 + + unplugin@3.0.0: + dependencies: + '@jridgewell/remapping': 2.3.5 + picomatch: 4.0.3 + webpack-virtual-modules: 0.6.2 + + vary@1.1.2: {} + + vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2): + dependencies: + esbuild: 0.27.3 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.8 + rollup: 4.59.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.12.0 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.31.1 + tsx: 4.21.0 + yaml: 2.8.2 + + vscode-uri@3.1.0: {} + + vue-router@5.0.3(@vue/compiler-sfc@3.5.29)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3)): + dependencies: + '@babel/generator': 7.29.1 + '@vue-macros/common': 3.1.2(vue@3.5.29(typescript@5.9.3)) + '@vue/devtools-api': 8.0.7 + ast-walker-scope: 0.8.3 + chokidar: 5.0.0 + json5: 2.2.3 + local-pkg: 1.1.2 + magic-string: 0.30.21 + mlly: 1.8.1 + muggle-string: 0.4.1 + pathe: 2.0.3 + picomatch: 4.0.3 + scule: 1.3.0 + tinyglobby: 0.2.15 + unplugin: 3.0.0 + unplugin-utils: 0.3.1 + vue: 3.5.29(typescript@5.9.3) + yaml: 2.8.2 + optionalDependencies: + '@vue/compiler-sfc': 3.5.29 + pinia: 3.0.4(typescript@5.9.3)(vue@3.5.29(typescript@5.9.3)) + + vue-tsc@3.2.5(typescript@5.9.3): + dependencies: + '@volar/typescript': 2.4.28 + '@vue/language-core': 3.2.5 + typescript: 5.9.3 + + vue@3.5.29(typescript@5.9.3): + dependencies: + '@vue/compiler-dom': 3.5.29 + '@vue/compiler-sfc': 3.5.29 + '@vue/runtime-dom': 3.5.29 + '@vue/server-renderer': 3.5.29(vue@3.5.29(typescript@5.9.3)) + '@vue/shared': 3.5.29 + optionalDependencies: + typescript: 5.9.3 + + web-streams-polyfill@3.3.3: {} + + webpack-virtual-modules@0.6.2: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + y18n@5.0.8: {} + + yaml@2.8.2: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 diff --git a/server/auth.ts b/server/auth.ts new file mode 100644 index 0000000..647724d --- /dev/null +++ b/server/auth.ts @@ -0,0 +1,114 @@ +import fs from 'fs' +import path from 'path' +import bcrypt from 'bcrypt' +import jwt from 'jsonwebtoken' +import type { Request, Response, NextFunction } from 'express' + +const DATA_PATH = path.join(process.cwd(), 'data', 'auth.json') +const JWT_SECRET = process.env.JWT_SECRET || 'fankarr-secret-change-me' +const SALT_ROUNDS = 10 + +interface AuthData { + username: string + passwordHash: string +} + +// ── Helpers fichier ─────────────────────────────────────────── +function readAuth(): AuthData | null { + try { + if (!fs.existsSync(DATA_PATH)) return null + return JSON.parse(fs.readFileSync(DATA_PATH, 'utf-8')) + } catch { return null } +} + +function writeAuth(data: AuthData): void { + fs.mkdirSync(path.dirname(DATA_PATH), { recursive: true }) + fs.writeFileSync(DATA_PATH, JSON.stringify(data, null, 2)) +} + +// ── Routes ──────────────────────────────────────────────────── + +// GET /api/auth/status +export function authStatus(req: Request, res: Response): void { + const existing = readAuth() + const token = req.cookies?.fankarr_token + + let loggedIn = false + if (token) { + try { + jwt.verify(token, JWT_SECRET) + loggedIn = true + } catch {} + } + + res.json({ + setup: !!existing, + loggedIn + }) +} + +// POST /api/auth/setup +export function authSetup(req: Request, res: Response): void { + if (readAuth()) { + res.status(400).json({ error: 'Un compte existe déjà' }) + return + } + + const { username, password } = req.body + if (!username || !password) { + res.status(400).json({ error: 'Username et password requis' }) + return + } + + const passwordHash = bcrypt.hashSync(password, SALT_ROUNDS) + writeAuth({ username, passwordHash }) + + const token = jwt.sign({ username }, JWT_SECRET, { expiresIn: '30d' }) + res.cookie('fankarr_token', token, { httpOnly: true, sameSite: 'lax' }) + res.json({ success: true }) +} + +// POST /api/auth/login +export function authLogin(req: Request, res: Response): void { + const auth = readAuth() + if (!auth) { + res.status(400).json({ error: 'Aucun compte configuré' }) + return + } + + const { username, password } = req.body + if (!username || !password) { + res.status(400).json({ error: 'Username et password requis' }) + return + } + + if (username !== auth.username || !bcrypt.compareSync(password, auth.passwordHash)) { + res.status(401).json({ error: 'Identifiants incorrects' }) + return + } + + const token = jwt.sign({ username }, JWT_SECRET, { expiresIn: '30d' }) + res.cookie('fankarr_token', token, { httpOnly: true, sameSite: 'lax' }) + res.json({ success: true }) +} + +// POST /api/auth/logout +export function authLogout(_req: Request, res: Response): void { + res.clearCookie('fankarr_token') + res.json({ success: true }) +} + +// Middleware protection des routes +export function requireAuth(req: Request, res: Response, next: NextFunction): void { + const token = req.cookies?.fankarr_token + if (!token) { + res.status(401).json({ error: 'Non authentifié' }) + return + } + try { + jwt.verify(token, JWT_SECRET) + next() + } catch { + res.status(401).json({ error: 'Token invalide' }) + } +} \ No newline at end of file diff --git a/server/index.ts b/server/index.ts new file mode 100644 index 0000000..c13ce9b --- /dev/null +++ b/server/index.ts @@ -0,0 +1,312 @@ +import express from 'express' +import cookieParser from 'cookie-parser' +import fs from 'fs' +import path from 'path' +import { authStatus, authSetup, authLogin, authLogout, requireAuth } from './auth.js' +import { readSettings, writeSettings } from './settings.js' +import { + registerDriver, getDriver, getAvailableClients, + listClients, addClient, removeClient, getClient, + sanitizeClient, dispatchDownload, dispatchList +} from './torrent-clients' +import qbittorrentDriver from './torrent-clients/qbittorrent.js' +import { organizeTorrent, autoOrganizeAll } from './organize.js' + +registerDriver(qbittorrentDriver) + +const app = express() +const PORT = 3001 +const FANKAI_API = 'https://metadata.fankai.fr' +const TORRENTS_PATH = path.join(process.cwd(), 'data', 'torrent_final.json') +const GITHUB_RAW_URL = process.env.GITHUB_RAW_URL + ?? 'https://raw.githubusercontent.com/CHANGE_ME/fankarr-scraper/main/data/torrent_final.json' + +app.use(express.json()) +app.use(cookieParser()) + +// ── Static frontend (prod) ───────────────────────────────────── +const PUBLIC_PATH = path.join(process.cwd(), 'public') +if (fs.existsSync(PUBLIC_PATH)) { + app.use(express.static(PUBLIC_PATH)) +} + +// ── Auth ─────────────────────────────────────────────────────── +app.get('/api/auth/status', authStatus) +app.post('/api/auth/setup', authSetup) +app.post('/api/auth/login', authLogin) +app.post('/api/auth/logout', authLogout) + +// ── Settings ─────────────────────────────────────────────────── +app.get('/api/settings', requireAuth, (_req, res) => { + res.json(readSettings()) +}) +app.post('/api/settings', requireAuth, (req, res) => { + const { mediaPath, completePath, organizeMode, category } = req.body + res.json(writeSettings({ mediaPath, completePath, organizeMode, category })) +}) + +// ── Torrent clients ──────────────────────────────────────────── +app.get('/api/torrent-clients/available', requireAuth, (_req, res) => { + res.json(getAvailableClients()) +}) +app.get('/api/torrent-clients', requireAuth, (_req, res) => { + res.json(listClients().map(c => sanitizeClient(c))) +}) +app.post('/api/torrent-clients', requireAuth, (req, res) => { + const { name, type, config } = req.body + if (!name || !type || !config) { res.status(400).json({ error: 'name, type et config requis' }); return } + if (!getDriver(type)) { res.status(400).json({ error: `Type inconnu : ${type}` }); return } + res.json(sanitizeClient(addClient(name, type, config))) +}) +app.delete('/api/torrent-clients/:uuid', requireAuth, (req, res) => { + const ok = removeClient(String(req.params.uuid)) + if (!ok) res.status(404).json({ error: 'Client introuvable' }) + else res.json({ ok: true }) +}) +app.post('/api/torrent-clients/test-config', requireAuth, async (req, res) => { + const { type, config } = req.body + if (!type || !config) { res.status(400).json({ error: 'type et config requis' }); return } + const driver = getDriver(type) + if (!driver) { res.status(400).json({ error: `Type inconnu : ${type}` }); return } + res.json(await driver.test(config)) +}) +app.post('/api/torrent-clients/:uuid/test', requireAuth, async (req, res) => { + const client = getClient(String(req.params.uuid)) + if (!client) { res.status(404).json({ error: 'Client introuvable' }); return } + const driver = getDriver(client.type) + if (!driver) { res.status(400).json({ error: 'Driver introuvable' }); return } + res.json(await driver.test(client.config)) +}) +app.get('/api/torrent-clients/:uuid/healthcheck', requireAuth, async (req, res) => { + const client = getClient(String(req.params.uuid)) + if (!client) { res.status(404).json({ error: 'Client introuvable' }); return } + const driver = getDriver(client.type) + if (!driver) { res.status(400).json({ error: 'Driver introuvable' }); return } + res.json(await driver.healthcheck(client.config)) +}) + +// ── Helpers ──────────────────────────────────────────────────── +function readTorrents(): any[] { + try { + if (!fs.existsSync(TORRENTS_PATH)) return [] + return JSON.parse(fs.readFileSync(TORRENTS_PATH, 'utf-8')) + } catch { return [] } +} +async function fankaiGet(endpoint: string): Promise { + const res = await fetch(`${FANKAI_API}${endpoint}`) + if (!res.ok) throw new Error(`Fankai API ${res.status}: ${endpoint}`) + return res.json() +} +function indexBySerie(torrents: any[]): Map { + const map = new Map() + for (const t of torrents) { + if (!t.serie_id) continue + if (!map.has(t.serie_id)) map.set(t.serie_id, []) + map.get(t.serie_id)!.push(t) + } + return map +} + +// ── Series ───────────────────────────────────────────────────── +app.get('/api/series', requireAuth, async (_req, res) => { + try { + const [apiData, torrents] = await Promise.all([fankaiGet('/series'), Promise.resolve(readTorrents())]) + const byId = indexBySerie(torrents) + const seriesList = Array.isArray(apiData) ? apiData : (apiData.series ?? []) + res.json({ series: seriesList.map((serie: any) => ({ + ...serie, + torrent_count: byId.get(serie.id)?.length ?? 0, + has_torrents : byId.has(serie.id) + }))}) + } catch (err) { + res.status(500).json({ error: err instanceof Error ? err.message : 'Erreur inconnue' }) + } +}) + +app.get('/api/series/:id', requireAuth, async (req, res) => { + const id = Number(req.params.id) + try { + const [serie, seasonsData] = await Promise.all([fankaiGet(`/series/${id}`), fankaiGet(`/series/${id}/seasons`)]) + const seasons = Array.isArray(seasonsData) ? seasonsData : (seasonsData.seasons ?? []) + const seasonsWithEpisodes = await Promise.all(seasons.map(async (season: any) => { + const epsData = await fankaiGet(`/seasons/${season.id}/episodes`) + return { ...season, episodes: Array.isArray(epsData) ? epsData : (epsData.episodes ?? []) } + })) + + const torrents = readTorrents().filter(t => t.serie_id === id) + const allSeasonIds = new Set(seasonsWithEpisodes.filter((s: any) => s.season_number !== 0).map((s: any) => s.id)) + const availableEpisodeIds = new Set() + const episodeTorrentMap: Record = {} + const seasonTorrentMap : Record = {} + const integraleTorrents: any[] = [] + const packEpisodesTorrents: any[] = [] + + for (const t of torrents) { + const ref = { torrent_url: t.torrent_url, magnet: t.magnet, type: t.type, raw: t.raw } + for (const ep of t.resolved_episodes ?? []) { + availableEpisodeIds.add(ep.episode_id) + if (t.type === 'episode') episodeTorrentMap[ep.episode_id] = ref + } + if (t.type === 'pack_saison') { + const rs: any[] = t.resolved_seasons ?? [] + if (rs.length > 1) { + packEpisodesTorrents.push(t) + } else { + const sids = rs.length > 0 ? rs.map((r: any) => r.season_id) : [t.season_id] + for (const sid of sids) { if (sid) seasonTorrentMap[sid] = ref } + } + } else if (t.type === 'pack_integrale') { + integraleTorrents.push(t) + } else if (t.type === 'pack_episodes') { + packEpisodesTorrents.push(t) + } + } + + let promotedIntegrale: any = null + if (integraleTorrents.length === 0 && allSeasonIds.size > 0) { + for (const t of torrents.filter(t => t.type === 'pack_saison')) { + const rs: any[] = t.resolved_seasons ?? [] + const tIds = new Set(rs.length > 0 ? rs.map((r: any) => r.season_id) : t.season_id ? [t.season_id] : []) + if ([...allSeasonIds].every(sid => tIds.has(sid))) { promotedIntegrale = t; break } + } + } + + if (integraleTorrents.length > 0 || promotedIntegrale) { + for (const key of Object.keys(seasonTorrentMap)) delete seasonTorrentMap[Number(key)] + if (promotedIntegrale) { + const idx = packEpisodesTorrents.indexOf(promotedIntegrale) + if (idx !== -1) packEpisodesTorrents.splice(idx, 1) + } + } + + function packEpisodesLabel(t: any): string { + const eps: any[] = t.resolved_episodes ?? [] + if (!eps.length) return t.raw + const bySeason = new Map() + for (const ep of eps) { + const sn = ep.season_number ?? 0 + if (!bySeason.has(sn)) bySeason.set(sn, []) + bySeason.get(sn)!.push(ep.episode_number) + } + return [...bySeason.entries()].sort(([a], [b]) => a - b).map(([sn, nums]) => { + const s = nums.sort((a, b) => a - b) + const label = sn === 0 ? 'S0' : `S${sn}` + return s[0] === s[s.length-1] ? `${label} Ep ${s[0]}` : `${label} Eps ${s[0]}-${s[s.length-1]}` + }).join(' · ') + } + + const enrichedSeasons = seasonsWithEpisodes.map((season: any) => ({ + ...season, + torrent: seasonTorrentMap[season.id] ?? null, + episodes: season.episodes.map((ep: any) => ({ + ...ep, + available: availableEpisodeIds.has(ep.id), + torrent : episodeTorrentMap[ep.id] ?? null + })) + })) + + const heroBundles: any[] = [ + ...integraleTorrents.map(t => ({ label: 'Intégrale', torrent_url: t.torrent_url, magnet: t.magnet, raw: t.raw })), + ...(promotedIntegrale ? [{ label: 'Intégrale', torrent_url: promotedIntegrale.torrent_url, magnet: promotedIntegrale.magnet, raw: promotedIntegrale.raw }] : []), + ...packEpisodesTorrents.map(t => ({ label: packEpisodesLabel(t), torrent_url: t.torrent_url, magnet: t.magnet, raw: t.raw })) + ] + + res.json({ serie, seasons: enrichedSeasons, torrents_integrale: heroBundles }) + } catch (err) { + res.status(500).json({ error: err instanceof Error ? err.message : 'Erreur inconnue' }) + } +}) + +// ── Download ─────────────────────────────────────────────────── +app.post('/api/download', requireAuth, async (req, res) => { + const url = req.body.torrent_url ?? req.body.magnet + if (!url) { res.status(400).json({ error: 'torrent_url ou magnet requis' }); return } + const results = await dispatchDownload(url) + res.status(results.every(r => r.ok) ? 200 : 207).json({ results }) +}) + +// ── Downloads — liste torrents Fankai ────────────────────────── +app.get('/api/downloads', requireAuth, async (_req, res) => { + try { + const { category } = readSettings() + const torrents = await dispatchList(category ?? 'fankai') + res.json(torrents) + } catch (err) { + res.status(500).json({ error: err instanceof Error ? err.message : 'Erreur inconnue' }) + } +}) + +// ── Organize ─────────────────────────────────────────────────── +app.post('/api/organize', requireAuth, async (req, res) => { + const { hash, name, save_path } = req.body + if (!hash || !name || !save_path) { + res.status(400).json({ error: 'hash, name et save_path requis' }) + return + } + try { + const result = await organizeTorrent(hash, name, save_path) + res.json(result) + } catch (err) { + res.status(500).json({ error: err instanceof Error ? err.message : 'Erreur inconnue' }) + } +}) + +// ── Torrents status ──────────────────────────────────────────── +app.get('/api/torrents/status', requireAuth, (_req, res) => { + try { + if (!fs.existsSync(TORRENTS_PATH)) { res.json({ exists: false, count: 0, empty: true }); return } + const raw = fs.readFileSync(TORRENTS_PATH, 'utf-8').trim() + if (!raw || raw === '[]') { res.json({ exists: true, count: 0, empty: true }); return } + const data = JSON.parse(raw) + const count = Array.isArray(data) ? data.length : 0 + res.json({ exists: true, count, empty: count === 0 }) + } catch { res.json({ exists: false, count: 0, empty: true }) } +}) + +// ── Update torrent_final.json ────────────────────────────────── +app.post('/api/update', requireAuth, async (_req, res) => { + try { + const response = await fetch(GITHUB_RAW_URL) + if (!response.ok) throw new Error(`GitHub a répondu ${response.status}`) + const data = await response.json() as unknown[] + if (!Array.isArray(data)) throw new Error('Format invalide') + fs.mkdirSync(path.dirname(TORRENTS_PATH), { recursive: true }) + fs.writeFileSync(TORRENTS_PATH, JSON.stringify(data, null, 2), 'utf-8') + res.json({ ok: true, count: data.length }) + } catch (err) { + res.status(500).json({ error: err instanceof Error ? err.message : 'Erreur inconnue' }) + } +}) + +// ── SPA fallback (prod) — doit être après toutes les routes API ─ +if (fs.existsSync(PUBLIC_PATH)) { + app.get('*path', (_req, res) => { + res.sendFile(path.join(PUBLIC_PATH, 'index.html')) + }) +} + +// ── Start ────────────────────────────────────────────────────── +app.listen(PORT, () => { + try { + const data = JSON.parse(fs.readFileSync(TORRENTS_PATH, 'utf-8')) + const count = Array.isArray(data) ? data.length : 0 + if (count === 0) console.warn('[fankarr] ⚠ torrent_final.json est vide') + else console.log(`[fankarr] ${count} torrents chargés`) + } catch { console.warn('[fankarr] ⚠ torrent_final.json introuvable ou illisible') } + console.log(`[fankarr] Serveur sur http://localhost:${PORT}`) + + // Auto-organise toutes les 30s + const autoOrganize = async () => { + try { + const { category } = readSettings() + await autoOrganizeAll(() => dispatchList(category ?? 'fankai')) + } catch (err) { + console.error('[organize] Erreur auto-organise:', err) + } + } + // Premier run 30s après démarrage, puis toutes les 30s + setTimeout(() => { + autoOrganize() + setInterval(autoOrganize, 30_000) + }, 30_000) +}) \ No newline at end of file diff --git a/server/organize.ts b/server/organize.ts new file mode 100644 index 0000000..e296f25 --- /dev/null +++ b/server/organize.ts @@ -0,0 +1,248 @@ +/** + * Organizer + * ========= + * Déplace ou hardlinke les fichiers téléchargés vers la médiathèque Jellyfin. + * Incrémental : skip les fichiers déjà organisés. + * + * Structure dest : + * mediaPath / serie_title / Saison 001 / filename.mkv + */ + +import fs from 'fs' +import path from 'path' +import { readSettings } from './settings.js' + +const TORRENTS_PATH = path.join(process.cwd(), 'data', 'torrent_final.json') +const ORGANIZED_PATH = path.join(process.cwd(), 'data', 'organized.json') + +// ─── Organized log ──────────────────────────────────────────── + +function loadOrganized(): Record> { + try { + if (!fs.existsSync(ORGANIZED_PATH)) return {} + return JSON.parse(fs.readFileSync(ORGANIZED_PATH, 'utf-8')) + } catch { return {} } +} + +function saveOrganized(data: Record>) { + fs.writeFileSync(ORGANIZED_PATH, JSON.stringify(data, null, 2), 'utf-8') +} + +function markOrganized(hash: string, filename: string) { + const data = loadOrganized() + if (!data[hash]) data[hash] = {} + data[hash][filename] = new Date().toISOString() + saveOrganized(data) +} + +function isOrganized(hash: string, filename: string): boolean { + const data = loadOrganized() + return !!data[hash]?.[filename] +} + +// ─── Torrent lookup ─────────────────────────────────────────── + +function readTorrents(): any[] { + try { + if (!fs.existsSync(TORRENTS_PATH)) return [] + return JSON.parse(fs.readFileSync(TORRENTS_PATH, 'utf-8')) + } catch { return [] } +} + +function findTorrentByName(name: string): any | null { + const torrents = readTorrents() + return torrents.find(t => t.raw === name) ?? null +} + +// ─── Filesystem ops ─────────────────────────────────────────── + +function ensureDir(dir: string) { + fs.mkdirSync(dir, { recursive: true }) +} + +function seasonFolder(n: number): string { + return `Saison ${String(n).padStart(3, '0')}` +} + +function tryHardlink(src: string, dest: string): boolean { + try { + fs.linkSync(src, dest) + return true + } catch { + return false + } +} + +function copyFile(src: string, dest: string) { + fs.copyFileSync(src, dest) +} + +// ─── Résultat d'organisation ────────────────────────────────── + +export interface OrganizeResult { + total : number + skipped : number + done : number + errors : { file: string; error: string }[] +} + +// ─── Organise un torrent ────────────────────────────────────── + +export async function organizeTorrent( + hash : string, + name : string, // nom du dossier torrent dans completePath + savePath: string // save_path retourné par qBittorrent +): Promise { + const settings = readSettings() + const { mediaPath, completePath, organizeMode } = settings + + const result: OrganizeResult = { total: 0, skipped: 0, done: 0, errors: [] } + + // Trouver le torrent dans torrent_final.json + const torrent = findTorrentByName(name) + if (!torrent) { + throw new Error(`Torrent introuvable dans torrent_final.json : "${name}"`) + } + + const serieTitle = torrent.serie_title || torrent.show_title || name + const torrentFiles: any[] = torrent.torrent_files ?? [] + + if (torrentFiles.length === 0) { + throw new Error(`Aucun fichier trouvé dans le torrent "${name}"`) + } + + // Résoudre season_number par episode_id + const epSeasonMap = new Map() + for (const ep of torrent.resolved_episodes ?? []) { + if (ep.filename) { + epSeasonMap.set(ep.episode_number, ep.season_number) + } + } + // Map filename → season_number + const filenameSeasonMap = new Map() + for (const ep of torrent.resolved_episodes ?? []) { + if (ep.filename && ep.season_number !== undefined) { + filenameSeasonMap.set(ep.filename, ep.season_number) + } + } + + result.total = torrentFiles.length + + for (const tf of torrentFiles) { + const filename = tf.filename + const filePath: string[] = tf.path // ex: ["Saga 1 - ...", "file.mkv"] + + // Skip si déjà organisé + if (isOrganized(hash, filename)) { + result.skipped++ + continue + } + + // Construire le chemin source + // save_path peut déjà contenir le nom du dossier ou pas selon le client + const torrentDir = path.join(savePath, name) + const torrentDirAlt = savePath // au cas où save_path inclut déjà le nom + + // Chemin source = dossier torrent + sous-dossiers + fichier + const subPath = filePath.length > 1 + ? path.join(...filePath.slice(0, -1), filename) + : filename + + let src = path.join(torrentDir, subPath) + if (!fs.existsSync(src)) { + // Fallback: save_path sans le nom du torrent + src = path.join(torrentDirAlt, subPath) + } + if (!fs.existsSync(src)) { + // Fallback: fichier directement dans save_path/name + src = path.join(torrentDir, filename) + } + if (!fs.existsSync(src)) { + result.errors.push({ file: filename, error: `Source introuvable : ${src}` }) + continue + } + + // Déterminer la saison + let seasonNum = filenameSeasonMap.get(filename) + if (seasonNum === undefined) { + // Fallback : season_number = 1 + seasonNum = 1 + } + + // Construire le chemin destination + const destDir = path.join(mediaPath, serieTitle, seasonFolder(seasonNum)) + const dest = path.join(destDir, filename) + + // Skip si dest existe déjà + if (fs.existsSync(dest)) { + markOrganized(hash, filename) + result.skipped++ + continue + } + + try { + ensureDir(destDir) + + if (organizeMode === 'hardlink') { + const ok = tryHardlink(src, dest) + if (!ok) { + // Hardlink impossible (filesystems différents) → copy + console.warn(`[organize] hardlink échoué pour ${filename}, fallback copy`) + copyFile(src, dest) + } + } else { + // move = copy + delete source + copyFile(src, dest) + fs.unlinkSync(src) + } + + markOrganized(hash, filename) + result.done++ + console.log(`[organize] ${organizeMode} ${filename} → ${dest}`) + } catch (err) { + result.errors.push({ + file : filename, + error: err instanceof Error ? err.message : 'Erreur inconnue' + }) + } + } + + return result +} + +// ─── Auto-organise polling ──────────────────────────────────── +// Appelé depuis index.ts au démarrage, toutes les 30s +// Détecte les torrents "seeding" non encore organisés + +export async function autoOrganizeAll( + listFn: () => Promise +): Promise { + let torrents: any[] + try { + torrents = await listFn() + } catch { return } + + const organized = loadOrganized() + + for (const t of torrents) { + if (t.state !== 'seeding') continue + + // Vérifier si tous les fichiers sont déjà organisés + const torrent = findTorrentByName(t.name) + if (!torrent) continue + + const files: any[] = torrent.torrent_files ?? [] + if (files.length === 0) continue + + const allDone = files.every(f => organized[t.hash]?.[f.filename]) + if (allDone) continue + + console.log(`[organize] Auto-organisation : ${t.name}`) + try { + const result = await organizeTorrent(t.hash, t.name, t.save_path) + console.log(`[organize] ${t.name} → ${result.done} fichiers, ${result.skipped} skippés, ${result.errors.length} erreurs`) + } catch (err) { + console.error(`[organize] Erreur pour ${t.name}:`, err) + } + } +} \ No newline at end of file diff --git a/server/settings.ts b/server/settings.ts new file mode 100644 index 0000000..bde8903 --- /dev/null +++ b/server/settings.ts @@ -0,0 +1,33 @@ +import fs from 'fs' +import path from 'path' + +const DATA_PATH = path.join(process.cwd(), 'data', 'settings.json') + +export interface Settings { + mediaPath : string // dossier Jellyfin destination ex: /media/Kai + completePath : string // dossier qB complétion ex: /downloads/complete + organizeMode : 'hardlink' | 'move' + category : string // catégorie qBittorrent +} + +const defaults: Settings = { + mediaPath : '/media/Kai', + completePath : '/downloads/complete', + organizeMode : 'hardlink', + category : 'fankai', +} + +export function readSettings(): Settings { + try { + if (!fs.existsSync(DATA_PATH)) return { ...defaults } + return { ...defaults, ...JSON.parse(fs.readFileSync(DATA_PATH, 'utf-8')) } + } catch { return { ...defaults } } +} + +export function writeSettings(settings: Partial): Settings { + const current = readSettings() + const updated = { ...current, ...settings } + fs.mkdirSync(path.dirname(DATA_PATH), { recursive: true }) + fs.writeFileSync(DATA_PATH, JSON.stringify(updated, null, 2)) + return updated +} \ No newline at end of file diff --git a/server/torrent-clients/index.ts b/server/torrent-clients/index.ts new file mode 100644 index 0000000..eb6aa8e --- /dev/null +++ b/server/torrent-clients/index.ts @@ -0,0 +1,181 @@ +/** + * Torrent Client Registry + * ======================= + * Interface commune + registry de tous les clients disponibles. + * Chaque client expose son schema de configuration (FieldDef[]) + * pour que le frontend génère le formulaire dynamiquement. + */ + +import { randomUUID } from 'crypto' +import fs from 'fs' +import path from 'path' + +// ─── Types ──────────────────────────────────────────────────────────────────── + +export interface FieldDef { + key : string + label : string + type : 'text' | 'password' | 'number' | 'url' + placeholder?: string + required : boolean + default? : string | number +} + +/** Définition statique d'un type de client (ce qu'il expose au front) */ +export interface TorrentClientDefinition { + id : string // "qbittorrent", "transmission", etc. + label : string // "qBittorrent" + fields : FieldDef[] // schema du formulaire +} + +/** Instance sauvegardée d'un client configuré par l'utilisateur */ +export interface SavedClient { + uuid : string // identifiant unique de cette instance + name : string // nom custom donné par l'utilisateur + type : string // id du TorrentClientDefinition + config: Record +} + +/** État normalisé d'un torrent (commun à tous les clients) */ +export interface TorrentInfo { + hash : string + name : string + state : 'downloading' | 'seeding' | 'paused' | 'checking' | 'error' | 'unknown' + progress : number // 0-100 + size : number // bytes + downloaded: number // bytes + speed : number // bytes/s + eta : number // secondes, -1 si inconnu + save_path : string // dossier de complétion + category : string +} + +/** Interface que chaque client doit implémenter */ +export interface TorrentClientDriver { + definition : TorrentClientDefinition + test : (config: Record) => Promise<{ ok: boolean; message: string }> + healthcheck : (config: Record) => Promise<{ online: boolean; version?: string }> + add : (config: Record, url: string) => Promise + list : (config: Record, category?: string) => Promise +} + +// ─── Registry ───────────────────────────────────────────────────────────────── + +const drivers = new Map() + +export function registerDriver(driver: TorrentClientDriver) { + drivers.set(driver.definition.id, driver) +} + +export function getDriver(type: string): TorrentClientDriver | undefined { + return drivers.get(type) +} + +export function getAvailableClients(): TorrentClientDefinition[] { + return [...drivers.values()].map(d => d.definition) +} + +// ─── Persistence (clients enregistrés) ──────────────────────────────────────── + +const CLIENTS_PATH = path.join(process.cwd(), 'data', 'torrent_clients.json') + +function loadClients(): SavedClient[] { + try { + if (!fs.existsSync(CLIENTS_PATH)) return [] + const raw = fs.readFileSync(CLIENTS_PATH, 'utf-8').trim() + if (!raw || raw === '[]') return [] + return JSON.parse(raw) + } catch { return [] } +} + +function saveClients(clients: SavedClient[]) { + fs.mkdirSync(path.dirname(CLIENTS_PATH), { recursive: true }) + fs.writeFileSync(CLIENTS_PATH, JSON.stringify(clients, null, 2), 'utf-8') +} + +export function listClients(): SavedClient[] { + return loadClients() +} + +export function addClient(name: string, type: string, config: Record): SavedClient { + const clients = loadClients() + const client: SavedClient = { uuid: randomUUID(), name, type, config } + clients.push(client) + saveClients(clients) + return client +} + +export function removeClient(uuid: string): boolean { + const clients = loadClients() + const filtered = clients.filter(c => c.uuid !== uuid) + if (filtered.length === clients.length) return false + saveClients(filtered) + return true +} + +export function getClient(uuid: string): SavedClient | undefined { + return loadClients().find(c => c.uuid === uuid) +} + +/** Masque les champs password pour l'envoi au front */ +export function sanitizeClient(client: SavedClient, definition?: TorrentClientDefinition): SavedClient { + if (!definition) definition = getDriver(client.type)?.definition + if (!definition) return client + const config = { ...client.config } + for (const field of definition.fields) { + if (field.type === 'password' && config[field.key]) { + config[field.key] = '••••••••' + } + } + return { ...client, config } +} + +// ─── Dispatch download vers tous les clients ────────────────────────────────── + +export async function dispatchDownload(url: string): Promise<{ uuid: string; name: string; ok: boolean; error?: string }[]> { + const clients = loadClients() + const results = [] + + for (const client of clients) { + const driver = getDriver(client.type) + if (!driver) { + results.push({ uuid: client.uuid, name: client.name, ok: false, error: `Driver "${client.type}" introuvable` }) + continue + } + try { + await driver.add(client.config, url) + results.push({ uuid: client.uuid, name: client.name, ok: true }) + } catch (err) { + results.push({ + uuid : client.uuid, + name : client.name, + ok : false, + error: err instanceof Error ? err.message : 'Erreur inconnue' + }) + } + } + + return results +} + +// ─── Dispatch list vers tous les clients ───────────────────────────────────── + +export async function dispatchList(category?: string): Promise<(TorrentInfo & { client_uuid: string; client_name: string })[]> { + const clients = loadClients() + const results: (TorrentInfo & { client_uuid: string; client_name: string })[] = [] + + for (const client of clients) { + const driver = getDriver(client.type) + if (!driver) continue + try { + const torrents = await driver.list(client.config, category) + for (const t of torrents) { + results.push({ ...t, client_uuid: client.uuid, client_name: client.name }) + } + } catch (err) { + console.error(`[torrent-clients] list() échoué pour ${client.name}:`, err) + } + } + + return results +} \ No newline at end of file diff --git a/server/torrent-clients/qbittorrent.ts b/server/torrent-clients/qbittorrent.ts new file mode 100644 index 0000000..40dbb47 --- /dev/null +++ b/server/torrent-clients/qbittorrent.ts @@ -0,0 +1,110 @@ +/** + * qBittorrent Driver + */ + +import type { TorrentClientDriver, TorrentInfo } from './index.js' + +// Mapping état qBittorrent → état normalisé +function mapState(state: string): TorrentInfo['state'] { + if (['downloading', 'metaDL', 'queuedDL', 'stalledDL', 'forcedDL'].includes(state)) return 'downloading' + if (['uploading', 'queuedUP', 'stalledUP', 'forcedUP'].includes(state)) return 'seeding' + if (['pausedDL', 'pausedUP'].includes(state)) return 'paused' + if (['checkingDL', 'checkingUP', 'checkingResumeData'].includes(state)) return 'checking' + if (state === 'error' || state === 'missingFiles') return 'error' + return 'unknown' +} + +async function qbLogin(config: Record): Promise { + const form = new URLSearchParams() + form.append('username', String(config.username ?? '')) + form.append('password', String(config.password ?? '')) + + const res = await fetch(`${config.url}/api/v2/auth/login`, { + method : 'POST', + body : form, + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + }) + + const text = await res.text() + if (text !== 'Ok.') throw new Error(`Login échoué : ${text}`) + + const cookie = res.headers.get('set-cookie') ?? '' + const sid = cookie.match(/SID=([^;]+)/)?.[1] + if (!sid) throw new Error('Cookie SID introuvable') + return sid +} + +const QB: TorrentClientDriver = { + definition: { + id : 'qbittorrent', + label : 'qBittorrent', + fields: [ + { key: 'url', label: 'URL WebUI', type: 'url', placeholder: 'http://localhost:8080', required: true }, + { key: 'username', label: 'Identifiant', type: 'text', placeholder: 'admin', required: true }, + { key: 'password', label: 'Mot de passe', type: 'password', placeholder: '••••••••', required: true }, + { key: 'category', label: 'Catégorie', type: 'text', placeholder: 'fankai', required: false, default: 'fankai' }, + ], + }, + + async test(config) { + try { + await qbLogin(config) + return { ok: true, message: 'Connexion réussie' } + } catch (err) { + return { ok: false, message: err instanceof Error ? err.message : 'Erreur inconnue' } + } + }, + + async healthcheck(config) { + try { + const sid = await qbLogin(config) + const res = await fetch(`${config.url}/api/v2/app/version`, { + headers: { Cookie: `SID=${sid}` } + }) + if (!res.ok) return { online: false } + return { online: true, version: (await res.text()).trim() } + } catch { + return { online: false } + } + }, + + async list(config, category) { + const sid = await qbLogin(config) + const params = new URLSearchParams() + if (category) params.set('category', category) + const res = await fetch(`${config.url}/api/v2/torrents/info?${params}`, { + headers: { Cookie: `SID=${sid}` } + }) + if (!res.ok) throw new Error(`qB list échoué: ${res.status}`) + const data: any[] = await res.json() + return data.map(t => ({ + hash : t.hash, + name : t.name, + state : mapState(t.state), + progress : Math.round(t.progress * 100), + size : t.size, + downloaded: t.downloaded, + speed : t.dlspeed, + eta : t.eta ?? -1, + save_path : t.save_path, + category : t.category ?? '', + } satisfies TorrentInfo)) + }, + + async add(config, url) { + const sid = await qbLogin(config) + const form = new FormData() + form.append('urls', url) + if (config.category) form.append('category', String(config.category)) + if (config.savePath) form.append('savepath', String(config.savePath)) + const res = await fetch(`${config.url}/api/v2/torrents/add`, { + method : 'POST', + body : form, + headers: { Cookie: `SID=${sid}` }, + }) + const text = await res.text() + if (text !== 'Ok.') throw new Error(`Ajout échoué : ${text}`) + }, +} + +export default QB \ No newline at end of file diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..e2c6869 --- /dev/null +++ b/src/App.vue @@ -0,0 +1,14 @@ + + + \ No newline at end of file diff --git a/src/api/fankai.ts b/src/api/fankai.ts new file mode 100644 index 0000000..53ef6ff --- /dev/null +++ b/src/api/fankai.ts @@ -0,0 +1,80 @@ +const BASE = '/api' + +export interface Production { + id: number + name: string + description: string + kaieur: string + cover: string | null + background: string | null + banner: string | null + status: string + torrents: string | null + multi: number + link: string + episodes_count: number +} + +export interface Episode { + id: number + serie_id: number + season_id: number + name: string + number: number + cover: string | null + duration: string + description: string + episodes: string + path_jap: string | null + path_fr: string | null +} + +export interface Season { + id: number + serie_id: number + name: string + number: number + cover: string | null + episodes: Episode[] +} + +export interface ProductionDetail extends Production { + seasons: Season[] + episodes: Episode[] +} + +export interface Torrent { + name: string + url: string +} + +// Parse "Nom@url|Nom@url" → Torrent[] +export function parseTorrents(raw: string | null): Torrent[] { + if (!raw) return [] + return raw.split('|').flatMap(item => { + const parts = item.split('@') + if (parts.length !== 2) return [] + return [{ name: parts[0] as string, url: parts[1] as string }] + }) +} + +// Préfixe les images relatives +export function imageUrl(path: string | null): string | null { + if (!path) return null + const encoded = encodeURIComponent(`https://api.fankai.fr${path}`) + return `https://fankai.fr/_next/image?url=${encoded}&w=640&q=100` +} + +export async function fetchProductions(): Promise { + const res = await fetch(`${BASE}/productions`) + if (!res.ok) throw new Error('Erreur chargement catalogue') + const data = await res.json() + return data.pageProps.data +} + +export async function fetchProduction(id: number, name: string): Promise { + const res = await fetch(`${BASE}/productions/${id}?name=${encodeURIComponent(name)}`) + if (!res.ok) throw new Error('Erreur chargement production') + const data = await res.json() + return data.pageProps.data +} \ No newline at end of file diff --git a/src/assets/main.css b/src/assets/main.css new file mode 100644 index 0000000..a461c50 --- /dev/null +++ b/src/assets/main.css @@ -0,0 +1 @@ +@import "tailwindcss"; \ No newline at end of file diff --git a/src/components/EpisodeRow.vue b/src/components/EpisodeRow.vue new file mode 100644 index 0000000..ce3ed86 --- /dev/null +++ b/src/components/EpisodeRow.vue @@ -0,0 +1,52 @@ + + + \ No newline at end of file diff --git a/src/components/ToastContainer.vue b/src/components/ToastContainer.vue new file mode 100644 index 0000000..1a7efd5 --- /dev/null +++ b/src/components/ToastContainer.vue @@ -0,0 +1,36 @@ + + + + + \ No newline at end of file diff --git a/src/composables/useToast.ts b/src/composables/useToast.ts new file mode 100644 index 0000000..c73bfdb --- /dev/null +++ b/src/composables/useToast.ts @@ -0,0 +1,25 @@ +import { ref } from 'vue' + +interface Toast { + id: number + message: string + type: 'success' | 'error' | 'info' +} + +const toasts = ref([]) +let nextId = 0 + +export function useToast() { + function add(message: string, type: Toast['type'] = 'info', duration = 3000) { + const id = nextId++ + toasts.value.push({ id, message, type }) + setTimeout(() => remove(id), duration) + } + + function remove(id: number) { + const idx = toasts.value.findIndex(t => t.id === id) + if (idx !== -1) toasts.value.splice(idx, 1) + } + + return { toasts, add, remove } +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..29a8ccd --- /dev/null +++ b/src/main.ts @@ -0,0 +1,10 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import router from './router' +import App from '@/App.vue' +import '@/assets/main.css' + +const app = createApp(App) +app.use(createPinia()) +app.use(router) +app.mount('#app') \ No newline at end of file diff --git a/src/router/index.ts b/src/router/index.ts new file mode 100644 index 0000000..be14be5 --- /dev/null +++ b/src/router/index.ts @@ -0,0 +1,52 @@ +import { createRouter, createWebHistory } from 'vue-router' +import { useAuthStore } from '@/stores/auth' + +const router = createRouter({ + history: createWebHistory(), + routes: [ + { + path: '/auth', + component: () => import('@/views/AuthView.vue'), + meta: { public: true } + }, + { + path: '/', + name: 'catalog', + component: () => import('@/views/CatalogView.vue') + }, + { + path: '/series/:id', + component: () => import('@/views/SerieView.vue') + }, + { + path: '/downloads', + component: () => import('@/views/DownloadsView.vue') + }, + { + path: '/settings', + component: () => import('@/views/SettingsView.vue') + } + ], + scrollBehavior(to, _from, savedPosition) { + if (savedPosition) return savedPosition + return { top: 0 } + } +}) + +router.beforeEach(async (to) => { + const auth = useAuthStore() + + if (auth.loading) { + await auth.checkStatus() + } + + if (!to.meta.public && !auth.loggedIn) { + return '/auth' + } + + if (to.path === '/auth' && auth.loggedIn) { + return '/' + } +}) + +export default router \ No newline at end of file diff --git a/src/stores/auth.ts b/src/stores/auth.ts new file mode 100644 index 0000000..2609932 --- /dev/null +++ b/src/stores/auth.ts @@ -0,0 +1,57 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' + +export const useAuthStore = defineStore('auth', () => { + const loggedIn = ref(false) + const setup = ref(false) + const loading = ref(true) + + async function checkStatus() { + try { + const res = await fetch('/api/auth/status') + const data = await res.json() + setup.value = data.setup + loggedIn.value = data.loggedIn + } catch { + loggedIn.value = false + } finally { + loading.value = false + } + } + + async function login(username: string, password: string): Promise { + const res = await fetch('/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password }) + }) + const data = await res.json() + if (res.ok) { + loggedIn.value = true + return null + } + return data.error + } + + async function setupAccount(username: string, password: string): Promise { + const res = await fetch('/api/auth/setup', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password }) + }) + const data = await res.json() + if (res.ok) { + loggedIn.value = true + setup.value = true + return null + } + return data.error + } + + async function logout() { + await fetch('/api/auth/logout', { method: 'POST' }) + loggedIn.value = false + } + + return { loggedIn, setup, loading, checkStatus, login, setupAccount, logout } +}) \ No newline at end of file diff --git a/src/stores/catalog.ts b/src/stores/catalog.ts new file mode 100644 index 0000000..3327936 --- /dev/null +++ b/src/stores/catalog.ts @@ -0,0 +1,33 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { + fetchProductions, + fetchProduction, + type Production, + type ProductionDetail +} from '@/api/fankai' + +export const useCatalogStore = defineStore('catalog', () => { + const productions = ref([]) + const loading = ref(false) + const error = ref(null) + + async function loadProductions() { + if (productions.value.length > 0) return + loading.value = true + error.value = null + try { + productions.value = await fetchProductions() + } catch (err) { + error.value = err instanceof Error ? err.message : 'Erreur inconnue' + } finally { + loading.value = false + } + } + + async function loadProduction(id: number, name: string): Promise { + return await fetchProduction(id, name) + } + + return { productions, loading, error, loadProductions, loadProduction } +}) \ No newline at end of file diff --git a/src/stores/series.ts b/src/stores/series.ts new file mode 100644 index 0000000..4542664 --- /dev/null +++ b/src/stores/series.ts @@ -0,0 +1,114 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' + +export interface TorrentRef { + torrent_raw: string + torrent_url: string | null + magnet: string | null + type: string +} + +export interface Episode { + id: number + episode_number: number + display_episode: number | null + title: string + plot: string | null + aired: string | null + duration: number | null + thumb_image: string | null + has_thumbnail: boolean + season_id: number + available: boolean + torrent: TorrentRef | null // non-null seulement si type=episode +} + +export interface Season { + id: number + season_number: number + title: string + plot: string | null + poster_image: string | null + episode_count: number + premiered: string | null + episodes: Episode[] + torrent: TorrentRef | null +} + +export interface Serie { + id: number + title: string + sort_title: string + plot: string | null + poster_image: string | null + fanart_image: string | null + year: number | null + status: string | null + torrent_count: number + has_torrents: boolean +} + +export interface SerieDetail { + serie: Serie + seasons: Season[] + torrents_integrale: { raw: string; torrent_url: string | null; magnet: string | null }[] +} + +export const useSeriesStore = defineStore('series', () => { + const series = ref([]) + const currentSerie = ref(null) + const loadingSeries = ref(false) + const loadingDetail = ref(false) + const error = ref(null) + + async function fetchSeries() { + loadingSeries.value = true + error.value = null + try { + const res = await fetch('/api/series') + if (!res.ok) throw new Error(`Erreur ${res.status}`) + const data = await res.json() + series.value = data.series + } catch (err) { + error.value = err instanceof Error ? err.message : 'Erreur inconnue' + } finally { + loadingSeries.value = false + } + } + + async function fetchSerieDetail(id: number) { + loadingDetail.value = true + error.value = null + currentSerie.value = null + try { + const res = await fetch(`/api/series/${id}`) + if (!res.ok) throw new Error(`Erreur ${res.status}`) + currentSerie.value = await res.json() + } catch (err) { + error.value = err instanceof Error ? err.message : 'Erreur inconnue' + } finally { + loadingDetail.value = false + } + } + + async function download(torrent_url: string | null, magnet: string | null): Promise<{ success: boolean; error?: string }> { + try { + const res = await fetch('/api/download', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ torrent_url, magnet }) + }) + const data = await res.json() + if (res.ok) return { success: true } + return { success: false, error: data.error } + } catch (err) { + return { success: false, error: err instanceof Error ? err.message : 'Erreur inconnue' } + } + } + + return { + series, currentSerie, + loadingSeries, loadingDetail, error, + fetchSeries, fetchSerieDetail, download + } +}) \ No newline at end of file diff --git a/src/views/AuthView.vue b/src/views/AuthView.vue new file mode 100644 index 0000000..fb5f3f6 --- /dev/null +++ b/src/views/AuthView.vue @@ -0,0 +1,115 @@ + + + \ No newline at end of file diff --git a/src/views/CatalogView.vue b/src/views/CatalogView.vue new file mode 100644 index 0000000..c9e38c6 --- /dev/null +++ b/src/views/CatalogView.vue @@ -0,0 +1,190 @@ + + + \ No newline at end of file diff --git a/src/views/DownloadsView.vue b/src/views/DownloadsView.vue new file mode 100644 index 0000000..f739b4b --- /dev/null +++ b/src/views/DownloadsView.vue @@ -0,0 +1,235 @@ + + + \ No newline at end of file diff --git a/src/views/ProductionView.vue b/src/views/ProductionView.vue new file mode 100644 index 0000000..fc287e8 --- /dev/null +++ b/src/views/ProductionView.vue @@ -0,0 +1,135 @@ + + + \ No newline at end of file diff --git a/src/views/SerieView.vue b/src/views/SerieView.vue new file mode 100644 index 0000000..1408495 --- /dev/null +++ b/src/views/SerieView.vue @@ -0,0 +1,275 @@ + + + + + \ No newline at end of file diff --git a/src/views/SettingsView.vue b/src/views/SettingsView.vue new file mode 100644 index 0000000..8b1f9b6 --- /dev/null +++ b/src/views/SettingsView.vue @@ -0,0 +1,401 @@ + + + \ No newline at end of file diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..8d16e42 --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,16 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "types": ["vite/client"], + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3225d29 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + }, + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "jsx": "preserve", + "skipLibCheck": true + }, + "include": ["src", "server"] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/tsconfig.server.json b/tsconfig.server.json new file mode 100644 index 0000000..852286e --- /dev/null +++ b/tsconfig.server.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target" : "ES2022", + "module" : "NodeNext", + "moduleResolution": "NodeNext", + "outDir" : "./dist/server", + "rootDir" : "./server", + "strict" : true, + "esModuleInterop" : true, + "skipLibCheck" : true, + "sourceMap" : false + }, + "include": ["server/**/*"], + "exclude": ["node_modules", "dist"] +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..75bcd11 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import tailwindcss from '@tailwindcss/vite' +import { fileURLToPath, URL } from 'node:url' + +export default defineConfig({ + plugins: [vue(), tailwindcss()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } + }, + server: { + port: 3000, + proxy: { + '/api': 'http://localhost:3001' + } + } +}) \ No newline at end of file