From 54d6ee553a50f65e3e4df4e068b770b3fdcabf59 Mon Sep 17 00:00:00 2001 From: Michelle Date: Tue, 24 Feb 2026 18:14:16 +0100 Subject: [PATCH] Init v1.0.0 --- .dockerignore | 11 + .env.example | 20 + .gitea/workflows/docker.yaml | 47 + .gitignore | 5 + Dockerfile | 44 + index.html | 16 + package-lock.json | 4694 +++++++++++++++++++++++++++++ package.json | 40 + postcss.config.js | 6 + public/vite.svg | 5 + redlight.db-shm | Bin 0 -> 32768 bytes redlight.db-wal | Bin 0 -> 144232 bytes server/config/bbb.js | 114 + server/config/database.js | 230 ++ server/index.js | 48 + server/middleware/auth.js | 37 + server/routes/admin.js | 139 + server/routes/auth.js | 219 ++ server/routes/recordings.js | 120 + server/routes/rooms.js | 368 +++ src/App.jsx | 45 + src/components/Layout.jsx | 31 + src/components/Modal.jsx | 25 + src/components/Navbar.jsx | 118 + src/components/ProtectedRoute.jsx | 20 + src/components/RecordingList.jsx | 159 + src/components/RoomCard.jsx | 108 + src/components/Sidebar.jsx | 108 + src/components/ThemeSelector.jsx | 99 + src/contexts/AuthContext.jsx | 60 + src/contexts/LanguageContext.jsx | 46 + src/contexts/ThemeContext.jsx | 32 + src/i18n/de.json | 244 ++ src/i18n/en.json | 244 ++ src/i18n/index.js | 42 + src/index.css | 486 +++ src/main.jsx | 34 + src/pages/Admin.jsx | 368 +++ src/pages/Dashboard.jsx | 230 ++ src/pages/GuestJoin.jsx | 222 ++ src/pages/Home.jsx | 146 + src/pages/Login.jsx | 120 + src/pages/Register.jsx | 165 + src/pages/RoomDetail.jsx | 477 +++ src/pages/Settings.jsx | 406 +++ src/services/api.js | 36 + src/themes/index.js | 122 + tailwind.config.js | 39 + vite.config.js | 15 + 49 files changed, 10410 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitea/workflows/docker.yaml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 public/vite.svg create mode 100644 redlight.db-shm create mode 100644 redlight.db-wal create mode 100644 server/config/bbb.js create mode 100644 server/config/database.js create mode 100644 server/index.js create mode 100644 server/middleware/auth.js create mode 100644 server/routes/admin.js create mode 100644 server/routes/auth.js create mode 100644 server/routes/recordings.js create mode 100644 server/routes/rooms.js create mode 100644 src/App.jsx create mode 100644 src/components/Layout.jsx create mode 100644 src/components/Modal.jsx create mode 100644 src/components/Navbar.jsx create mode 100644 src/components/ProtectedRoute.jsx create mode 100644 src/components/RecordingList.jsx create mode 100644 src/components/RoomCard.jsx create mode 100644 src/components/Sidebar.jsx create mode 100644 src/components/ThemeSelector.jsx create mode 100644 src/contexts/AuthContext.jsx create mode 100644 src/contexts/LanguageContext.jsx create mode 100644 src/contexts/ThemeContext.jsx create mode 100644 src/i18n/de.json create mode 100644 src/i18n/en.json create mode 100644 src/i18n/index.js create mode 100644 src/index.css create mode 100644 src/main.jsx create mode 100644 src/pages/Admin.jsx create mode 100644 src/pages/Dashboard.jsx create mode 100644 src/pages/GuestJoin.jsx create mode 100644 src/pages/Home.jsx create mode 100644 src/pages/Login.jsx create mode 100644 src/pages/Register.jsx create mode 100644 src/pages/RoomDetail.jsx create mode 100644 src/pages/Settings.jsx create mode 100644 src/services/api.js create mode 100644 src/themes/index.js create mode 100644 tailwind.config.js create mode 100644 vite.config.js diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ea7f1b4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +node_modules/ +dist/ +.env +.env.* +!.env.example +redlight.db +uploads/ +.git/ +.gitignore +.vscode/ +*.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8ef9057 --- /dev/null +++ b/.env.example @@ -0,0 +1,20 @@ +# BigBlueButton API Configuration +BBB_URL=https://your-bbb-server.com/bigbluebutton/api/ +BBB_SECRET=your-bbb-shared-secret + +# Server Configuration +PORT=3001 +JWT_SECRET=your-super-secret-jwt-key-change-this + +# Database Configuration +# Leave DATABASE_URL empty or unset to use SQLite (default: redlight.db) +# Set a PostgreSQL connection string to use PostgreSQL instead: +# DATABASE_URL=postgres://user:password@localhost:5432/redlight +DATABASE_URL= + +# SQLite file path (only used when DATABASE_URL is not set) +# SQLITE_PATH=./redlight.db + +# Default Admin Account (created on first run) +ADMIN_EMAIL=admin@example.com +ADMIN_PASSWORD=admin123 diff --git a/.gitea/workflows/docker.yaml b/.gitea/workflows/docker.yaml new file mode 100644 index 0000000..30b5636 --- /dev/null +++ b/.gitea/workflows/docker.yaml @@ -0,0 +1,47 @@ +name: Build & Push Docker Image + +on: + push: + branches: + - main + release: + types: [published] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Gitea Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ github.server_url }} + username: ${{ github.repository_owner }} + password: ${{ secrets.REGISTRY_TOKEN }} + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ github.server_url }}/${{ github.repository }} + tags: | + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,prefix= + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c35c035 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +redlight.db +uploads/ +.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d8dcb82 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,44 @@ +# ── Stage 1: Build frontend ────────────────────────────────────────────────── +FROM node:20-alpine AS builder + +WORKDIR /app + +COPY package.json package-lock.json* ./ +RUN npm ci + +COPY . . +RUN npm run build + +# ── Stage 2: Production image ─────────────────────────────────────────────── +FROM node:20-alpine + +# better-sqlite3 needs build tools for native compilation +RUN apk add --no-cache python3 make g++ + +WORKDIR /app + +COPY package.json package-lock.json* ./ +RUN npm ci --omit=dev && npm cache clean --force + +# Remove build tools after install to keep image smaller +RUN apk del python3 make g++ + +# Copy server code +COPY server/ ./server/ + +# Copy built frontend from builder stage +COPY --from=builder /app/dist ./dist + +# Create uploads directory +RUN mkdir -p uploads/avatars + +ENV NODE_ENV=production +ENV PORT=3001 +ENV SQLITE_PATH=/app/data/redlight.db + +EXPOSE 3001 + +# Data volumes for persistent storage +VOLUME ["/app/uploads", "/app/data"] + +CMD ["node", "server/index.js"] diff --git a/index.html b/index.html new file mode 100644 index 0000000..129d32a --- /dev/null +++ b/index.html @@ -0,0 +1,16 @@ + + + + + + + + + + Redlight - BigBlueButton Frontend + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..bcc554a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4694 @@ +{ + "name": "redlight", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "redlight", + "version": "1.0.0", + "dependencies": { + "axios": "^1.7.0", + "bcryptjs": "^2.4.3", + "better-sqlite3": "^11.0.0", + "concurrently": "^9.0.0", + "cors": "^2.8.5", + "dotenv": "^16.4.0", + "express": "^4.21.0", + "jsonwebtoken": "^9.0.0", + "lucide-react": "^0.460.0", + "pg": "^8.18.0", + "react": "^18.3.0", + "react-dom": "^18.3.0", + "react-hot-toast": "^2.4.0", + "react-router-dom": "^6.28.0", + "xml2js": "^0.6.0" + }, + "devDependencies": { + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.0", + "autoprefixer": "^10.4.0", + "postcss": "^8.4.0", + "tailwindcss": "^3.4.0", + "vite": "^5.4.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.24", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz", + "integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001766", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001774", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", + "integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "license": "MIT", + "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" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/goober": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz", + "integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "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.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.460.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.460.0.tgz", + "integrity": "sha512-BVtq/DykVeIvRTJvRAgCsOwaGL8Un3Bxh8MbDxMhEWlZay3T4IpEKDEpwt5KZ0KJMHzgm6jrltxlT5eXOWXDHg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz", + "integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.11.0", + "pg-pool": "^3.11.0", + "pg-protocol": "^1.11.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.11.0.tgz", + "integrity": "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.11.0.tgz", + "integrity": "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.11.0.tgz", + "integrity": "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-hot-toast": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz", + "integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.3", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "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.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", + "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..559a84f --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "redlight", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "concurrently -n client,server -c blue,green \"vite\" \"node --watch server/index.js\"", + "dev:client": "vite", + "dev:server": "node --watch server/index.js", + "build": "vite build", + "preview": "vite preview", + "start": "NODE_ENV=production node server/index.js" + }, + "dependencies": { + "axios": "^1.7.0", + "bcryptjs": "^2.4.3", + "better-sqlite3": "^11.0.0", + "concurrently": "^9.0.0", + "cors": "^2.8.5", + "dotenv": "^16.4.0", + "express": "^4.21.0", + "jsonwebtoken": "^9.0.0", + "lucide-react": "^0.460.0", + "pg": "^8.18.0", + "react": "^18.3.0", + "react-dom": "^18.3.0", + "react-hot-toast": "^2.4.0", + "react-router-dom": "^6.28.0", + "xml2js": "^0.6.0" + }, + "devDependencies": { + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.0", + "autoprefixer": "^10.4.0", + "postcss": "^8.4.0", + "tailwindcss": "^3.4.0", + "vite": "^5.4.0" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/public/vite.svg b/public/vite.svg new file mode 100644 index 0000000..a50be2b --- /dev/null +++ b/public/vite.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/redlight.db-shm b/redlight.db-shm new file mode 100644 index 0000000000000000000000000000000000000000..6ec64e46d82525970100d0e1098bebbc1f255d28 GIT binary patch literal 32768 zcmeI)NlFAU6adiQgFQHPJGb++6b~SX``&v4QSczW5qUMT}3=Vwm#bdUkO&efpf;KaZwokC*Q^cfH$}x7W!p_t&*^ z-LFP8zvp@SceGP>Qg*BLTpD}TykCujYRq?R?r*Mt&U3xGR<2Xm$mi=C`Aoh;PCarq~BBHKkwe1?>={D*U#_wd!Yl4ui3W!@j9Droz0jQq{HrL+vB&N{6_Za z-#V0sXpz0VeE!IxiFY=pt{)$ZNxH~QDymF$E=ZTItF!IoI9j)k{xulOlqO^O>gVRX zrS9u&9S?17aerc~Z+XGia@zeay~6|n2tWV=5P$##AOHafKmY<>4wbrv*H z)!0p4#qz9(Yi;J(BxSg0JjoC632rnI9S$cZxFLRm3#XDJ(Kuahn2#shOLdJ`;*$LC zBo`l{f2ml^oFmFa$`)s(;?c1bUsEGbG;LN1CXR#A&vY1P}@6YS}k^jlTasz`J(BhYWyee~!gqr*IBR*s|+2?{3|h2zQa@Tj=~ zg?#4x7h>!_U!B8qhsSP{av5>1bnr<*SJ-ob9ZkW{?&|5O<}96V#`k=yyG z1q=mD5P$##AOHafKmY;|fB*y_0D-kEV8j44nDGUB4&DEcTb};OG4vN$%ZD8M1_1~_ z00Izz00bZa0SG_<0uZnSD)I{qum9J~rXSt++&S_KIIN`?&o@vud(nqheu2j3qow=; z%|~127p;pz2tWV=5P$##AOHafoS(pgzk!8I9}2T@X;o2Vjoma>giEuc6@^O|Xlyl$ zmR=$xdg*X#Bbrwzi_ESLx1JTVX+;(VS=6-Jc;{VGHoH%eWiiJ|GB+%Wx|EyRWW_DZ zWKPHvRhQCIp5%1RivKo)m%GehWs;_$=o$@%&J;tc&2qUwW7UUdf3RH=)>EpOfk^YecA03FZ0Q=U`G4^o@zMrST;t_tFV^Q3#QpU%P#Az&fjHf~W zk??pV+{c$V4;jh{WX!Grt)tNf^9^C18M z2tWV=5P$##AOHafKmY>gCr}Yb5Rdh~urOYK<2m98n#xvfpiVG~UPW=4$E~J+oOo zs{=RU2oOhL45f!S0*q6LaSAVVoI>L}ev{c>;P-z!^V-J)|IDDjz}4|_jJ<>a1Rwwb z2tWV=5P$##AOL}@UZA4Cz^|T+D1QpcgXick;4D*P;~A7aXX`6_zSu)6?_g82ucE&I zeToSJ5P$##AOHafK;TLi=x=g(ydL|W>fiaL4Eycgxzum`H0R#g-}h-AHuM+Z;;C57 zOkjt+gUCB5&T0Fz5={U}3OddINc+*0I?*8S;B+zFC-M&R$>=ao6Ik|#Q?Vo$NnOOe zgT{A!r`caX->}ZR{n+vQ&|lz6{xHV2K>z{}fB*y_009U<00Izz00b_YKt+FnZ@quy zwhw-I>$&<1xXNDL7|&qE`30P=<5m3yXaOb&KmY;|fB*y_0D+4s5OX>_Z63R5{ld@2 zP7_3#NLlvKRr4EvSqK$4@O?ea`GL^?u)V-xDw{N7!Gy;^TP zP`LY^7)|UlI}sSl<~njBTUkcT5Ov??o?uVUq(9IV=-Jj4*cRyK{5?B@!5#kYMT+Qd z8RHCE?Eo@TZ;V!PTSn4SO1P<4Lpd=(J%*N6;MEH009U<00Izz00bZa0SH`vfyH4&!ar7VIbo#7nP{Lbx_yda+Q5c7TW3~%C8ogaVv2I_u7)L<+3!t~a zYCm_d{Sbfv1Rwwb2tWV=5P$##AOHa-u%UjNtGYdb&S)}bn(YF6pZWDm-~RhwKP$Ei zoUysjutjkL0SG_<0uX=z1Rwwb2tWV=5P-lM6R59u894`?^v`9s3p9GV{`<<$DV009U<00Izz00bZa0SH|60!#E4uu~YTU10Vp-{IfA7yZTfSWMDIZc}Z^8Eta-(UXqceoY&_ww<1e&Qw1_ry^@URHEZb8qPYv!Yh;MJ&#FLXh)z@? ztz;FoxRqAD%{{@Mo=Lw|HLZ$77c&C=|5P8b29=j;zGUA++nG@In7T6(S4_&3hAC33%ySb_w+(^85 zMzpd8eEm%hkJn@0W37!Puo4PVrkKg9W?j~NV@>QyE3NEHI2@i%kDVT%(pHt~(E}=! z*44Dsx{7tm)>ka>E%+PQ34SQdPH?tHcGFl|Q}G06MJqbN>4qs8Ft zt;;;UNIFelfS$gY;zeMV%LQE&l$@|vk#aRl`^{RaxW6E&qCoO_Rhc2#nwn={CpWL; zL?KObf<~_weVLlsEPGW!rrV?vonBVv`%TT(t76)Chmec=UsKQ8&P#vgnIioo2|hXy zXK#tErDN>l5_~^TFYgFHUb=MX`EH!8eSD0kZ?lo`cqH7%v#ZzVYjk+-@YrqaI?(oK z>9;U}6m)tV=yf3!FA6`qbBxy03%Sic!A|Z{RgE4JF-H$-aY^%OL&XVSHM7+?P1|Wz zpZgR0Z`db76Fw2>rE3mgpCTWsFWgN>f`*W3^c2E#_{ zw4~{*(Tg=z?-b{VoX?6Kv?*{)fNb#xwhT^^xpe0sk(2^Sj)u0&GvN^F2n~)+sAKbc zcLnxpz41Wd?t5aiiD-5rFqF-8JD|}b5nGejCKJlzrf8$4}Em(?Tw!n+Xeb8gn540SG_<0uX=z z1Rwwb2tWV=5I7jJngnk8zcdM`iY9K$OF8jE+64lOw+k432ds91sfLzc&P_h>VKKkJ zDVzHgjxGcs009U<00Izz00bZa0SG_<0&75EV|}NqrrQGDFC$lg6-V&e%tx=Q_a5Jb zc7Zi;#IXwyfB*y_009U<00Izz00bZafwKZjwF^|_9elR^;hWyFz5f0(c?XRggXMV# zjd2byH1A*o%RAV7)aGnlSIRrsxDI&-&mJs1f&c`r9D#+gVhXEA?MPw8lFpeqdCsPm zLJF&kl02@Rn<|K!E)-LO)g;rhygz@6E94zS-odJ@f-L#en#nuZa=PvnBaIsJ4wf>7 z;!XP!W&A|mL1dyWCELC7GSRLu?_fo{K<`5G(f>RXz4HRv1&sWKD{mKQ__Wk6(C{hR z1+JX$LD&)qT%`gFlcgqrHZ%dC3E)!Zk3VM<0B0rTu1y(^b^(k>P&J4F#v@>f+O0(J ZXcxeE1jZmL7>@ws5nRP1)U2dk;D2_#%4`4t literal 0 HcmV?d00001 diff --git a/server/config/bbb.js b/server/config/bbb.js new file mode 100644 index 0000000..3784f5b --- /dev/null +++ b/server/config/bbb.js @@ -0,0 +1,114 @@ +import crypto from 'crypto'; +import xml2js from 'xml2js'; + +const BBB_URL = process.env.BBB_URL || 'https://your-bbb-server.com/bigbluebutton/api/'; +const BBB_SECRET = process.env.BBB_SECRET || ''; + +function getChecksum(apiCall, params) { + const queryString = new URLSearchParams(params).toString(); + const raw = apiCall + queryString + BBB_SECRET; + return crypto.createHash('sha256').update(raw).digest('hex'); +} + +function buildUrl(apiCall, params = {}) { + const checksum = getChecksum(apiCall, params); + const queryString = new URLSearchParams({ ...params, checksum }).toString(); + return `${BBB_URL}${apiCall}?${queryString}`; +} + +async function apiCall(apiCallName, params = {}) { + const url = buildUrl(apiCallName, params); + try { + const response = await fetch(url); + const xml = await response.text(); + const result = await xml2js.parseStringPromise(xml, { + explicitArray: false, + trim: true, + }); + return result.response; + } catch (error) { + console.error(`BBB API error (${apiCallName}):`, error.message); + throw error; + } +} + +// Generate deterministic passwords from room UID +function getRoomPasswords(uid) { + const modPw = crypto.createHash('sha256').update(uid + '_mod_' + BBB_SECRET).digest('hex').substring(0, 16); + const attPw = crypto.createHash('sha256').update(uid + '_att_' + BBB_SECRET).digest('hex').substring(0, 16); + return { moderatorPW: modPw, attendeePW: attPw }; +} + +export async function createMeeting(room) { + const { moderatorPW, attendeePW } = getRoomPasswords(room.uid); + const params = { + meetingID: room.uid, + name: room.name, + attendeePW, + moderatorPW, + welcome: room.welcome_message || 'Willkommen!', + record: room.record_meeting ? 'true' : 'false', + autoStartRecording: 'false', + allowStartStopRecording: 'true', + muteOnStart: room.mute_on_join ? 'true' : 'false', + 'meta_bbb-origin': 'Redlight', + 'meta_bbb-origin-server-name': 'Redlight', + }; + if (room.max_participants > 0) { + params.maxParticipants = room.max_participants.toString(); + } + if (room.access_code) { + params.lockSettingsLockOnJoin = 'true'; + } + return apiCall('create', params); +} + +export async function joinMeeting(uid, name, isModerator = false) { + const { moderatorPW, attendeePW } = getRoomPasswords(uid); + const params = { + meetingID: uid, + fullName: name, + password: isModerator ? moderatorPW : attendeePW, + redirect: 'true', + }; + return buildUrl('join', params); +} + +export async function endMeeting(uid) { + const { moderatorPW } = getRoomPasswords(uid); + return apiCall('end', { meetingID: uid, password: moderatorPW }); +} + +export async function getMeetingInfo(uid) { + return apiCall('getMeetingInfo', { meetingID: uid }); +} + +export async function isMeetingRunning(uid) { + const result = await apiCall('isMeetingRunning', { meetingID: uid }); + return result.running === 'true'; +} + +export async function getMeetings() { + return apiCall('getMeetings', {}); +} + +export async function getRecordings(meetingID) { + const params = meetingID ? { meetingID } : {}; + const result = await apiCall('getRecordings', params); + if (result.returncode !== 'SUCCESS' || !result.recordings) { + return []; + } + const recordings = result.recordings.recording; + if (!recordings) return []; + return Array.isArray(recordings) ? recordings : [recordings]; +} + +export async function deleteRecording(recordID) { + return apiCall('deleteRecordings', { recordID }); +} + +export async function publishRecording(recordID, publish) { + return apiCall('publishRecordings', { recordID, publish: publish ? 'true' : 'false' }); +} + +export { getRoomPasswords }; diff --git a/server/config/database.js b/server/config/database.js new file mode 100644 index 0000000..bdac28a --- /dev/null +++ b/server/config/database.js @@ -0,0 +1,230 @@ +import bcrypt from 'bcryptjs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const DATABASE_URL = process.env.DATABASE_URL; +const isPostgres = !!(DATABASE_URL && DATABASE_URL.startsWith('postgres')); + +let db; + +// Convert ? placeholders to $1, $2, ... for PostgreSQL +function convertPlaceholders(sql) { + let index = 0; + return sql.replace(/\?/g, () => `$${++index}`); +} + +// ── SQLite Adapter ────────────────────────────────────────────────────────── +class SqliteAdapter { + async init() { + const { default: Database } = await import('better-sqlite3'); + const dbPath = process.env.SQLITE_PATH || path.join(__dirname, '..', '..', 'redlight.db'); + this.db = Database(dbPath); + this.db.pragma('journal_mode = WAL'); + this.db.pragma('foreign_keys = ON'); + } + + async get(sql, params = []) { + return this.db.prepare(sql).get(...params); + } + + async all(sql, params = []) { + return this.db.prepare(sql).all(...params); + } + + async run(sql, params = []) { + const result = this.db.prepare(sql).run(...params); + return { lastInsertRowid: Number(result.lastInsertRowid), changes: result.changes }; + } + + async exec(sql) { + this.db.exec(sql); + } + + async columnExists(table, column) { + const columns = this.db.pragma(`table_info(${table})`); + return !!columns.find(c => c.name === column); + } + + close() { + this.db.close(); + } +} + +// ── PostgreSQL Adapter ────────────────────────────────────────────────────── +class PostgresAdapter { + async init() { + const pg = await import('pg'); + // Parse int8 (bigint / COUNT) as JS number instead of string + pg.default.types.setTypeParser(20, val => parseInt(val, 10)); + this.pool = new pg.default.Pool({ connectionString: DATABASE_URL }); + } + + async get(sql, params = []) { + const result = await this.pool.query(convertPlaceholders(sql), params); + return result.rows[0] || undefined; + } + + async all(sql, params = []) { + const result = await this.pool.query(convertPlaceholders(sql), params); + return result.rows; + } + + async run(sql, params = []) { + let pgSql = convertPlaceholders(sql); + const isInsert = /^\s*INSERT/i.test(pgSql); + if (isInsert && !/RETURNING/i.test(pgSql)) { + pgSql += ' RETURNING id'; + } + const result = await this.pool.query(pgSql, params); + return { + lastInsertRowid: isInsert ? result.rows[0]?.id : undefined, + changes: result.rowCount, + }; + } + + async exec(sql) { + await this.pool.query(sql); + } + + async columnExists(table, column) { + const result = await this.pool.query( + 'SELECT column_name FROM information_schema.columns WHERE table_name = $1 AND column_name = $2', + [table, column] + ); + return result.rows.length > 0; + } + + close() { + this.pool?.end(); + } +} + +// ── Public API ────────────────────────────────────────────────────────────── +export function getDb() { + if (!db) { + throw new Error('Database not initialised – call initDatabase() first'); + } + return db; +} + +export async function initDatabase() { + // Create the right adapter + if (isPostgres) { + console.log('📦 Using PostgreSQL database'); + db = new PostgresAdapter(); + } else { + console.log('📦 Using SQLite database'); + db = new SqliteAdapter(); + } + await db.init(); + + // ── Schema creation ───────────────────────────────────────────────────── + if (isPostgres) { + await db.exec(` + CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + email TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + role TEXT DEFAULT 'user' CHECK(role IN ('user', 'admin')), + language TEXT DEFAULT 'de', + theme TEXT DEFAULT 'dark', + avatar_color TEXT DEFAULT '#6366f1', + avatar_image TEXT DEFAULT NULL, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() + ); + + CREATE TABLE IF NOT EXISTS rooms ( + id SERIAL PRIMARY KEY, + uid TEXT UNIQUE NOT NULL, + name TEXT NOT NULL, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + welcome_message TEXT DEFAULT 'Willkommen im Meeting!', + max_participants INTEGER DEFAULT 0, + access_code TEXT, + mute_on_join INTEGER DEFAULT 1, + require_approval INTEGER DEFAULT 0, + anyone_can_start INTEGER DEFAULT 0, + all_join_moderator INTEGER DEFAULT 0, + record_meeting INTEGER DEFAULT 1, + guest_access INTEGER DEFAULT 0, + moderator_code TEXT, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() + ); + + CREATE INDEX IF NOT EXISTS idx_rooms_user_id ON rooms(user_id); + CREATE INDEX IF NOT EXISTS idx_rooms_uid ON rooms(uid); + CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); + `); + } else { + await db.exec(` + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + email TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + role TEXT DEFAULT 'user' CHECK(role IN ('user', 'admin')), + language TEXT DEFAULT 'de', + theme TEXT DEFAULT 'dark', + avatar_color TEXT DEFAULT '#6366f1', + avatar_image TEXT DEFAULT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + + CREATE TABLE IF NOT EXISTS rooms ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + uid TEXT UNIQUE NOT NULL, + name TEXT NOT NULL, + user_id INTEGER NOT NULL, + welcome_message TEXT DEFAULT 'Willkommen im Meeting!', + max_participants INTEGER DEFAULT 0, + access_code TEXT, + mute_on_join INTEGER DEFAULT 1, + require_approval INTEGER DEFAULT 0, + anyone_can_start INTEGER DEFAULT 0, + all_join_moderator INTEGER DEFAULT 0, + record_meeting INTEGER DEFAULT 1, + guest_access INTEGER DEFAULT 0, + moderator_code TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + ); + + CREATE INDEX IF NOT EXISTS idx_rooms_user_id ON rooms(user_id); + CREATE INDEX IF NOT EXISTS idx_rooms_uid ON rooms(uid); + CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); + `); + } + + // ── Migrations ────────────────────────────────────────────────────────── + if (!(await db.columnExists('users', 'avatar_image'))) { + await db.exec('ALTER TABLE users ADD COLUMN avatar_image TEXT DEFAULT NULL'); + } + if (!(await db.columnExists('rooms', 'guest_access'))) { + await db.exec('ALTER TABLE rooms ADD COLUMN guest_access INTEGER DEFAULT 0'); + } + if (!(await db.columnExists('rooms', 'moderator_code'))) { + await db.exec('ALTER TABLE rooms ADD COLUMN moderator_code TEXT'); + } + + // ── Default admin ─────────────────────────────────────────────────────── + const adminEmail = process.env.ADMIN_EMAIL || 'admin@example.com'; + const adminPassword = process.env.ADMIN_PASSWORD || 'admin123'; + + const existingAdmin = await db.get('SELECT id FROM users WHERE email = ?', [adminEmail]); + if (!existingAdmin) { + const hash = bcrypt.hashSync(adminPassword, 12); + await db.run( + 'INSERT INTO users (name, email, password_hash, role) VALUES (?, ?, ?, ?)', + ['Administrator', adminEmail, hash, 'admin'] + ); + console.log(`✅ Default admin created: ${adminEmail}`); + } +} diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000..6e4ef89 --- /dev/null +++ b/server/index.js @@ -0,0 +1,48 @@ +import 'dotenv/config'; +import express from 'express'; +import cors from 'cors'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { initDatabase } from './config/database.js'; +import authRoutes from './routes/auth.js'; +import roomRoutes from './routes/rooms.js'; +import recordingRoutes from './routes/recordings.js'; +import adminRoutes from './routes/admin.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const app = express(); +const PORT = process.env.PORT || 3001; + +// Middleware +app.use(cors()); +app.use(express.json()); + +// Initialize database & start server +async function start() { + await initDatabase(); + + // API Routes + app.use('/api/auth', authRoutes); + app.use('/api/rooms', roomRoutes); + app.use('/api/recordings', recordingRoutes); + app.use('/api/admin', adminRoutes); + + // Serve static files in production + if (process.env.NODE_ENV === 'production') { + app.use(express.static(path.join(__dirname, '..', 'dist'))); + app.get('*', (req, res) => { + res.sendFile(path.join(__dirname, '..', 'dist', 'index.html')); + }); + } + + app.listen(PORT, () => { + console.log(`🔴 Redlight server running on http://localhost:${PORT}`); + }); +} + +start().catch(err => { + console.error('❌ Failed to start server:', err); + process.exit(1); +}); diff --git a/server/middleware/auth.js b/server/middleware/auth.js new file mode 100644 index 0000000..c749395 --- /dev/null +++ b/server/middleware/auth.js @@ -0,0 +1,37 @@ +import jwt from 'jsonwebtoken'; +import { getDb } from '../config/database.js'; + +const JWT_SECRET = process.env.JWT_SECRET || 'fallback-secret-change-me'; + +export async function authenticateToken(req, res, next) { + const authHeader = req.headers.authorization; + const token = authHeader && authHeader.split(' ')[1]; + + if (!token) { + return res.status(401).json({ error: 'Authentifizierung erforderlich' }); + } + + try { + const decoded = jwt.verify(token, JWT_SECRET); + const db = getDb(); + const user = await db.get('SELECT id, name, email, role, theme, language, avatar_color, avatar_image FROM users WHERE id = ?', [decoded.userId]); + if (!user) { + return res.status(401).json({ error: 'Benutzer nicht gefunden' }); + } + req.user = user; + next(); + } catch (err) { + return res.status(403).json({ error: 'Ungültiges Token' }); + } +} + +export function requireAdmin(req, res, next) { + if (req.user.role !== 'admin') { + return res.status(403).json({ error: 'Administratorrechte erforderlich' }); + } + next(); +} + +export function generateToken(userId) { + return jwt.sign({ userId }, JWT_SECRET, { expiresIn: '7d' }); +} diff --git a/server/routes/admin.js b/server/routes/admin.js new file mode 100644 index 0000000..c337708 --- /dev/null +++ b/server/routes/admin.js @@ -0,0 +1,139 @@ +import { Router } from 'express'; +import bcrypt from 'bcryptjs'; +import { getDb } from '../config/database.js'; +import { authenticateToken, requireAdmin } from '../middleware/auth.js'; + +const router = Router(); + +// POST /api/admin/users - Create user (admin) +router.post('/users', authenticateToken, requireAdmin, async (req, res) => { + try { + const { name, email, password, role } = req.body; + + if (!name || !email || !password) { + return res.status(400).json({ error: 'Alle Felder sind erforderlich' }); + } + + if (password.length < 6) { + return res.status(400).json({ error: 'Passwort muss mindestens 6 Zeichen lang sein' }); + } + + const validRole = ['user', 'admin'].includes(role) ? role : 'user'; + const db = getDb(); + + const existing = await db.get('SELECT id FROM users WHERE email = ?', [email.toLowerCase()]); + if (existing) { + return res.status(409).json({ error: 'E-Mail wird bereits verwendet' }); + } + + const hash = bcrypt.hashSync(password, 12); + const result = await db.run( + 'INSERT INTO users (name, email, password_hash, role) VALUES (?, ?, ?, ?)', + [name, email.toLowerCase(), hash, validRole] + ); + + const user = await db.get('SELECT id, name, email, role, created_at FROM users WHERE id = ?', [result.lastInsertRowid]); + res.status(201).json({ user }); + } catch (err) { + console.error('Create user error:', err); + res.status(500).json({ error: 'Benutzer konnte nicht erstellt werden' }); + } +}); + +// GET /api/admin/users - List all users +router.get('/users', authenticateToken, requireAdmin, async (req, res) => { + try { + const db = getDb(); + const users = await db.all(` + SELECT id, name, email, role, language, theme, avatar_color, avatar_image, created_at, + (SELECT COUNT(*) FROM rooms WHERE rooms.user_id = users.id) as room_count + FROM users + ORDER BY created_at DESC + `); + + res.json({ users }); + } catch (err) { + console.error('List users error:', err); + res.status(500).json({ error: 'Benutzer konnten nicht geladen werden' }); + } +}); + +// PUT /api/admin/users/:id/role - Update user role +router.put('/users/:id/role', authenticateToken, requireAdmin, async (req, res) => { + try { + const { role } = req.body; + if (!['user', 'admin'].includes(role)) { + return res.status(400).json({ error: 'Ungültige Rolle' }); + } + + const db = getDb(); + + // Prevent demoting last admin + if (role === 'user') { + const adminCount = await db.get('SELECT COUNT(*) as count FROM users WHERE role = ?', ['admin']); + const currentUser = await db.get('SELECT role FROM users WHERE id = ?', [req.params.id]); + if (currentUser?.role === 'admin' && adminCount.count <= 1) { + return res.status(400).json({ error: 'Der letzte Admin kann nicht herabgestuft werden' }); + } + } + + await db.run('UPDATE users SET role = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [role, req.params.id]); + const updated = await db.get('SELECT id, name, email, role, created_at FROM users WHERE id = ?', [req.params.id]); + + res.json({ user: updated }); + } catch (err) { + console.error('Update role error:', err); + res.status(500).json({ error: 'Rolle konnte nicht aktualisiert werden' }); + } +}); + +// DELETE /api/admin/users/:id - Delete user +router.delete('/users/:id', authenticateToken, requireAdmin, async (req, res) => { + try { + const db = getDb(); + + if (parseInt(req.params.id) === req.user.id) { + return res.status(400).json({ error: 'Sie können sich nicht selbst löschen' }); + } + + const user = await db.get('SELECT id, role FROM users WHERE id = ?', [req.params.id]); + if (!user) { + return res.status(404).json({ error: 'Benutzer nicht gefunden' }); + } + + // Check if it's the last admin + if (user.role === 'admin') { + const adminCount = await db.get('SELECT COUNT(*) as count FROM users WHERE role = ?', ['admin']); + if (adminCount.count <= 1) { + return res.status(400).json({ error: 'Der letzte Admin kann nicht gelöscht werden' }); + } + } + + await db.run('DELETE FROM users WHERE id = ?', [req.params.id]); + res.json({ message: 'Benutzer gelöscht' }); + } catch (err) { + console.error('Delete user error:', err); + res.status(500).json({ error: 'Benutzer konnte nicht gelöscht werden' }); + } +}); + +// PUT /api/admin/users/:id/password - Reset user password (admin) +router.put('/users/:id/password', authenticateToken, requireAdmin, async (req, res) => { + try { + const { newPassword } = req.body; + if (!newPassword || newPassword.length < 6) { + return res.status(400).json({ error: 'Passwort muss mindestens 6 Zeichen lang sein' }); + } + + const db = getDb(); + const hash = bcrypt.hashSync(newPassword, 12); + await db.run('UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [hash, req.params.id]); + + res.json({ message: 'Passwort zurückgesetzt' }); + } catch (err) { + console.error('Reset password error:', err); + res.status(500).json({ error: 'Passwort konnte nicht zurückgesetzt werden' }); + } +}); + +export default router; diff --git a/server/routes/auth.js b/server/routes/auth.js new file mode 100644 index 0000000..25ee5ce --- /dev/null +++ b/server/routes/auth.js @@ -0,0 +1,219 @@ +import { Router } from 'express'; +import bcrypt from 'bcryptjs'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { getDb } from '../config/database.js'; +import { authenticateToken, generateToken } from '../middleware/auth.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const uploadsDir = path.join(__dirname, '..', '..', 'uploads', 'avatars'); + +// Ensure uploads directory exists +if (!fs.existsSync(uploadsDir)) { + fs.mkdirSync(uploadsDir, { recursive: true }); +} + +const router = Router(); + +// POST /api/auth/register +router.post('/register', async (req, res) => { + try { + const { name, email, password } = req.body; + + if (!name || !email || !password) { + return res.status(400).json({ error: 'Alle Felder sind erforderlich' }); + } + + if (password.length < 6) { + return res.status(400).json({ error: 'Passwort muss mindestens 6 Zeichen lang sein' }); + } + + const db = getDb(); + const existing = await db.get('SELECT id FROM users WHERE email = ?', [email]); + if (existing) { + return res.status(409).json({ error: 'E-Mail wird bereits verwendet' }); + } + + const hash = bcrypt.hashSync(password, 12); + const result = await db.run( + 'INSERT INTO users (name, email, password_hash) VALUES (?, ?, ?)', + [name, email.toLowerCase(), hash] + ); + + const token = generateToken(result.lastInsertRowid); + const user = await db.get('SELECT id, name, email, role, theme, language, avatar_color, avatar_image FROM users WHERE id = ?', [result.lastInsertRowid]); + + res.status(201).json({ token, user }); + } catch (err) { + console.error('Register error:', err); + res.status(500).json({ error: 'Registrierung fehlgeschlagen' }); + } +}); + +// POST /api/auth/login +router.post('/login', async (req, res) => { + try { + const { email, password } = req.body; + + if (!email || !password) { + return res.status(400).json({ error: 'E-Mail und Passwort sind erforderlich' }); + } + + const db = getDb(); + const user = await db.get('SELECT * FROM users WHERE email = ?', [email.toLowerCase()]); + + if (!user || !bcrypt.compareSync(password, user.password_hash)) { + return res.status(401).json({ error: 'Ungültige Anmeldedaten' }); + } + + const token = generateToken(user.id); + const { password_hash, ...safeUser } = user; + + res.json({ token, user: safeUser }); + } catch (err) { + console.error('Login error:', err); + res.status(500).json({ error: 'Anmeldung fehlgeschlagen' }); + } +}); + +// GET /api/auth/me +router.get('/me', authenticateToken, (req, res) => { + res.json({ user: req.user }); +}); + +// PUT /api/auth/profile +router.put('/profile', authenticateToken, async (req, res) => { + try { + const { name, email, theme, language, avatar_color } = req.body; + const db = getDb(); + + if (email && email !== req.user.email) { + const existing = await db.get('SELECT id FROM users WHERE email = ? AND id != ?', [email.toLowerCase(), req.user.id]); + if (existing) { + return res.status(409).json({ error: 'E-Mail wird bereits verwendet' }); + } + } + + await db.run(` + UPDATE users SET + name = COALESCE(?, name), + email = COALESCE(?, email), + theme = COALESCE(?, theme), + language = COALESCE(?, language), + avatar_color = COALESCE(?, avatar_color), + updated_at = CURRENT_TIMESTAMP + WHERE id = ? + `, [name, email?.toLowerCase(), theme, language, avatar_color, req.user.id]); + + const updated = await db.get('SELECT id, name, email, role, theme, language, avatar_color, avatar_image FROM users WHERE id = ?', [req.user.id]); + res.json({ user: updated }); + } catch (err) { + console.error('Profile update error:', err); + res.status(500).json({ error: 'Profil konnte nicht aktualisiert werden' }); + } +}); + +// PUT /api/auth/password +router.put('/password', authenticateToken, async (req, res) => { + try { + const { currentPassword, newPassword } = req.body; + const db = getDb(); + + const user = await db.get('SELECT password_hash FROM users WHERE id = ?', [req.user.id]); + if (!bcrypt.compareSync(currentPassword, user.password_hash)) { + return res.status(401).json({ error: 'Aktuelles Passwort ist falsch' }); + } + + if (newPassword.length < 6) { + return res.status(400).json({ error: 'Neues Passwort muss mindestens 6 Zeichen lang sein' }); + } + + const hash = bcrypt.hashSync(newPassword, 12); + await db.run('UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [hash, req.user.id]); + + res.json({ message: 'Passwort erfolgreich geändert' }); + } catch (err) { + console.error('Password change error:', err); + res.status(500).json({ error: 'Passwort konnte nicht geändert werden' }); + } +}); + +// POST /api/auth/avatar - Upload avatar image +router.post('/avatar', authenticateToken, async (req, res) => { + try { + const buffer = await new Promise((resolve, reject) => { + const chunks = []; + req.on('data', chunk => chunks.push(chunk)); + req.on('end', () => resolve(Buffer.concat(chunks))); + req.on('error', reject); + }); + + // Validate content type + const contentType = req.headers['content-type']; + if (!contentType || !contentType.startsWith('image/')) { + return res.status(400).json({ error: 'Nur Bilddateien sind erlaubt' }); + } + + // Max 2MB + if (buffer.length > 2 * 1024 * 1024) { + return res.status(400).json({ error: 'Bild darf maximal 2MB groß sein' }); + } + + const ext = contentType.includes('png') ? 'png' : contentType.includes('gif') ? 'gif' : contentType.includes('webp') ? 'webp' : 'jpg'; + const filename = `${req.user.id}_${Date.now()}.${ext}`; + const filepath = path.join(uploadsDir, filename); + + // Remove old avatar if exists + const db = getDb(); + const current = await db.get('SELECT avatar_image FROM users WHERE id = ?', [req.user.id]); + if (current?.avatar_image) { + const oldPath = path.join(uploadsDir, current.avatar_image); + if (fs.existsSync(oldPath)) fs.unlinkSync(oldPath); + } + + fs.writeFileSync(filepath, buffer); + + await db.run('UPDATE users SET avatar_image = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [filename, req.user.id]); + const updated = await db.get('SELECT id, name, email, role, theme, language, avatar_color, avatar_image FROM users WHERE id = ?', [req.user.id]); + + res.json({ user: updated }); + } catch (err) { + console.error('Avatar upload error:', err); + res.status(500).json({ error: 'Avatar konnte nicht hochgeladen werden' }); + } +}); + +// DELETE /api/auth/avatar - Remove avatar image +router.delete('/avatar', authenticateToken, async (req, res) => { + try { + const db = getDb(); + const current = await db.get('SELECT avatar_image FROM users WHERE id = ?', [req.user.id]); + if (current?.avatar_image) { + const oldPath = path.join(uploadsDir, current.avatar_image); + if (fs.existsSync(oldPath)) fs.unlinkSync(oldPath); + } + await db.run('UPDATE users SET avatar_image = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [req.user.id]); + const updated = await db.get('SELECT id, name, email, role, theme, language, avatar_color, avatar_image FROM users WHERE id = ?', [req.user.id]); + res.json({ user: updated }); + } catch (err) { + console.error('Avatar delete error:', err); + res.status(500).json({ error: 'Avatar konnte nicht entfernt werden' }); + } +}); + +// GET /api/auth/avatar/:filename - Serve avatar image +router.get('/avatar/:filename', (req, res) => { + const filepath = path.join(uploadsDir, req.params.filename); + if (!fs.existsSync(filepath)) { + return res.status(404).json({ error: 'Avatar nicht gefunden' }); + } + const ext = path.extname(filepath).slice(1); + const mimeMap = { jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif', webp: 'image/webp' }; + res.setHeader('Content-Type', mimeMap[ext] || 'image/jpeg'); + res.setHeader('Cache-Control', 'public, max-age=86400'); + fs.createReadStream(filepath).pipe(res); +}); + +export default router; diff --git a/server/routes/recordings.js b/server/routes/recordings.js new file mode 100644 index 0000000..1c5d208 --- /dev/null +++ b/server/routes/recordings.js @@ -0,0 +1,120 @@ +import { Router } from 'express'; +import { authenticateToken } from '../middleware/auth.js'; +import { getDb } from '../config/database.js'; +import { + getRecordings, + deleteRecording, + publishRecording, +} from '../config/bbb.js'; + +const router = Router(); + +// GET /api/recordings - Get recordings for a room (by meetingID/uid) +router.get('/', authenticateToken, async (req, res) => { + try { + const { meetingID } = req.query; + const recordings = await getRecordings(meetingID || undefined); + + // Format recordings + const formatted = recordings.map(rec => { + const playback = rec.playback?.format; + let formats = []; + if (playback) { + formats = Array.isArray(playback) ? playback : [playback]; + } + + return { + recordID: rec.recordID, + meetingID: rec.meetingID, + name: rec.name || 'Aufnahme', + state: rec.state, + published: rec.published === 'true', + startTime: rec.startTime, + endTime: rec.endTime, + participants: rec.participants, + size: rec.size, + formats: formats.map(f => ({ + type: f.type, + url: f.url, + length: f.length, + size: f.size, + })), + metadata: rec.metadata || {}, + }; + }); + + res.json({ recordings: formatted }); + } catch (err) { + console.error('Get recordings error:', err); + res.status(500).json({ error: 'Aufnahmen konnten nicht geladen werden', recordings: [] }); + } +}); + +// GET /api/recordings/room/:uid - Get recordings for a specific room +router.get('/room/:uid', authenticateToken, async (req, res) => { + try { + const db = getDb(); + const room = await db.get('SELECT * FROM rooms WHERE uid = ?', [req.params.uid]); + + if (!room) { + return res.status(404).json({ error: 'Raum nicht gefunden' }); + } + + const recordings = await getRecordings(room.uid); + const formatted = recordings.map(rec => { + const playback = rec.playback?.format; + let formats = []; + if (playback) { + formats = Array.isArray(playback) ? playback : [playback]; + } + + return { + recordID: rec.recordID, + meetingID: rec.meetingID, + name: rec.name || room.name, + state: rec.state, + published: rec.published === 'true', + startTime: rec.startTime, + endTime: rec.endTime, + participants: rec.participants, + size: rec.size, + formats: formats.map(f => ({ + type: f.type, + url: f.url, + length: f.length, + size: f.size, + })), + }; + }); + + res.json({ recordings: formatted }); + } catch (err) { + console.error('Get room recordings error:', err); + res.status(500).json({ error: 'Aufnahmen konnten nicht geladen werden', recordings: [] }); + } +}); + +// DELETE /api/recordings/:recordID +router.delete('/:recordID', authenticateToken, async (req, res) => { + try { + await deleteRecording(req.params.recordID); + res.json({ message: 'Aufnahme gelöscht' }); + } catch (err) { + console.error('Delete recording error:', err); + res.status(500).json({ error: 'Aufnahme konnte nicht gelöscht werden' }); + } +}); + +// PUT /api/recordings/:recordID/publish +router.put('/:recordID/publish', authenticateToken, async (req, res) => { + try { + const { publish } = req.body; + await publishRecording(req.params.recordID, publish); + res.json({ message: publish ? 'Aufnahme veröffentlicht' : 'Aufnahme nicht mehr öffentlich' }); + } catch (err) { + console.error('Publish recording error:', err); + res.status(500).json({ error: 'Aufnahme konnte nicht aktualisiert werden' }); + } +}); + +export default router; diff --git a/server/routes/rooms.js b/server/routes/rooms.js new file mode 100644 index 0000000..5fde173 --- /dev/null +++ b/server/routes/rooms.js @@ -0,0 +1,368 @@ +import { Router } from 'express'; +import crypto from 'crypto'; +import { getDb } from '../config/database.js'; +import { authenticateToken } from '../middleware/auth.js'; +import { + createMeeting, + joinMeeting, + endMeeting, + getMeetingInfo, + isMeetingRunning, +} from '../config/bbb.js'; + +const router = Router(); + +// GET /api/rooms - List user's rooms +router.get('/', authenticateToken, async (req, res) => { + try { + const db = getDb(); + const rooms = await db.all(` + SELECT r.*, u.name as owner_name + FROM rooms r + JOIN users u ON r.user_id = u.id + WHERE r.user_id = ? + ORDER BY r.created_at DESC + `, [req.user.id]); + + res.json({ rooms }); + } catch (err) { + console.error('List rooms error:', err); + res.status(500).json({ error: 'Räume konnten nicht geladen werden' }); + } +}); + +// GET /api/rooms/:uid - Get room details +router.get('/:uid', authenticateToken, async (req, res) => { + try { + const db = getDb(); + const room = await db.get(` + SELECT r.*, u.name as owner_name + FROM rooms r + JOIN users u ON r.user_id = u.id + WHERE r.uid = ? + `, [req.params.uid]); + + if (!room) { + return res.status(404).json({ error: 'Raum nicht gefunden' }); + } + + res.json({ room }); + } catch (err) { + console.error('Get room error:', err); + res.status(500).json({ error: 'Raum konnte nicht geladen werden' }); + } +}); + +// POST /api/rooms - Create room +router.post('/', authenticateToken, async (req, res) => { + try { + const { + name, + welcome_message, + max_participants, + access_code, + mute_on_join, + require_approval, + anyone_can_start, + all_join_moderator, + record_meeting, + guest_access, + moderator_code, + } = req.body; + + if (!name || name.trim().length === 0) { + return res.status(400).json({ error: 'Raumname ist erforderlich' }); + } + + const uid = crypto.randomBytes(8).toString('hex'); + const db = getDb(); + + const result = await db.run(` + INSERT INTO rooms (uid, name, user_id, welcome_message, max_participants, access_code, mute_on_join, require_approval, anyone_can_start, all_join_moderator, record_meeting, guest_access, moderator_code) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, [ + uid, + name.trim(), + req.user.id, + welcome_message || 'Willkommen im Meeting!', + max_participants || 0, + access_code || null, + mute_on_join !== false ? 1 : 0, + require_approval ? 1 : 0, + anyone_can_start ? 1 : 0, + all_join_moderator ? 1 : 0, + record_meeting !== false ? 1 : 0, + guest_access ? 1 : 0, + moderator_code || null, + ]); + + const room = await db.get('SELECT * FROM rooms WHERE id = ?', [result.lastInsertRowid]); + res.status(201).json({ room }); + } catch (err) { + console.error('Create room error:', err); + res.status(500).json({ error: 'Raum konnte nicht erstellt werden' }); + } +}); + +// PUT /api/rooms/:uid - Update room +router.put('/:uid', authenticateToken, async (req, res) => { + try { + const db = getDb(); + const room = await db.get('SELECT * FROM rooms WHERE uid = ? AND user_id = ?', [req.params.uid, req.user.id]); + + if (!room) { + return res.status(404).json({ error: 'Raum nicht gefunden oder keine Berechtigung' }); + } + + const { + name, + welcome_message, + max_participants, + access_code, + mute_on_join, + require_approval, + anyone_can_start, + all_join_moderator, + record_meeting, + guest_access, + moderator_code, + } = req.body; + + await db.run(` + UPDATE rooms SET + name = COALESCE(?, name), + welcome_message = COALESCE(?, welcome_message), + max_participants = COALESCE(?, max_participants), + access_code = ?, + mute_on_join = COALESCE(?, mute_on_join), + require_approval = COALESCE(?, require_approval), + anyone_can_start = COALESCE(?, anyone_can_start), + all_join_moderator = COALESCE(?, all_join_moderator), + record_meeting = COALESCE(?, record_meeting), + guest_access = COALESCE(?, guest_access), + moderator_code = ?, + updated_at = CURRENT_TIMESTAMP + WHERE uid = ? + `, [ + name, + welcome_message, + max_participants, + access_code ?? room.access_code, + mute_on_join !== undefined ? (mute_on_join ? 1 : 0) : null, + require_approval !== undefined ? (require_approval ? 1 : 0) : null, + anyone_can_start !== undefined ? (anyone_can_start ? 1 : 0) : null, + all_join_moderator !== undefined ? (all_join_moderator ? 1 : 0) : null, + record_meeting !== undefined ? (record_meeting ? 1 : 0) : null, + guest_access !== undefined ? (guest_access ? 1 : 0) : null, + moderator_code !== undefined ? (moderator_code || null) : room.moderator_code, + req.params.uid, + ]); + + const updated = await db.get('SELECT * FROM rooms WHERE uid = ?', [req.params.uid]); + res.json({ room: updated }); + } catch (err) { + console.error('Update room error:', err); + res.status(500).json({ error: 'Raum konnte nicht aktualisiert werden' }); + } +}); + +// DELETE /api/rooms/:uid - Delete room +router.delete('/:uid', authenticateToken, async (req, res) => { + try { + const db = getDb(); + const room = await db.get('SELECT * FROM rooms WHERE uid = ?', [req.params.uid]); + + if (!room) { + return res.status(404).json({ error: 'Raum nicht gefunden' }); + } + + if (room.user_id !== req.user.id && req.user.role !== 'admin') { + return res.status(403).json({ error: 'Keine Berechtigung' }); + } + + await db.run('DELETE FROM rooms WHERE uid = ?', [req.params.uid]); + res.json({ message: 'Raum erfolgreich gelöscht' }); + } catch (err) { + console.error('Delete room error:', err); + res.status(500).json({ error: 'Raum konnte nicht gelöscht werden' }); + } +}); + +// POST /api/rooms/:uid/start - Start meeting +router.post('/:uid/start', authenticateToken, async (req, res) => { + try { + const db = getDb(); + const room = await db.get('SELECT * FROM rooms WHERE uid = ? AND user_id = ?', [req.params.uid, req.user.id]); + + if (!room) { + return res.status(404).json({ error: 'Raum nicht gefunden oder keine Berechtigung' }); + } + + await createMeeting(room); + const joinUrl = await joinMeeting(room.uid, req.user.name, true); + res.json({ joinUrl }); + } catch (err) { + console.error('Start meeting error:', err); + res.status(500).json({ error: 'Meeting konnte nicht gestartet werden' }); + } +}); + +// POST /api/rooms/:uid/join - Join meeting +router.post('/:uid/join', authenticateToken, async (req, res) => { + try { + const db = getDb(); + const room = await db.get('SELECT * FROM rooms WHERE uid = ?', [req.params.uid]); + + if (!room) { + return res.status(404).json({ error: 'Raum nicht gefunden' }); + } + + // Check access code if set + if (room.access_code && req.body.access_code !== room.access_code) { + return res.status(403).json({ error: 'Falscher Zugangscode' }); + } + + // Check if meeting is running + const running = await isMeetingRunning(room.uid); + if (!running) { + return res.status(400).json({ error: 'Meeting läuft nicht. Bitte warten Sie, bis der Moderator das Meeting gestartet hat.' }); + } + + const isModerator = room.user_id === req.user.id || room.all_join_moderator; + const joinUrl = await joinMeeting(room.uid, req.user.name, isModerator); + res.json({ joinUrl }); + } catch (err) { + console.error('Join meeting error:', err); + res.status(500).json({ error: 'Meeting konnte nicht beigetreten werden' }); + } +}); + +// POST /api/rooms/:uid/end - End meeting +router.post('/:uid/end', authenticateToken, async (req, res) => { + try { + const db = getDb(); + const room = await db.get('SELECT * FROM rooms WHERE uid = ? AND user_id = ?', [req.params.uid, req.user.id]); + + if (!room) { + return res.status(404).json({ error: 'Raum nicht gefunden oder keine Berechtigung' }); + } + + await endMeeting(room.uid); + res.json({ message: 'Meeting beendet' }); + } catch (err) { + console.error('End meeting error:', err); + res.status(500).json({ error: 'Meeting konnte nicht beendet werden' }); + } +}); + +// GET /api/rooms/:uid/public - Get public room info (no auth needed) +router.get('/:uid/public', async (req, res) => { + try { + const db = getDb(); + const room = await db.get(` + SELECT r.uid, r.name, r.guest_access, r.welcome_message, r.access_code, + u.name as owner_name + FROM rooms r + JOIN users u ON r.user_id = u.id + WHERE r.uid = ? + `, [req.params.uid]); + + if (!room) { + return res.status(404).json({ error: 'Raum nicht gefunden' }); + } + + if (!room.guest_access) { + return res.status(403).json({ error: 'Gastzugang ist für diesen Raum nicht aktiviert' }); + } + + const running = await isMeetingRunning(room.uid); + + res.json({ + room: { + uid: room.uid, + name: room.name, + owner_name: room.owner_name, + welcome_message: room.welcome_message, + has_access_code: !!room.access_code, + }, + running, + }); + } catch (err) { + console.error('Public room info error:', err); + res.status(500).json({ error: 'Rauminfos konnten nicht geladen werden' }); + } +}); + +// POST /api/rooms/:uid/guest-join - Join meeting as guest (no auth needed) +router.post('/:uid/guest-join', async (req, res) => { + try { + const { name, access_code, moderator_code } = req.body; + + if (!name || name.trim().length === 0) { + return res.status(400).json({ error: 'Name ist erforderlich' }); + } + + const db = getDb(); + const room = await db.get('SELECT * FROM rooms WHERE uid = ?', [req.params.uid]); + + if (!room) { + return res.status(404).json({ error: 'Raum nicht gefunden' }); + } + + if (!room.guest_access) { + return res.status(403).json({ error: 'Gastzugang ist für diesen Raum nicht aktiviert' }); + } + + // Check access code if set + if (room.access_code && access_code !== room.access_code) { + return res.status(403).json({ error: 'Falscher Zugangscode' }); + } + + // Check if meeting is running (or if anyone_can_start is enabled) + const running = await isMeetingRunning(room.uid); + if (!running && !room.anyone_can_start) { + return res.status(400).json({ error: 'Meeting läuft nicht. Bitte warten Sie, bis der Moderator das Meeting gestartet hat.' }); + } + + // If meeting not running but anyone_can_start, create it + if (!running && room.anyone_can_start) { + await createMeeting(room); + } + + // Check moderator code + let isModerator = !!room.all_join_moderator; + if (!isModerator && moderator_code && room.moderator_code && moderator_code === room.moderator_code) { + isModerator = true; + } + + const joinUrl = await joinMeeting(room.uid, name.trim(), isModerator); + res.json({ joinUrl }); + } catch (err) { + console.error('Guest join error:', err); + res.status(500).json({ error: 'Beitritt als Gast fehlgeschlagen' }); + } +}); + +// GET /api/rooms/:uid/status - Check if meeting is running (public, no guard needed) +router.get('/:uid/status', async (req, res) => { + try { + const running = await isMeetingRunning(req.params.uid); + let info = null; + if (running) { + try { + info = await getMeetingInfo(req.params.uid); + } catch (e) { + // Meeting info might fail + } + } + res.json({ + running, + participantCount: info?.participantCount || 0, + moderatorCount: info?.moderatorCount || 0, + }); + } catch (err) { + res.json({ running: false, participantCount: 0, moderatorCount: 0 }); + } +}); + +export default router; diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 0000000..e26924e --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,45 @@ +import { Routes, Route, Navigate } from 'react-router-dom'; +import { useAuth } from './contexts/AuthContext'; +import Layout from './components/Layout'; +import ProtectedRoute from './components/ProtectedRoute'; +import Home from './pages/Home'; +import Login from './pages/Login'; +import Register from './pages/Register'; +import Dashboard from './pages/Dashboard'; +import RoomDetail from './pages/RoomDetail'; +import Settings from './pages/Settings'; +import Admin from './pages/Admin'; +import GuestJoin from './pages/GuestJoin'; + +export default function App() { + const { user, loading } = useAuth(); + + if (loading) { + return ( +
+
+
+ ); + } + + return ( + + {/* Public routes */} + : } /> + : } /> + : } /> + } /> + + {/* Protected routes */} + }> + } /> + } /> + } /> + } /> + + + {/* Catch all */} + } /> + + ); +} diff --git a/src/components/Layout.jsx b/src/components/Layout.jsx new file mode 100644 index 0000000..4760f74 --- /dev/null +++ b/src/components/Layout.jsx @@ -0,0 +1,31 @@ +import { Outlet } from 'react-router-dom'; +import { useState } from 'react'; +import Navbar from './Navbar'; +import Sidebar from './Sidebar'; + +export default function Layout() { + const [sidebarOpen, setSidebarOpen] = useState(false); + + return ( +
+ {/* Sidebar */} + setSidebarOpen(false)} /> + + {/* Main content */} +
+ setSidebarOpen(true)} /> +
+ +
+
+ + {/* Mobile overlay */} + {sidebarOpen && ( +
setSidebarOpen(false)} + /> + )} +
+ ); +} diff --git a/src/components/Modal.jsx b/src/components/Modal.jsx new file mode 100644 index 0000000..7083a5d --- /dev/null +++ b/src/components/Modal.jsx @@ -0,0 +1,25 @@ +import { X } from 'lucide-react'; + +export default function Modal({ title, children, onClose, maxWidth = 'max-w-lg' }) { + return ( +
+
+
+ {/* Header */} +
+

{title}

+ +
+ {/* Body */} +
+ {children} +
+
+
+ ); +} diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx new file mode 100644 index 0000000..ed1755d --- /dev/null +++ b/src/components/Navbar.jsx @@ -0,0 +1,118 @@ +import { Menu, Search, LogOut, User } from 'lucide-react'; +import { useAuth } from '../contexts/AuthContext'; +import { useLanguage } from '../contexts/LanguageContext'; +import { useNavigate } from 'react-router-dom'; +import { useState, useRef, useEffect } from 'react'; +import api from '../services/api'; + +export default function Navbar({ onMenuClick }) { + const { user, logout } = useAuth(); + const { t } = useLanguage(); + const navigate = useNavigate(); + const [dropdownOpen, setDropdownOpen] = useState(false); + const dropdownRef = useRef(null); + + useEffect(() => { + function handleClick(e) { + if (dropdownRef.current && !dropdownRef.current.contains(e.target)) { + setDropdownOpen(false); + } + } + document.addEventListener('mousedown', handleClick); + return () => document.removeEventListener('mousedown', handleClick); + }, []); + + const handleLogout = () => { + logout(); + navigate('/'); + }; + + const initials = user?.name + ? user.name + .split(' ') + .map(n => n[0]) + .join('') + .toUpperCase() + .slice(0, 2) + : '?'; + + return ( +
+
+ {/* Left section */} +
+ + + {/* Search */} +
+ + +
+
+ + {/* Right section */} +
+ {/* User dropdown */} +
+ + + {dropdownOpen && ( +
+
+

{user?.name}

+

{user?.email}

+
+
+ + +
+
+ )} +
+
+
+
+ ); +} diff --git a/src/components/ProtectedRoute.jsx b/src/components/ProtectedRoute.jsx new file mode 100644 index 0000000..2339e47 --- /dev/null +++ b/src/components/ProtectedRoute.jsx @@ -0,0 +1,20 @@ +import { Navigate } from 'react-router-dom'; +import { useAuth } from '../contexts/AuthContext'; + +export default function ProtectedRoute({ children }) { + const { user, loading } = useAuth(); + + if (loading) { + return ( +
+
+
+ ); + } + + if (!user) { + return ; + } + + return children; +} diff --git a/src/components/RecordingList.jsx b/src/components/RecordingList.jsx new file mode 100644 index 0000000..8ac3448 --- /dev/null +++ b/src/components/RecordingList.jsx @@ -0,0 +1,159 @@ +import { Play, Trash2, Eye, EyeOff, Download, Clock, Users, FileVideo } from 'lucide-react'; +import { useState } from 'react'; +import api from '../services/api'; +import { useLanguage } from '../contexts/LanguageContext'; +import toast from 'react-hot-toast'; + +export default function RecordingList({ recordings, onRefresh }) { + const [loading, setLoading] = useState({}); + const { t, language } = useLanguage(); + + const formatDuration = (startTime, endTime) => { + if (!startTime || !endTime) return '—'; + const ms = parseInt(endTime) - parseInt(startTime); + const minutes = Math.floor(ms / 60000); + const hours = Math.floor(minutes / 60); + const mins = minutes % 60; + if (hours > 0) return `${hours}h ${mins}m`; + return `${mins}m`; + }; + + const formatDate = (timestamp) => { + if (!timestamp) return '—'; + return new Date(parseInt(timestamp)).toLocaleDateString(language === 'de' ? 'de-DE' : 'en-US', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + }; + + const formatSize = (bytes) => { + if (!bytes) return ''; + const mb = parseInt(bytes) / (1024 * 1024); + if (mb > 1024) return `${(mb / 1024).toFixed(1)} GB`; + return `${mb.toFixed(1)} MB`; + }; + + const handleDelete = async (recordID) => { + if (!confirm(t('recordings.deleteConfirm'))) return; + setLoading(prev => ({ ...prev, [recordID]: 'deleting' })); + try { + await api.delete(`/recordings/${recordID}`); + toast.success(t('recordings.deleted')); + onRefresh?.(); + } catch (err) { + toast.error(t('recordings.deleteFailed')); + } finally { + setLoading(prev => ({ ...prev, [recordID]: null })); + } + }; + + const handlePublish = async (recordID, publish) => { + setLoading(prev => ({ ...prev, [recordID]: 'publishing' })); + try { + await api.put(`/recordings/${recordID}/publish`, { publish }); + toast.success(publish ? t('recordings.publishSuccess') : t('recordings.unpublishSuccess')); + onRefresh?.(); + } catch (err) { + toast.error(t('recordings.publishFailed')); + } finally { + setLoading(prev => ({ ...prev, [recordID]: null })); + } + }; + + if (!recordings || recordings.length === 0) { + return ( +
+ +

{t('recordings.noRecordings')}

+
+ ); + } + + return ( +
+ {recordings.map(rec => ( +
+
+
+
+

+ {rec.name} +

+ + {rec.published ? t('recordings.published') : t('recordings.unpublished')} + +
+ +
+ + + {formatDate(rec.startTime)} + + + + {formatDuration(rec.startTime, rec.endTime)} + + {rec.participants && ( + + + {rec.participants} + + )} + {rec.size && ( + {formatSize(rec.size)} + )} +
+ + {/* Playback formats */} + {rec.formats && rec.formats.length > 0 && ( + + )} +
+ + {/* Actions */} +
+ + +
+
+
+ ))} +
+ ); +} diff --git a/src/components/RoomCard.jsx b/src/components/RoomCard.jsx new file mode 100644 index 0000000..c3bd3cd --- /dev/null +++ b/src/components/RoomCard.jsx @@ -0,0 +1,108 @@ +import { Users, Play, Trash2, Radio, Loader2 } from 'lucide-react'; +import { useNavigate } from 'react-router-dom'; +import { useState, useEffect } from 'react'; +import api from '../services/api'; +import { useLanguage } from '../contexts/LanguageContext'; +import toast from 'react-hot-toast'; + +export default function RoomCard({ room, onDelete }) { + const navigate = useNavigate(); + const { t } = useLanguage(); + const [status, setStatus] = useState({ running: false, participantCount: 0 }); + const [starting, setStarting] = useState(false); + + useEffect(() => { + const checkStatus = async () => { + try { + const res = await api.get(`/rooms/${room.uid}/status`); + setStatus(res.data); + } catch { + // Ignore errors + } + }; + checkStatus(); + const interval = setInterval(checkStatus, 15000); + return () => clearInterval(interval); + }, [room.uid]); + + return ( +
navigate(`/rooms/${room.uid}`)}> +
+
+
+

+ {room.name} +

+ {status.running && ( + + + {t('common.live')} + + )} +
+

+ {room.uid.substring(0, 8)}... +

+
+
+ + {/* Room info */} +
+ + + {status.running ? t('room.participants', { count: status.participantCount }) : t('common.offline')} + + {room.max_participants > 0 && ( + Max: {room.max_participants} + )} + {room.access_code && ( + + {t('common.protected')} + + )} +
+ + {/* Actions */} +
+ + {onDelete && ( + + )} +
+
+ ); +} diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx new file mode 100644 index 0000000..935ed55 --- /dev/null +++ b/src/components/Sidebar.jsx @@ -0,0 +1,108 @@ +import { NavLink } from 'react-router-dom'; +import { LayoutDashboard, Settings, Shield, Video, X, Palette } from 'lucide-react'; +import { useAuth } from '../contexts/AuthContext'; +import { useLanguage } from '../contexts/LanguageContext'; +import ThemeSelector from './ThemeSelector'; +import { useState } from 'react'; + +export default function Sidebar({ open, onClose }) { + const { user } = useAuth(); + const { t } = useLanguage(); + const [themeOpen, setThemeOpen] = useState(false); + + const navItems = [ + { to: '/dashboard', icon: LayoutDashboard, label: t('nav.dashboard') }, + { to: '/settings', icon: Settings, label: t('nav.settings') }, + ]; + + if (user?.role === 'admin') { + navItems.push({ to: '/admin', icon: Shield, label: t('nav.admin') }); + } + + const linkClasses = ({ isActive }) => + `flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 ${ + isActive + ? 'bg-th-accent text-th-accent-t shadow-sm' + : 'text-th-text-s hover:text-th-text hover:bg-th-hover' + }`; + + return ( + <> + + + {/* Theme Selector Modal */} + {themeOpen && setThemeOpen(false)} />} + + ); +} diff --git a/src/components/ThemeSelector.jsx b/src/components/ThemeSelector.jsx new file mode 100644 index 0000000..1d3a1e5 --- /dev/null +++ b/src/components/ThemeSelector.jsx @@ -0,0 +1,99 @@ +import { X, Check, Sun, Moon } from 'lucide-react'; +import { useTheme } from '../contexts/ThemeContext'; +import { useLanguage } from '../contexts/LanguageContext'; +import { getThemeGroups } from '../themes'; + +export default function ThemeSelector({ onClose }) { + const { theme, setTheme } = useTheme(); + const { t } = useLanguage(); + const groups = getThemeGroups(); + + return ( +
+
+ +
+ {/* Header */} +
+
+

{t('themes.selectTheme')}

+

{t('themes.selectThemeSubtitle')}

+
+ +
+ + {/* Theme Grid */} +
+ {Object.entries(groups).map(([groupName, groupThemes]) => ( +
+

+ {groupName} +

+
+ {groupThemes.map(t => ( + + ))} +
+
+ ))} +
+
+
+ ); +} diff --git a/src/contexts/AuthContext.jsx b/src/contexts/AuthContext.jsx new file mode 100644 index 0000000..fb61f95 --- /dev/null +++ b/src/contexts/AuthContext.jsx @@ -0,0 +1,60 @@ +import { createContext, useContext, useState, useEffect, useCallback } from 'react'; +import api from '../services/api'; + +const AuthContext = createContext(null); + +export function AuthProvider({ children }) { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const token = localStorage.getItem('token'); + if (token) { + api.get('/auth/me') + .then(res => setUser(res.data.user)) + .catch(() => { + localStorage.removeItem('token'); + }) + .finally(() => setLoading(false)); + } else { + setLoading(false); + } + }, []); + + const login = useCallback(async (email, password) => { + const res = await api.post('/auth/login', { email, password }); + localStorage.setItem('token', res.data.token); + setUser(res.data.user); + return res.data.user; + }, []); + + const register = useCallback(async (name, email, password) => { + const res = await api.post('/auth/register', { name, email, password }); + localStorage.setItem('token', res.data.token); + setUser(res.data.user); + return res.data.user; + }, []); + + const logout = useCallback(() => { + localStorage.removeItem('token'); + setUser(null); + }, []); + + const updateUser = useCallback((updatedUser) => { + setUser(updatedUser); + }, []); + + return ( + + {children} + + ); +} + +export function useAuth() { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +} diff --git a/src/contexts/LanguageContext.jsx b/src/contexts/LanguageContext.jsx new file mode 100644 index 0000000..4342f9f --- /dev/null +++ b/src/contexts/LanguageContext.jsx @@ -0,0 +1,46 @@ +import { createContext, useContext, useState, useEffect, useCallback } from 'react'; +import { getTranslation, interpolate, pluralize, defaultLanguage } from '../i18n'; + +const LanguageContext = createContext(null); + +export function LanguageProvider({ children }) { + const [language, setLanguageState] = useState(() => { + return localStorage.getItem('language') || defaultLanguage; + }); + + useEffect(() => { + localStorage.setItem('language', language); + document.documentElement.setAttribute('lang', language); + }, [language]); + + const setLanguage = useCallback((lang) => { + setLanguageState(lang); + }, []); + + // t('auth.login') -> translated string + // t('dashboard.roomCount', { count: 5 }) -> with interpolation + const t = useCallback((key, params) => { + const raw = getTranslation(language, key); + if (params && params.count !== undefined) { + return pluralize(raw, params.count); + } + if (params) { + return interpolate(raw, params); + } + return raw; + }, [language]); + + return ( + + {children} + + ); +} + +export function useLanguage() { + const context = useContext(LanguageContext); + if (!context) { + throw new Error('useLanguage must be used within a LanguageProvider'); + } + return context; +} diff --git a/src/contexts/ThemeContext.jsx b/src/contexts/ThemeContext.jsx new file mode 100644 index 0000000..835e1e4 --- /dev/null +++ b/src/contexts/ThemeContext.jsx @@ -0,0 +1,32 @@ +import { createContext, useContext, useState, useEffect, useCallback } from 'react'; + +const ThemeContext = createContext(null); + +export function ThemeProvider({ children }) { + const [theme, setThemeState] = useState(() => { + return localStorage.getItem('theme') || 'dark'; + }); + + useEffect(() => { + document.documentElement.setAttribute('data-theme', theme); + localStorage.setItem('theme', theme); + }, [theme]); + + const setTheme = useCallback((newTheme) => { + setThemeState(newTheme); + }, []); + + return ( + + {children} + + ); +} + +export function useTheme() { + const context = useContext(ThemeContext); + if (!context) { + throw new Error('useTheme must be used within a ThemeProvider'); + } + return context; +} diff --git a/src/i18n/de.json b/src/i18n/de.json new file mode 100644 index 0000000..21aacb6 --- /dev/null +++ b/src/i18n/de.json @@ -0,0 +1,244 @@ +{ + "common": { + "appName": "Redlight", + "loading": "Laden...", + "save": "Speichern", + "cancel": "Abbrechen", + "delete": "Löschen", + "edit": "Bearbeiten", + "create": "Erstellen", + "search": "Suchen...", + "close": "Schließen", + "confirm": "Bestätigen", + "back": "Zurück", + "yes": "Ja", + "no": "Nein", + "or": "oder", + "optional": "Optional", + "unlimited": "Unbegrenzt", + "none": "Keiner", + "offline": "Offline", + "active": "Aktiv", + "inactive": "Inaktiv", + "protected": "Geschützt", + "live": "Live", + "error": "Fehler", + "success": "Erfolg" + }, + "nav": { + "dashboard": "Dashboard", + "settings": "Einstellungen", + "admin": "Administration", + "appearance": "Darstellung", + "changeTheme": "Theme ändern", + "navigation": "Navigation" + }, + "auth": { + "login": "Anmelden", + "register": "Registrieren", + "logout": "Abmelden", + "email": "E-Mail", + "password": "Passwort", + "name": "Name", + "welcomeBack": "Willkommen zurück", + "loginSubtitle": "Melden Sie sich an, um auf Ihre Räume zuzugreifen.", + "createAccount": "Konto erstellen", + "registerSubtitle": "Registrieren Sie sich, um Räume zu erstellen und Meetings zu starten.", + "noAccount": "Noch kein Konto?", + "hasAccount": "Bereits ein Konto?", + "signUpNow": "Jetzt registrieren", + "signInNow": "Jetzt anmelden", + "backToHome": "← Zurück zur Startseite", + "emailPlaceholder": "name@beispiel.de", + "passwordPlaceholder": "••••••••", + "namePlaceholder": "Max Mustermann", + "minPassword": "Mindestens 6 Zeichen", + "confirmPassword": "Passwort bestätigen", + "repeatPassword": "Passwort wiederholen", + "passwordMismatch": "Passwörter stimmen nicht überein", + "passwordTooShort": "Passwort muss mindestens 6 Zeichen lang sein", + "loginSuccess": "Willkommen zurück!", + "registerSuccess": "Registrierung erfolgreich!", + "loginFailed": "Anmeldung fehlgeschlagen", + "registerFailed": "Registrierung fehlgeschlagen", + "allFieldsRequired": "Alle Felder sind erforderlich" + }, + "home": { + "poweredBy": "Powered by BigBlueButton", + "heroTitle": "Meetings neu ", + "heroTitleHighlight": "definiert", + "heroSubtitle": "Das moderne, selbst gehostete BigBlueButton-Frontend. Erstellen Sie Räume, verwalten Sie Aufnahmen und genießen Sie ein wunderschönes Interface mit über 15 Themes.", + "getStarted": "Jetzt starten", + "features": "Alles was Sie brauchen", + "featuresSubtitle": "Redlight bietet alle Funktionen, die Sie für professionelle Videokonferenzen benötigen.", + "featureVideoTitle": "Videokonferenzen", + "featureVideoDesc": "Erstellen und verwalten Sie Meetings direkt über BigBlueButton.", + "featureRoomsTitle": "Raumverwaltung", + "featureRoomsDesc": "Unbegrenzte Räume mit individuellen Einstellungen und Zugangscodes.", + "featureUsersTitle": "Benutzerverwaltung", + "featureUsersDesc": "Registrierung, Login und Rollenverwaltung für Ihre Organisation.", + "featureThemesTitle": "15+ Themes", + "featureThemesDesc": "Dracula, Nord, Catppuccin, Rosé Pine, Gruvbox und viele mehr.", + "featureRecordingsTitle": "Aufnahmen", + "featureRecordingsDesc": "Alle Aufnahmen pro Raum einsehen, veröffentlichen oder löschen.", + "featureOpenSourceTitle": "Open Source", + "featureOpenSourceDesc": "Vollständig quelloffen und selbst gehostet. Ihre Daten bleiben bei Ihnen.", + "statThemes": "Themes", + "statRooms": "Räume", + "statOpenSource": "Open Source", + "footer": "© {year} Redlight. Ein Open-Source BigBlueButton Frontend." + }, + "dashboard": { + "myRooms": "Meine Räume", + "roomCount": "{count} Raum erstellt | {count} Räume erstellt", + "newRoom": "Neuer Raum", + "noRooms": "Noch keine Räume", + "noRoomsSubtitle": "Erstellen Sie Ihren ersten Raum, um Meetings zu starten.", + "createFirst": "Ersten Raum erstellen", + "createRoom": "Neuen Raum erstellen", + "roomName": "Raumname", + "roomNamePlaceholder": "z.B. Team Meeting", + "roomNameRequired": "Raumname ist erforderlich", + "welcomeMessage": "Willkommensnachricht", + "welcomeMessageDefault": "Willkommen im Meeting!", + "maxParticipants": "Max. Teilnehmer", + "maxParticipantsHint": "0 = unbegrenzt", + "accessCode": "Zugangscode", + "muteOnJoin": "Teilnehmer beim Beitritt stummschalten", + "allowRecording": "Aufnahme erlauben", + "roomCreated": "Raum erstellt!", + "roomCreateFailed": "Raum konnte nicht erstellt werden", + "roomDeleted": "Raum gelöscht", + "roomDeleteFailed": "Raum konnte nicht gelöscht werden", + "roomDeleteConfirm": "Raum \"{name}\" wirklich löschen?", + "loadFailed": "Räume konnten nicht geladen werden" + }, + "room": { + "backToDashboard": "Zurück zum Dashboard", + "start": "Starten", + "startMeeting": "Meeting starten", + "join": "Beitreten", + "end": "Beenden", + "openDetails": "Details öffnen", + "overview": "Übersicht", + "recordings": "Aufnahmen", + "settings": "Einstellungen", + "participants": "{count} Teilnehmer", + "copyLink": "Link kopieren", + "linkCopied": "Link kopiert!", + "meetingDetails": "Meeting-Details", + "meetingId": "Meeting ID", + "status": "Status", + "maxParticipants": "Max. Teilnehmer", + "accessCode": "Zugangscode", + "roomSettings": "Raumeinstellungen", + "mutedOnJoin": "Beim Beitritt stummgeschaltet", + "micActiveOnJoin": "Mikrofon aktiv beim Beitritt", + "approvalRequired": "Genehmigung erforderlich", + "freeJoin": "Freier Beitritt", + "allModerators": "Alle als Moderator", + "rolesAssigned": "Rollen werden zugewiesen", + "recordingAllowed": "Aufnahme erlaubt", + "recordingDisabled": "Aufnahme deaktiviert", + "welcomeMsg": "Willkommensnachricht", + "muteOnJoin": "Beim Beitritt stummschalten", + "requireApproval": "Moderator-Genehmigung erforderlich", + "anyoneCanStart": "Jeder kann das Meeting starten", + "allJoinModerator": "Alle Teilnehmer als Moderator", + "allowRecording": "Aufnahme erlauben", + "noAccessCode": "Kein Zugangscode", + "emptyNoCode": "Leer = kein Code", + "settingsSaved": "Einstellungen gespeichert", + "settingsSaveFailed": "Einstellungen konnten nicht gespeichert werden", + "meetingStarted": "Meeting gestartet!", + "meetingStartFailed": "Meeting konnte nicht gestartet werden", + "meetingEnded": "Meeting beendet", + "meetingEndFailed": "Meeting konnte nicht beendet werden", + "joinFailed": "Beitritt fehlgeschlagen", + "endConfirm": "Meeting wirklich beenden?", + "enterAccessCode": "Zugangscode eingeben:", + "notFound": "Raum nicht gefunden" + }, + "recordings": { + "title": "Aufnahmen", + "noRecordings": "Keine Aufnahmen vorhanden", + "published": "Veröffentlicht", + "unpublished": "Nicht veröffentlicht", + "presentation": "Präsentation", + "deleted": "Aufnahme gelöscht", + "deleteFailed": "Fehler beim Löschen", + "deleteConfirm": "Aufnahme wirklich löschen?", + "publishSuccess": "Aufnahme veröffentlicht", + "unpublishSuccess": "Aufnahme versteckt", + "publishFailed": "Fehler beim Aktualisieren", + "hide": "Verstecken", + "publish": "Veröffentlichen", + "loadFailed": "Aufnahmen konnten nicht geladen werden" + }, + "settings": { + "title": "Einstellungen", + "subtitle": "Verwalten Sie Ihr Profil und Ihre Einstellungen", + "profile": "Profil", + "password": "Passwort", + "themes": "Themes", + "language": "Sprache", + "editProfile": "Profil bearbeiten", + "avatar": "Profilbild", + "avatarColor": "Avatar-Farbe", + "avatarColorHint": "Wird als Fallback verwendet, wenn kein Bild hochgeladen ist.", + "uploadImage": "Bild hochladen", + "removeImage": "Bild entfernen", + "avatarHint": "JPG, PNG, GIF oder WebP. Max. 2 MB.", + "avatarUploaded": "Profilbild aktualisiert", + "avatarUploadFailed": "Fehler beim Hochladen", + "avatarRemoved": "Profilbild entfernt", + "avatarRemoveFailed": "Fehler beim Entfernen", + "avatarInvalidType": "Nur Bilddateien sind erlaubt", + "avatarTooLarge": "Bild darf maximal 2 MB groß sein", + "changePassword": "Passwort ändern", + "currentPassword": "Aktuelles Passwort", + "newPassword": "Neues Passwort", + "confirmNewPassword": "Neues Passwort bestätigen", + "profileSaved": "Profil gespeichert", + "profileSaveFailed": "Fehler beim Speichern", + "passwordChanged": "Passwort geändert", + "passwordChangeFailed": "Fehler beim Ändern", + "passwordMismatch": "Passwörter stimmen nicht überein", + "selectLanguage": "Sprache auswählen" + }, + "themes": { + "selectTheme": "Theme auswählen", + "selectThemeSubtitle": "Wähle dein bevorzugtes Farbschema", + "light": "Hell", + "dark": "Dunkel" + }, + "admin": { + "title": "Administration", + "userCount": "{count} Benutzer registriert | {count} Benutzer registriert", + "searchUsers": "Benutzer suchen...", + "user": "Benutzer", + "role": "Rolle", + "rooms": "Räume", + "registered": "Registriert", + "actions": "Aktionen", + "admin": "Admin", + "makeAdmin": "Zum Admin machen", + "makeUser": "Zum Benutzer machen", + "resetPassword": "Passwort zurücksetzen", + "deleteUser": "Löschen", + "createUser": "Benutzer erstellen", + "createUserTitle": "Neuen Benutzer erstellen", + "userCreated": "Benutzer erstellt", + "userCreateFailed": "Benutzer konnte nicht erstellt werden", + "newPasswordLabel": "Neues Passwort", + "resetPasswordTitle": "Passwort zurücksetzen", + "noUsersFound": "Keine Benutzer gefunden", + "roleUpdated": "Rolle aktualisiert", + "roleUpdateFailed": "Fehler beim Aktualisieren", + "userDeleted": "Benutzer gelöscht", + "userDeleteFailed": "Fehler beim Löschen", + "passwordReset": "Passwort zurückgesetzt", + "passwordResetFailed": "Fehler beim Zurücksetzen", + "deleteUserConfirm": "Benutzer \"{name}\" wirklich löschen? Alle Räume werden ebenfalls gelöscht." + } +} diff --git a/src/i18n/en.json b/src/i18n/en.json new file mode 100644 index 0000000..0dd55d3 --- /dev/null +++ b/src/i18n/en.json @@ -0,0 +1,244 @@ +{ + "common": { + "appName": "Redlight", + "loading": "Loading...", + "save": "Save", + "cancel": "Cancel", + "delete": "Delete", + "edit": "Edit", + "create": "Create", + "search": "Search...", + "close": "Close", + "confirm": "Confirm", + "back": "Back", + "yes": "Yes", + "no": "No", + "or": "or", + "optional": "Optional", + "unlimited": "Unlimited", + "none": "None", + "offline": "Offline", + "active": "Active", + "inactive": "Inactive", + "protected": "Protected", + "live": "Live", + "error": "Error", + "success": "Success" + }, + "nav": { + "dashboard": "Dashboard", + "settings": "Settings", + "admin": "Administration", + "appearance": "Appearance", + "changeTheme": "Change theme", + "navigation": "Navigation" + }, + "auth": { + "login": "Sign in", + "register": "Sign up", + "logout": "Sign out", + "email": "Email", + "password": "Password", + "name": "Name", + "welcomeBack": "Welcome back", + "loginSubtitle": "Sign in to access your rooms.", + "createAccount": "Create account", + "registerSubtitle": "Sign up to create rooms and start meetings.", + "noAccount": "Don't have an account?", + "hasAccount": "Already have an account?", + "signUpNow": "Sign up now", + "signInNow": "Sign in now", + "backToHome": "← Back to homepage", + "emailPlaceholder": "name@example.com", + "passwordPlaceholder": "••••••••", + "namePlaceholder": "John Doe", + "minPassword": "At least 6 characters", + "confirmPassword": "Confirm password", + "repeatPassword": "Repeat password", + "passwordMismatch": "Passwords do not match", + "passwordTooShort": "Password must be at least 6 characters", + "loginSuccess": "Welcome back!", + "registerSuccess": "Registration successful!", + "loginFailed": "Login failed", + "registerFailed": "Registration failed", + "allFieldsRequired": "All fields are required" + }, + "home": { + "poweredBy": "Powered by BigBlueButton", + "heroTitle": "Meetings re", + "heroTitleHighlight": "defined", + "heroSubtitle": "The modern, self-hosted BigBlueButton frontend. Create rooms, manage recordings and enjoy a beautiful interface with over 15 themes.", + "getStarted": "Get started", + "features": "Everything you need", + "featuresSubtitle": "Redlight provides all the features you need for professional video conferencing.", + "featureVideoTitle": "Video Conferencing", + "featureVideoDesc": "Create and manage meetings directly via BigBlueButton.", + "featureRoomsTitle": "Room Management", + "featureRoomsDesc": "Unlimited rooms with individual settings and access codes.", + "featureUsersTitle": "User Management", + "featureUsersDesc": "Registration, login and role management for your organization.", + "featureThemesTitle": "15+ Themes", + "featureThemesDesc": "Dracula, Nord, Catppuccin, Rosé Pine, Gruvbox and many more.", + "featureRecordingsTitle": "Recordings", + "featureRecordingsDesc": "View, publish or delete all recordings per room.", + "featureOpenSourceTitle": "Open Source", + "featureOpenSourceDesc": "Fully open source and self-hosted. Your data stays with you.", + "statThemes": "Themes", + "statRooms": "Rooms", + "statOpenSource": "Open Source", + "footer": "© {year} Redlight. An open source BigBlueButton frontend." + }, + "dashboard": { + "myRooms": "My Rooms", + "roomCount": "{count} room created | {count} rooms created", + "newRoom": "New Room", + "noRooms": "No rooms yet", + "noRoomsSubtitle": "Create your first room to start meetings.", + "createFirst": "Create first room", + "createRoom": "Create new room", + "roomName": "Room name", + "roomNamePlaceholder": "e.g. Team Meeting", + "roomNameRequired": "Room name is required", + "welcomeMessage": "Welcome message", + "welcomeMessageDefault": "Welcome to the meeting!", + "maxParticipants": "Max. participants", + "maxParticipantsHint": "0 = unlimited", + "accessCode": "Access code", + "muteOnJoin": "Mute participants on join", + "allowRecording": "Allow recording", + "roomCreated": "Room created!", + "roomCreateFailed": "Room could not be created", + "roomDeleted": "Room deleted", + "roomDeleteFailed": "Room could not be deleted", + "roomDeleteConfirm": "Really delete room \"{name}\"?", + "loadFailed": "Rooms could not be loaded" + }, + "room": { + "backToDashboard": "Back to Dashboard", + "start": "Start", + "startMeeting": "Start meeting", + "join": "Join", + "end": "End", + "openDetails": "Open details", + "overview": "Overview", + "recordings": "Recordings", + "settings": "Settings", + "participants": "{count} participants", + "copyLink": "Copy link", + "linkCopied": "Link copied!", + "meetingDetails": "Meeting details", + "meetingId": "Meeting ID", + "status": "Status", + "maxParticipants": "Max. participants", + "accessCode": "Access code", + "roomSettings": "Room settings", + "mutedOnJoin": "Muted on join", + "micActiveOnJoin": "Microphone active on join", + "approvalRequired": "Approval required", + "freeJoin": "Free join", + "allModerators": "All as moderator", + "rolesAssigned": "Roles are assigned", + "recordingAllowed": "Recording allowed", + "recordingDisabled": "Recording disabled", + "welcomeMsg": "Welcome message", + "muteOnJoin": "Mute on join", + "requireApproval": "Moderator approval required", + "anyoneCanStart": "Anyone can start the meeting", + "allJoinModerator": "All participants as moderator", + "allowRecording": "Allow recording", + "noAccessCode": "No access code", + "emptyNoCode": "Empty = no code", + "settingsSaved": "Settings saved", + "settingsSaveFailed": "Settings could not be saved", + "meetingStarted": "Meeting started!", + "meetingStartFailed": "Meeting could not be started", + "meetingEnded": "Meeting ended", + "meetingEndFailed": "Meeting could not be ended", + "joinFailed": "Join failed", + "endConfirm": "Really end meeting?", + "enterAccessCode": "Enter access code:", + "notFound": "Room not found" + }, + "recordings": { + "title": "Recordings", + "noRecordings": "No recordings available", + "published": "Published", + "unpublished": "Unpublished", + "presentation": "Presentation", + "deleted": "Recording deleted", + "deleteFailed": "Error deleting recording", + "deleteConfirm": "Really delete recording?", + "publishSuccess": "Recording published", + "unpublishSuccess": "Recording unpublished", + "publishFailed": "Error updating recording", + "hide": "Hide", + "publish": "Publish", + "loadFailed": "Recordings could not be loaded" + }, + "settings": { + "title": "Settings", + "subtitle": "Manage your profile and settings", + "profile": "Profile", + "password": "Password", + "themes": "Themes", + "language": "Language", + "editProfile": "Edit profile", + "avatar": "Profile picture", + "avatarColor": "Avatar color", + "avatarColorHint": "Used as fallback when no image is uploaded.", + "uploadImage": "Upload image", + "removeImage": "Remove image", + "avatarHint": "JPG, PNG, GIF or WebP. Max. 2 MB.", + "avatarUploaded": "Profile picture updated", + "avatarUploadFailed": "Error uploading image", + "avatarRemoved": "Profile picture removed", + "avatarRemoveFailed": "Error removing image", + "avatarInvalidType": "Only image files are allowed", + "avatarTooLarge": "Image must be less than 2 MB", + "changePassword": "Change password", + "currentPassword": "Current password", + "newPassword": "New password", + "confirmNewPassword": "Confirm new password", + "profileSaved": "Profile saved", + "profileSaveFailed": "Error saving profile", + "passwordChanged": "Password changed", + "passwordChangeFailed": "Error changing password", + "passwordMismatch": "Passwords do not match", + "selectLanguage": "Select language" + }, + "themes": { + "selectTheme": "Select theme", + "selectThemeSubtitle": "Choose your preferred color scheme", + "light": "Light", + "dark": "Dark" + }, + "admin": { + "title": "Administration", + "userCount": "{count} user registered | {count} users registered", + "searchUsers": "Search users...", + "user": "User", + "role": "Role", + "rooms": "Rooms", + "registered": "Registered", + "actions": "Actions", + "admin": "Admin", + "makeAdmin": "Make admin", + "makeUser": "Make user", + "resetPassword": "Reset password", + "deleteUser": "Delete", + "createUser": "Create user", + "createUserTitle": "Create new user", + "userCreated": "User created", + "userCreateFailed": "User could not be created", + "newPasswordLabel": "New password", + "resetPasswordTitle": "Reset password", + "noUsersFound": "No users found", + "roleUpdated": "Role updated", + "roleUpdateFailed": "Error updating role", + "userDeleted": "User deleted", + "userDeleteFailed": "Error deleting user", + "passwordReset": "Password reset", + "passwordResetFailed": "Error resetting password", + "deleteUserConfirm": "Really delete user \"{name}\"? All rooms will also be deleted." + } +} diff --git a/src/i18n/index.js b/src/i18n/index.js new file mode 100644 index 0000000..7f43e89 --- /dev/null +++ b/src/i18n/index.js @@ -0,0 +1,42 @@ +import en from './en.json'; +import de from './de.json'; + +export const languages = { + en: { name: 'English', flag: '🇬🇧', translations: en }, + de: { name: 'Deutsch', flag: '🇩🇪', translations: de }, +}; + +export const defaultLanguage = 'en'; + +export function getTranslation(lang, key) { + const keys = key.split('.'); + const translations = languages[lang]?.translations || languages[defaultLanguage].translations; + let value = translations; + for (const k of keys) { + value = value?.[k]; + if (value === undefined) { + // Fallback to default language + let fallback = languages[defaultLanguage].translations; + for (const fk of keys) { + fallback = fallback?.[fk]; + } + return fallback || key; + } + } + return value || key; +} + +export function interpolate(template, params = {}) { + if (!template) return ''; + return template.replace(/\{(\w+)\}/g, (_, key) => { + return params[key] !== undefined ? params[key] : `{${key}}`; + }); +} + +// Handle plural: "singular | plural" format +export function pluralize(template, count) { + if (!template) return ''; + const parts = template.split(' | '); + if (parts.length === 1) return interpolate(template, { count }); + return interpolate(count === 1 ? parts[0] : parts[1], { count }); +} diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..7207448 --- /dev/null +++ b/src/index.css @@ -0,0 +1,486 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + /* ===== DEFAULT LIGHT ===== */ + :root, + [data-theme="light"] { + --bg-primary: #ffffff; + --bg-secondary: #f8fafc; + --bg-tertiary: #f1f5f9; + --text-primary: #0f172a; + --text-secondary: #64748b; + --accent: #3b82f6; + --accent-hover: #2563eb; + --accent-text: #ffffff; + --border: #e2e8f0; + --card-bg: #ffffff; + --input-bg: #ffffff; + --input-border: #cbd5e1; + --nav-bg: #ffffff; + --sidebar-bg: #f8fafc; + --hover-bg: #f1f5f9; + --success: #22c55e; + --warning: #eab308; + --error: #ef4444; + --ring: #3b82f6; + --shadow-color: rgba(0, 0, 0, 0.08); + --gradient-start: #3b82f6; + --gradient-end: #8b5cf6; + } + + /* ===== DEFAULT DARK ===== */ + [data-theme="dark"] { + --bg-primary: #0f172a; + --bg-secondary: #1e293b; + --bg-tertiary: #334155; + --text-primary: #f1f5f9; + --text-secondary: #94a3b8; + --accent: #3b82f6; + --accent-hover: #60a5fa; + --accent-text: #ffffff; + --border: #334155; + --card-bg: #1e293b; + --input-bg: #1e293b; + --input-border: #475569; + --nav-bg: #1e293b; + --sidebar-bg: #0f172a; + --hover-bg: #334155; + --success: #22c55e; + --warning: #eab308; + --error: #ef4444; + --ring: #3b82f6; + --shadow-color: rgba(0, 0, 0, 0.3); + --gradient-start: #3b82f6; + --gradient-end: #8b5cf6; + } + + /* ===== DRACULA ===== */ + [data-theme="dracula"] { + --bg-primary: #282a36; + --bg-secondary: #44475a; + --bg-tertiary: #383a4c; + --text-primary: #f8f8f2; + --text-secondary: #6272a4; + --accent: #bd93f9; + --accent-hover: #caa8fc; + --accent-text: #282a36; + --border: #44475a; + --card-bg: #383a4c; + --input-bg: #44475a; + --input-border: #6272a4; + --nav-bg: #21222c; + --sidebar-bg: #21222c; + --hover-bg: #44475a; + --success: #50fa7b; + --warning: #f1fa8c; + --error: #ff5555; + --ring: #bd93f9; + --shadow-color: rgba(0, 0, 0, 0.4); + --gradient-start: #bd93f9; + --gradient-end: #ff79c6; + } + + /* ===== CATPPUCCIN MOCHA ===== */ + [data-theme="mocha"] { + --bg-primary: #1e1e2e; + --bg-secondary: #313244; + --bg-tertiary: #45475a; + --text-primary: #cdd6f4; + --text-secondary: #a6adc8; + --accent: #89b4fa; + --accent-hover: #b4befe; + --accent-text: #1e1e2e; + --border: #45475a; + --card-bg: #313244; + --input-bg: #313244; + --input-border: #585b70; + --nav-bg: #181825; + --sidebar-bg: #181825; + --hover-bg: #45475a; + --success: #a6e3a1; + --warning: #f9e2af; + --error: #f38ba8; + --ring: #89b4fa; + --shadow-color: rgba(0, 0, 0, 0.4); + --gradient-start: #89b4fa; + --gradient-end: #cba6f7; + } + + /* ===== CATPPUCCIN LATTE (Light) ===== */ + [data-theme="latte"] { + --bg-primary: #eff1f5; + --bg-secondary: #e6e9ef; + --bg-tertiary: #dce0e8; + --text-primary: #4c4f69; + --text-secondary: #6c6f85; + --accent: #1e66f5; + --accent-hover: #2a6ef5; + --accent-text: #ffffff; + --border: #ccd0da; + --card-bg: #e6e9ef; + --input-bg: #eff1f5; + --input-border: #bcc0cc; + --nav-bg: #e6e9ef; + --sidebar-bg: #dce0e8; + --hover-bg: #dce0e8; + --success: #40a02b; + --warning: #df8e1d; + --error: #d20f39; + --ring: #1e66f5; + --shadow-color: rgba(76, 79, 105, 0.1); + --gradient-start: #1e66f5; + --gradient-end: #8839ef; + } + + /* ===== NORD ===== */ + [data-theme="nord"] { + --bg-primary: #2e3440; + --bg-secondary: #3b4252; + --bg-tertiary: #434c5e; + --text-primary: #eceff4; + --text-secondary: #d8dee9; + --accent: #88c0d0; + --accent-hover: #8fbcbb; + --accent-text: #2e3440; + --border: #4c566a; + --card-bg: #3b4252; + --input-bg: #3b4252; + --input-border: #4c566a; + --nav-bg: #2e3440; + --sidebar-bg: #2e3440; + --hover-bg: #434c5e; + --success: #a3be8c; + --warning: #ebcb8b; + --error: #bf616a; + --ring: #88c0d0; + --shadow-color: rgba(0, 0, 0, 0.3); + --gradient-start: #88c0d0; + --gradient-end: #b48ead; + } + + /* ===== TOKYO NIGHT ===== */ + [data-theme="tokyo-night"] { + --bg-primary: #1a1b26; + --bg-secondary: #24283b; + --bg-tertiary: #2f3349; + --text-primary: #a9b1d6; + --text-secondary: #565f89; + --accent: #7aa2f7; + --accent-hover: #89b4fa; + --accent-text: #1a1b26; + --border: #3b4261; + --card-bg: #24283b; + --input-bg: #24283b; + --input-border: #3b4261; + --nav-bg: #16161e; + --sidebar-bg: #16161e; + --hover-bg: #2f3349; + --success: #9ece6a; + --warning: #e0af68; + --error: #f7768e; + --ring: #7aa2f7; + --shadow-color: rgba(0, 0, 0, 0.4); + --gradient-start: #7aa2f7; + --gradient-end: #bb9af7; + } + + /* ===== GRUVBOX DARK ===== */ + [data-theme="gruvbox-dark"] { + --bg-primary: #282828; + --bg-secondary: #3c3836; + --bg-tertiary: #504945; + --text-primary: #ebdbb2; + --text-secondary: #a89984; + --accent: #fe8019; + --accent-hover: #fabd2f; + --accent-text: #282828; + --border: #504945; + --card-bg: #3c3836; + --input-bg: #3c3836; + --input-border: #665c54; + --nav-bg: #1d2021; + --sidebar-bg: #1d2021; + --hover-bg: #504945; + --success: #b8bb26; + --warning: #fabd2f; + --error: #fb4934; + --ring: #fe8019; + --shadow-color: rgba(0, 0, 0, 0.4); + --gradient-start: #fe8019; + --gradient-end: #fabd2f; + } + + /* ===== GRUVBOX LIGHT ===== */ + [data-theme="gruvbox-light"] { + --bg-primary: #fbf1c7; + --bg-secondary: #ebdbb2; + --bg-tertiary: #d5c4a1; + --text-primary: #3c3836; + --text-secondary: #665c54; + --accent: #d65d0e; + --accent-hover: #af3a03; + --accent-text: #fbf1c7; + --border: #d5c4a1; + --card-bg: #ebdbb2; + --input-bg: #fbf1c7; + --input-border: #bdae93; + --nav-bg: #ebdbb2; + --sidebar-bg: #ebdbb2; + --hover-bg: #d5c4a1; + --success: #79740e; + --warning: #b57614; + --error: #cc241d; + --ring: #d65d0e; + --shadow-color: rgba(60, 56, 54, 0.1); + --gradient-start: #d65d0e; + --gradient-end: #b57614; + } + + /* ===== ROSE PINE ===== */ + [data-theme="rose-pine"] { + --bg-primary: #191724; + --bg-secondary: #1f1d2e; + --bg-tertiary: #26233a; + --text-primary: #e0def4; + --text-secondary: #908caa; + --accent: #c4a7e7; + --accent-hover: #ebbcba; + --accent-text: #191724; + --border: #393552; + --card-bg: #1f1d2e; + --input-bg: #1f1d2e; + --input-border: #393552; + --nav-bg: #191724; + --sidebar-bg: #191724; + --hover-bg: #26233a; + --success: #9ccfd8; + --warning: #f6c177; + --error: #eb6f92; + --ring: #c4a7e7; + --shadow-color: rgba(0, 0, 0, 0.4); + --gradient-start: #c4a7e7; + --gradient-end: #eb6f92; + } + + /* ===== ROSE PINE DAWN (Light) ===== */ + [data-theme="rose-pine-dawn"] { + --bg-primary: #faf4ed; + --bg-secondary: #fffaf3; + --bg-tertiary: #f2e9e1; + --text-primary: #575279; + --text-secondary: #797593; + --accent: #907aa9; + --accent-hover: #b4637a; + --accent-text: #faf4ed; + --border: #dfdad9; + --card-bg: #fffaf3; + --input-bg: #faf4ed; + --input-border: #dfdad9; + --nav-bg: #fffaf3; + --sidebar-bg: #f2e9e1; + --hover-bg: #f2e9e1; + --success: #56949f; + --warning: #ea9d34; + --error: #b4637a; + --ring: #907aa9; + --shadow-color: rgba(87, 82, 121, 0.08); + --gradient-start: #907aa9; + --gradient-end: #b4637a; + } + + /* ===== SOLARIZED DARK ===== */ + [data-theme="solarized-dark"] { + --bg-primary: #002b36; + --bg-secondary: #073642; + --bg-tertiary: #0a4050; + --text-primary: #839496; + --text-secondary: #657b83; + --accent: #268bd2; + --accent-hover: #2aa198; + --accent-text: #fdf6e3; + --border: #073642; + --card-bg: #073642; + --input-bg: #073642; + --input-border: #586e75; + --nav-bg: #002b36; + --sidebar-bg: #002b36; + --hover-bg: #0a4050; + --success: #859900; + --warning: #b58900; + --error: #dc322f; + --ring: #268bd2; + --shadow-color: rgba(0, 0, 0, 0.4); + --gradient-start: #268bd2; + --gradient-end: #2aa198; + } + + /* ===== SOLARIZED LIGHT ===== */ + [data-theme="solarized-light"] { + --bg-primary: #fdf6e3; + --bg-secondary: #eee8d5; + --bg-tertiary: #e4ddc8; + --text-primary: #657b83; + --text-secondary: #93a1a1; + --accent: #268bd2; + --accent-hover: #2aa198; + --accent-text: #fdf6e3; + --border: #eee8d5; + --card-bg: #eee8d5; + --input-bg: #fdf6e3; + --input-border: #d3cbb7; + --nav-bg: #eee8d5; + --sidebar-bg: #eee8d5; + --hover-bg: #e4ddc8; + --success: #859900; + --warning: #b58900; + --error: #dc322f; + --ring: #268bd2; + --shadow-color: rgba(101, 123, 131, 0.1); + --gradient-start: #268bd2; + --gradient-end: #2aa198; + } + + /* ===== ONE DARK ===== */ + [data-theme="one-dark"] { + --bg-primary: #282c34; + --bg-secondary: #2c313a; + --bg-tertiary: #353b45; + --text-primary: #abb2bf; + --text-secondary: #636d83; + --accent: #61afef; + --accent-hover: #79bcf5; + --accent-text: #282c34; + --border: #3e4452; + --card-bg: #2c313a; + --input-bg: #2c313a; + --input-border: #3e4452; + --nav-bg: #21252b; + --sidebar-bg: #21252b; + --hover-bg: #353b45; + --success: #98c379; + --warning: #e5c07b; + --error: #e06c75; + --ring: #61afef; + --shadow-color: rgba(0, 0, 0, 0.4); + --gradient-start: #61afef; + --gradient-end: #c678dd; + } + + /* ===== GITHUB DARK ===== */ + [data-theme="github-dark"] { + --bg-primary: #0d1117; + --bg-secondary: #161b22; + --bg-tertiary: #21262d; + --text-primary: #c9d1d9; + --text-secondary: #8b949e; + --accent: #58a6ff; + --accent-hover: #79c0ff; + --accent-text: #0d1117; + --border: #30363d; + --card-bg: #161b22; + --input-bg: #0d1117; + --input-border: #30363d; + --nav-bg: #161b22; + --sidebar-bg: #0d1117; + --hover-bg: #21262d; + --success: #3fb950; + --warning: #d29922; + --error: #f85149; + --ring: #58a6ff; + --shadow-color: rgba(0, 0, 0, 0.4); + --gradient-start: #58a6ff; + --gradient-end: #bc8cff; + } + + * { + scrollbar-width: thin; + scrollbar-color: var(--border) transparent; + } + + *::-webkit-scrollbar { + width: 6px; + height: 6px; + } + + *::-webkit-scrollbar-track { + background: transparent; + } + + *::-webkit-scrollbar-thumb { + background-color: var(--border); + border-radius: 3px; + } +} + +@layer components { + .btn-primary { + @apply inline-flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg font-medium + bg-th-accent text-th-accent-t hover:bg-th-accent-h + transition-all duration-200 ease-out + focus:outline-none focus:ring-2 focus:ring-th-ring focus:ring-offset-2 + disabled:opacity-50 disabled:cursor-not-allowed; + --tw-ring-offset-color: var(--bg-primary); + } + + .btn-secondary { + @apply inline-flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg font-medium + bg-th-bg-s text-th-text border border-th-border + hover:bg-th-hover transition-all duration-200 ease-out + focus:outline-none focus:ring-2 focus:ring-th-ring focus:ring-offset-2 + disabled:opacity-50 disabled:cursor-not-allowed; + --tw-ring-offset-color: var(--bg-primary); + } + + .btn-danger { + @apply inline-flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg font-medium + bg-th-error text-white hover:opacity-90 + transition-all duration-200 ease-out + focus:outline-none focus:ring-2 focus:ring-th-error focus:ring-offset-2 + disabled:opacity-50 disabled:cursor-not-allowed; + --tw-ring-offset-color: var(--bg-primary); + } + + .btn-ghost { + @apply inline-flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg font-medium + text-th-text-s hover:bg-th-hover hover:text-th-text + transition-all duration-200 ease-out + focus:outline-none focus:ring-2 focus:ring-th-ring focus:ring-offset-2 + disabled:opacity-50 disabled:cursor-not-allowed; + --tw-ring-offset-color: var(--bg-primary); + } + + .input-field { + @apply w-full px-4 py-2.5 rounded-lg + bg-th-input text-th-text placeholder-th-text-s + border border-th-input-b + focus:outline-none focus:ring-2 focus:ring-th-ring focus:border-transparent + transition-all duration-200; + } + + .card { + @apply bg-th-card rounded-xl border border-th-border + shadow-th transition-all duration-200; + } + + .card-hover { + @apply card hover:shadow-th-lg cursor-pointer; + } + .card-hover:hover { + border-color: color-mix(in srgb, var(--accent) 30%, transparent); + } +} + +@layer utilities { + .gradient-text { + background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + } + + .gradient-bg { + background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end)); + } +} diff --git a/src/main.jsx b/src/main.jsx new file mode 100644 index 0000000..6006bd0 --- /dev/null +++ b/src/main.jsx @@ -0,0 +1,34 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; +import { Toaster } from 'react-hot-toast'; +import App from './App'; +import { AuthProvider } from './contexts/AuthContext'; +import { ThemeProvider } from './contexts/ThemeContext'; +import { LanguageProvider } from './contexts/LanguageContext'; +import './index.css'; + +ReactDOM.createRoot(document.getElementById('root')).render( + + + + + + + + + + + + , +); diff --git a/src/pages/Admin.jsx b/src/pages/Admin.jsx new file mode 100644 index 0000000..49e6dd1 --- /dev/null +++ b/src/pages/Admin.jsx @@ -0,0 +1,368 @@ +import { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + Users, Shield, Search, Trash2, ChevronDown, Loader2, + MoreVertical, Key, UserCheck, UserX, UserPlus, Mail, Lock, User, +} from 'lucide-react'; +import { useAuth } from '../contexts/AuthContext'; +import { useLanguage } from '../contexts/LanguageContext'; +import api from '../services/api'; +import toast from 'react-hot-toast'; + +export default function Admin() { + const { user } = useAuth(); + const { t, language } = useLanguage(); + const navigate = useNavigate(); + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(true); + const [search, setSearch] = useState(''); + const [openMenu, setOpenMenu] = useState(null); + const [resetPwModal, setResetPwModal] = useState(null); + const [newPassword, setNewPassword] = useState(''); + const [showCreateUser, setShowCreateUser] = useState(false); + const [creatingUser, setCreatingUser] = useState(false); + const [newUser, setNewUser] = useState({ name: '', email: '', password: '', role: 'user' }); + + useEffect(() => { + if (user?.role !== 'admin') { + navigate('/dashboard'); + return; + } + fetchUsers(); + }, [user]); + + const fetchUsers = async () => { + try { + const res = await api.get('/admin/users'); + setUsers(res.data.users); + } catch { + toast.error(t('admin.roleUpdateFailed')); + } finally { + setLoading(false); + } + }; + + const handleRoleChange = async (userId, newRole) => { + try { + await api.put(`/admin/users/${userId}/role`, { role: newRole }); + toast.success(t('admin.roleUpdated')); + fetchUsers(); + } catch (err) { + toast.error(err.response?.data?.error || t('admin.roleUpdateFailed')); + } + setOpenMenu(null); + }; + + const handleDelete = async (userId, userName) => { + if (!confirm(t('admin.deleteUserConfirm', { name: userName }))) return; + try { + await api.delete(`/admin/users/${userId}`); + toast.success(t('admin.userDeleted')); + fetchUsers(); + } catch (err) { + toast.error(err.response?.data?.error || t('admin.userDeleteFailed')); + } + setOpenMenu(null); + }; + + const handleResetPassword = async (e) => { + e.preventDefault(); + try { + await api.put(`/admin/users/${resetPwModal}/password`, { newPassword }); + toast.success(t('admin.passwordReset')); + setResetPwModal(null); + setNewPassword(''); + } catch (err) { + toast.error(err.response?.data?.error || t('admin.passwordResetFailed')); + } + }; + + const handleCreateUser = async (e) => { + e.preventDefault(); + setCreatingUser(true); + try { + await api.post('/admin/users', newUser); + toast.success(t('admin.userCreated')); + setShowCreateUser(false); + setNewUser({ name: '', email: '', password: '', role: 'user' }); + fetchUsers(); + } catch (err) { + toast.error(err.response?.data?.error || t('admin.userCreateFailed')); + } finally { + setCreatingUser(false); + } + }; + + const filteredUsers = users.filter(u => + u.name.toLowerCase().includes(search.toLowerCase()) || + u.email.toLowerCase().includes(search.toLowerCase()) + ); + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+
+
+
+
+ +

{t('admin.title')}

+
+

+ {t('admin.userCount', { count: users.length })} +

+
+ +
+
+ + {/* Search */} +
+
+ + setSearch(e.target.value)} + className="input-field pl-11" + placeholder={t('admin.searchUsers')} + /> +
+
+ + {/* Users table */} +
+
+ + + + + + + + + + + + {filteredUsers.map(u => ( + + + + + + + + ))} + +
+ {t('admin.user')} + + {t('admin.role')} + + {t('admin.rooms')} + + {t('admin.registered')} + + {t('admin.actions')} +
+
+
+ {u.avatar_image ? ( + + ) : ( + u.name[0]?.toUpperCase() + )} +
+
+

{u.name}

+

{u.email}

+
+
+
+ + {u.role === 'admin' ? : } + {u.role === 'admin' ? t('admin.admin') : t('admin.user')} + + + {u.room_count} + + {new Date(u.created_at).toLocaleDateString(language === 'de' ? 'de-DE' : 'en-US')} + +
+ + + {openMenu === u.id && u.id !== user.id && ( + <> +
setOpenMenu(null)} /> +
+ + + +
+ + )} +
+
+
+ + {filteredUsers.length === 0 && ( +
+ +

{t('admin.noUsersFound')}

+
+ )} +
+ + {/* Reset password modal */} + {resetPwModal && ( +
+
setResetPwModal(null)} /> +
+

{t('admin.resetPasswordTitle')}

+
+
+ + setNewPassword(e.target.value)} + className="input-field" + placeholder={t('auth.minPassword')} + required + minLength={6} + /> +
+
+ + +
+
+
+
+ )} + + {/* Create user modal */} + {showCreateUser && ( +
+
setShowCreateUser(false)} /> +
+

{t('admin.createUserTitle')}

+
+
+ +
+ + setNewUser({ ...newUser, name: e.target.value })} + className="input-field pl-11" + placeholder={t('auth.namePlaceholder')} + required + /> +
+
+
+ +
+ + setNewUser({ ...newUser, email: e.target.value })} + className="input-field pl-11" + placeholder={t('auth.emailPlaceholder')} + required + /> +
+
+
+ +
+ + setNewUser({ ...newUser, password: e.target.value })} + className="input-field pl-11" + placeholder={t('auth.minPassword')} + required + minLength={6} + /> +
+
+
+ + +
+
+ + +
+
+
+
+ )} +
+ ); +} diff --git a/src/pages/Dashboard.jsx b/src/pages/Dashboard.jsx new file mode 100644 index 0000000..61fe01b --- /dev/null +++ b/src/pages/Dashboard.jsx @@ -0,0 +1,230 @@ +import { useState, useEffect } from 'react'; +import { Plus, Video, Loader2, LayoutGrid, List } from 'lucide-react'; +import api from '../services/api'; +import { useLanguage } from '../contexts/LanguageContext'; +import RoomCard from '../components/RoomCard'; +import Modal from '../components/Modal'; +import toast from 'react-hot-toast'; + +export default function Dashboard() { + const { t } = useLanguage(); + const [rooms, setRooms] = useState([]); + const [loading, setLoading] = useState(true); + const [showCreate, setShowCreate] = useState(false); + const [viewMode, setViewMode] = useState('grid'); + const [creating, setCreating] = useState(false); + const [newRoom, setNewRoom] = useState({ + name: '', + welcome_message: '', + max_participants: 0, + access_code: '', + mute_on_join: true, + record_meeting: true, + }); + + const fetchRooms = async () => { + try { + const res = await api.get('/rooms'); + setRooms(res.data.rooms); + } catch (err) { + toast.error(t('dashboard.loadFailed')); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchRooms(); + }, []); + + const handleCreate = async (e) => { + e.preventDefault(); + setCreating(true); + try { + await api.post('/rooms', newRoom); + toast.success(t('dashboard.roomCreated')); + setShowCreate(false); + setNewRoom({ + name: '', + welcome_message: t('dashboard.welcomeMessageDefault'), + max_participants: 0, + access_code: '', + mute_on_join: true, + record_meeting: true, + }); + fetchRooms(); + } catch (err) { + toast.error(err.response?.data?.error || t('dashboard.roomCreateFailed')); + } finally { + setCreating(false); + } + }; + + const handleDelete = async (room) => { + if (!confirm(t('dashboard.roomDeleteConfirm', { name: room.name }))) return; + try { + await api.delete(`/rooms/${room.uid}`); + toast.success(t('dashboard.roomDeleted')); + fetchRooms(); + } catch (err) { + toast.error(t('dashboard.roomDeleteFailed')); + } + }; + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+ {/* Header */} +
+
+

{t('dashboard.myRooms')}

+

+ {t('dashboard.roomCount', { count: rooms.length })} +

+
+
+ {/* View toggle */} +
+ + +
+ + +
+
+ + {/* Room grid/list */} + {rooms.length === 0 ? ( +
+
+ ) : ( +
+ {rooms.map(room => ( + + ))} +
+ )} + + {/* Create Room Modal */} + {showCreate && ( + setShowCreate(false)}> +
+
+ + setNewRoom({ ...newRoom, name: e.target.value })} + className="input-field" + placeholder={t('dashboard.roomNamePlaceholder')} + required + /> +
+ +
+ +