diff --git a/.gitignore b/.gitignore index a547bf36..d0adddb5 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ dist-ssr *.njsproj *.sln *.sw? +/db/ diff --git a/docs/docs.md b/docs/docs.md index 269e751e..66e7ff50 100644 --- a/docs/docs.md +++ b/docs/docs.md @@ -95,7 +95,7 @@ For production environments, we recommend running the website via Nginx. See the ```bash cd src/backend node database.cjs -node ssh.cjs +node ssh.ts ``` This will start the WebSocket services on ports 8081 and 8082. diff --git a/package-lock.json b/package-lock.json index 31fb92eb..7af026e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,14 @@ "version": "0.0.0", "dependencies": { "@hookform/resolvers": "^5.1.1", + "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-popover": "^1.1.14", + "@radix-ui/react-scroll-area": "^1.2.9", "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slider": "^1.3.5", @@ -22,14 +25,24 @@ "@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-tooltip": "^1.2.7", "@tailwindcss/vite": "^4.1.11", + "@types/bcryptjs": "^2.4.6", "@xterm/addon-attach": "^0.11.0", "@xterm/addon-clipboard": "^0.1.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-unicode11": "^0.8.0", "@xterm/xterm": "^5.5.0", + "axios": "^1.10.0", + "bcryptjs": "^3.0.2", + "better-sqlite3": "^12.2.0", + "chalk": "^4.1.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cors": "^2.8.5", + "drizzle-orm": "^0.44.3", + "express": "^5.1.0", + "jsonwebtoken": "^9.0.2", "lucide-react": "^0.525.0", + "nanoid": "^5.1.5", "react": "^19.1.0", "react-dom": "^19.1.0", "react-hook-form": "^7.60.0", @@ -43,14 +56,22 @@ }, "devDependencies": { "@eslint/js": "^9.30.1", + "@types/better-sqlite3": "^7.6.13", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.3", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.0.13", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", + "@types/ssh2": "^1.15.5", + "@types/ws": "^8.18.1", "@vitejs/plugin-react-swc": "^3.10.2", + "autoprefixer": "^10.4.21", "eslint": "^9.30.1", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.3.0", + "ts-node": "^10.9.2", "tw-animate-css": "^1.3.5", "typescript": "~5.8.3", "typescript-eslint": "^8.35.1", @@ -70,6 +91,30 @@ "node": ">=6.0.0" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.6", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", @@ -853,6 +898,37 @@ "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", "license": "MIT" }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.11.tgz", + "integrity": "sha512-l3W5D54emV2ues7jjeG1xcyN7S3jnK3zE2zHqgn0CmMsy9lNJwmgcrmaxS+7ipw15FAivzKNzH3d5EcGoFKw0A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collapsible": "1.1.11", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", @@ -933,6 +1009,36 @@ } } }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.11.tgz", + "integrity": "sha512-2qrRsVGSCYasSz1RFOorXwl0H7g7J1frQtgpQgYrt+MOidtPAINHn9CPovQXb83r8ahapdx3Tu0fa/pdFFSdPg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", @@ -1217,6 +1323,43 @@ } } }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.14.tgz", + "integrity": "sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", @@ -1351,6 +1494,37 @@ } } }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.9.tgz", + "integrity": "sha512-YSjEfBXnhUELsO2VzjdtYYD4CfQjvao+lhhrX5XsHD7/cyUNzljF1FHEbgTPN7LH2MClfwRMIsYlqTYpKTTe2A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-select": { "version": "2.2.5", "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.5.tgz", @@ -2505,12 +2679,119 @@ "vite": "^5.2.0 || ^6 || ^7" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "license": "MIT" + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "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==", "license": "MIT" }, + "node_modules/@types/express": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", + "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz", + "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2518,6 +2799,31 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.0.13", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.13.tgz", @@ -2528,6 +2834,20 @@ "undici-types": "~7.8.0" } }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.1.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", @@ -2548,6 +2868,66 @@ "@types/react": "^19.0.0" } }, + "node_modules/@types/send": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/ssh2": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.5.tgz", + "integrity": "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18" + } + }, + "node_modules/@types/ssh2/node_modules/@types/node": { + "version": "18.19.119", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.119.tgz", + "integrity": "sha512-d0F6m9itIPaKnrvEMlzE48UjwZaAnFW7Jwibacw9MNdqadjKNpUm9tfJYDwmShJmgqcoqYUX3EMKO1+RWiuuNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/ssh2/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.37.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz", @@ -2865,6 +3245,19 @@ "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", "license": "MIT" }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -2888,6 +3281,19 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2909,7 +3315,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -2921,6 +3326,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2949,6 +3361,61 @@ "safer-buffer": "~2.1.0" } }, + "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.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "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.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "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.10.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2956,6 +3423,26 @@ "dev": true, "license": "MIT" }, + "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/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -2965,6 +3452,69 @@ "tweetnacl": "^0.14.3" } }, + "node_modules/bcryptjs": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", + "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/better-sqlite3": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.2.0.tgz", + "integrity": "sha512-eGbYq2CT+tos1fBwLQ/tkBt9J5M3JEHjku4hbvQUePCckkvVf14xWj+1m7dGoK81M/fOjFT7yM9UMeKT/+vFLQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x" + } + }, + "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": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -2989,6 +3539,69 @@ "node": ">=8" } }, + "node_modules/browserslist": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "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": { + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "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/buildcheck": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", @@ -2998,6 +3611,44 @@ "node": ">=10.0.0" } }, + "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/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3008,11 +3659,31 @@ "node": ">=6" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001727", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", + "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "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==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -3059,7 +3730,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -3072,9 +3742,20 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "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/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3082,6 +3763,58 @@ "dev": true, "license": "MIT" }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "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/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.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cpu-features": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", @@ -3096,6 +3829,13 @@ "node": ">=10.0.0" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3122,7 +3862,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3136,6 +3875,30 @@ } } }, + "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/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3143,6 +3906,24 @@ "dev": true, "license": "MIT" }, + "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/detect-libc": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", @@ -3158,6 +3939,195 @@ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/drizzle-orm": { + "version": "0.44.3", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.44.3.tgz", + "integrity": "sha512-8nIiYQxOpgUicEL04YFojJmvC4DNO4KoyXsEIqN44+g6gNBr6hmVpWk3uyAt4CaTiRGDwoU+alfqNNeonLAFOQ==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=4", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1.13", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/sql.js": "*", + "@upstash/redis": ">=1.34.7", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "gel": ">=2", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "gel": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, + "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.187", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.187.tgz", + "integrity": "sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA==", + "dev": true, + "license": "ISC" + }, + "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/enhanced-resolve": { "version": "5.18.2", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", @@ -3171,6 +4141,51 @@ "node": ">=10.13.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.25.6", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", @@ -3212,6 +4227,22 @@ "@esbuild/win32-x64": "0.25.6" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "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/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -3403,6 +4434,66 @@ "node": ">=0.10.0" } }, + "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": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3477,6 +4568,12 @@ "node": ">=16.0.0" } }, + "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", @@ -3490,6 +4587,23 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -3528,6 +4642,101 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "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.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "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/form-data/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/form-data/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/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": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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", @@ -3542,6 +4751,39 @@ "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/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-nonce": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", @@ -3551,6 +4793,25 @@ "node": ">=6" } }, + "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", @@ -3577,6 +4838,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -3594,12 +4867,107 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "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.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "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/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3637,6 +5005,27 @@ "node": ">=0.8.19" } }, + "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-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3670,6 +5059,12 @@ "node": ">=0.12.0" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3726,6 +5121,49 @@ "dev": true, "license": "MIT" }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "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/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "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": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3994,6 +5432,42 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4001,6 +5475,12 @@ "dev": true, "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/lucide-react": { "version": "0.525.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.525.0.tgz", @@ -4019,6 +5499,43 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "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": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "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", @@ -4043,6 +5560,39 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.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/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4056,6 +5606,15 @@ "node": "*" } }, + "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/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -4092,11 +5651,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "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==", - "dev": true, "license": "MIT" }, "node_modules/nan": { @@ -4107,9 +5671,9 @@ "optional": true }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz", + "integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==", "funding": [ { "type": "github", @@ -4118,12 +5682,18 @@ ], "license": "MIT", "bin": { - "nanoid": "bin/nanoid.cjs" + "nanoid": "bin/nanoid.js" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": "^18 || >=20" } }, + "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/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -4131,6 +5701,86 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.75.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", + "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "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-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/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -4194,6 +5844,15 @@ "node": ">=6" } }, + "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-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4214,6 +5873,15 @@ "node": ">=8" } }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -4261,6 +5929,57 @@ "node": "^10 || ^12 || >=14" } }, + "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/postcss/node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "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/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -4271,6 +5990,35 @@ "node": ">= 0.8.0" } }, + "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/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4281,6 +6029,21 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "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", @@ -4302,6 +6065,54 @@ ], "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": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "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/rc/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/react": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", @@ -4427,6 +6238,20 @@ "@xterm/xterm": "^5.5.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/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -4487,6 +6312,22 @@ "fsevents": "~2.3.2" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -4511,6 +6352,26 @@ "queue-microtask": "^1.2.2" } }, + "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", @@ -4527,7 +6388,6 @@ "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -4536,6 +6396,49 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "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/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -4559,6 +6462,123 @@ "node": ">=8" } }, + "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", @@ -4585,6 +6605,24 @@ "nan": "^2.20.0" } }, + "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/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -4602,7 +6640,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -4653,6 +6690,40 @@ "node": ">=18" } }, + "node_modules/tar-fs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", + "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/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/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/tinyglobby": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", @@ -4708,6 +6779,15 @@ "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/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -4721,12 +6801,68 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "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/tw-animate-css": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.5.tgz", @@ -4756,6 +6892,20 @@ "node": ">= 0.8.0" } }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -4801,6 +6951,46 @@ "devOptional": true, "license": "MIT" }, + "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.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "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/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -4863,6 +7053,28 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.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/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "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": "7.0.4", "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.4.tgz", @@ -4989,6 +7201,12 @@ "node": ">=0.10.0" } }, + "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/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", @@ -5019,6 +7237,16 @@ "node": ">=18" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 24a2ca66..04c48aac 100644 --- a/package.json +++ b/package.json @@ -6,16 +6,21 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", + "build:backend": "tsc -p tsconfig.node.json", + "dev:backend": "tsc -p tsconfig.node.json && node ./dist/backend/starter.js", "lint": "eslint .", "preview": "vite preview" }, "dependencies": { "@hookform/resolvers": "^5.1.1", + "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-popover": "^1.1.14", + "@radix-ui/react-scroll-area": "^1.2.9", "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slider": "^1.3.5", @@ -24,14 +29,24 @@ "@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-tooltip": "^1.2.7", "@tailwindcss/vite": "^4.1.11", + "@types/bcryptjs": "^2.4.6", "@xterm/addon-attach": "^0.11.0", "@xterm/addon-clipboard": "^0.1.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-unicode11": "^0.8.0", "@xterm/xterm": "^5.5.0", + "axios": "^1.10.0", + "bcryptjs": "^3.0.2", + "better-sqlite3": "^12.2.0", + "chalk": "^4.1.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cors": "^2.8.5", + "drizzle-orm": "^0.44.3", + "express": "^5.1.0", + "jsonwebtoken": "^9.0.2", "lucide-react": "^0.525.0", + "nanoid": "^5.1.5", "react": "^19.1.0", "react-dom": "^19.1.0", "react-hook-form": "^7.60.0", @@ -45,14 +60,22 @@ }, "devDependencies": { "@eslint/js": "^9.30.1", + "@types/better-sqlite3": "^7.6.13", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.3", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.0.13", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", + "@types/ssh2": "^1.15.5", + "@types/ws": "^8.18.1", "@vitejs/plugin-react-swc": "^3.10.2", + "autoprefixer": "^10.4.21", "eslint": "^9.30.1", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.3.0", + "ts-node": "^10.9.2", "tw-animate-css": "^1.3.5", "typescript": "~5.8.3", "typescript-eslint": "^8.35.1", diff --git a/src/apps/Homepage/Homepage.tsx b/src/apps/Homepage/Homepage.tsx index 279fcbeb..d4fe427b 100644 --- a/src/apps/Homepage/Homepage.tsx +++ b/src/apps/Homepage/Homepage.tsx @@ -1,16 +1,25 @@ -import {HomepageSidebar} from "@/apps/Homepage/HomepageSidebar.tsx"; -import React from "react"; +import { HomepageSidebar } from "@/apps/Homepage/HomepageSidebar.tsx"; +import React, { useState } from "react"; +import { HomepageAuth } from "@/apps/Homepage/HomepageAuth.tsx"; interface HomepageProps { onSelectView: (view: string) => void; } export function Homepage({ onSelectView }: HomepageProps): React.ReactElement { + const [loggedIn, setLoggedIn] = useState(false); + const [isAdmin, setIsAdmin] = useState(false); + const [username, setUsername] = useState(null); return ( -
- +
+ +
+
+ +
- ) + ); } \ No newline at end of file diff --git a/src/apps/Homepage/HomepageAuth.tsx b/src/apps/Homepage/HomepageAuth.tsx new file mode 100644 index 00000000..f07747e5 --- /dev/null +++ b/src/apps/Homepage/HomepageAuth.tsx @@ -0,0 +1,287 @@ +import React, { useState, useEffect } from "react"; +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert"; +import axios from "axios"; + +function setCookie(name: string, value: string, days = 7) { + const expires = new Date(Date.now() + days * 864e5).toUTCString(); + document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/`; +} +function getCookie(name: string) { + return document.cookie.split('; ').reduce((r, v) => { + const parts = v.split('='); + return parts[0] === name ? decodeURIComponent(parts[1]) : r; + }, ""); +} + +const apiBase = + typeof window !== "undefined" && window.location.hostname === "localhost" + ? "http://localhost:8081/users" + : "/users"; + +const API = axios.create({ + baseURL: apiBase, +}); + +interface HomepageAuthProps extends React.ComponentProps<"div"> { + setLoggedIn: (loggedIn: boolean) => void; + setIsAdmin: (isAdmin: boolean) => void; + setUsername: (username: string | null) => void; +} + +export function HomepageAuth({ className, setLoggedIn, setIsAdmin, setUsername, ...props }: HomepageAuthProps) { + const [tab, setTab] = useState<"login" | "signup">("login"); + const [localUsername, setLocalUsername] = useState(""); + const [password, setPassword] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [internalLoggedIn, setInternalLoggedIn] = useState(false); + const [firstUser, setFirstUser] = useState(false); + const [dbError, setDbError] = useState(null); + const [registrationAllowed, setRegistrationAllowed] = useState(true); + useEffect(() => { + API.get("/registration-allowed").then(res => { + setRegistrationAllowed(res.data.allowed); + }); + }, []); + + useEffect(() => { + API.get("/count").then(res => { + if (res.data.count === 0) { + setFirstUser(true); + setTab("signup"); + } else { + setFirstUser(false); + } + setDbError(null); + }).catch(() => { + setFirstUser(true); + setTab("signup"); + setDbError("Could not connect to the database. Please try again later."); + }); + }, []); + + useEffect(() => { + const jwt = getCookie("jwt"); + if (jwt) { + setLoading(true); + Promise.all([ + API.get("/me", { headers: { Authorization: `Bearer ${jwt}` } }), + API.get("/db-health") + ]) + .then(([meRes]) => { + setInternalLoggedIn(true); + setLoggedIn(true); + setIsAdmin(!!meRes.data.is_admin); + setUsername(meRes.data.username || null); + setDbError(null); + }) + .catch((err) => { + setInternalLoggedIn(false); + setLoggedIn(false); + setIsAdmin(false); + setUsername(null); + setCookie("jwt", "", -1); + if (err?.response?.data?.error?.includes("Database")) { + setDbError("Could not connect to the database. Please try again later."); + } else { + setDbError(null); + } + }) + .finally(() => setLoading(false)); + } else { + setInternalLoggedIn(false); + setLoggedIn(false); + setIsAdmin(false); + setUsername(null); + } + }, [setLoggedIn, setIsAdmin, setUsername]); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(null); + setLoading(true); + try { + let res, meRes; + if (tab === "login") { + res = await API.post("/get", { username: localUsername, password }); + } else { + await API.post("/create", { username: localUsername, password }); + res = await API.post("/get", { username: localUsername, password }); + } + setCookie("jwt", res.data.token); + [meRes] = await Promise.all([ + API.get("/me", { headers: { Authorization: `Bearer ${res.data.token}` } }), + API.get("/db-health") + ]); + setInternalLoggedIn(true); + setLoggedIn(true); + setIsAdmin(!!meRes.data.is_admin); + setUsername(meRes.data.username || null); + setDbError(null); + } catch (err: any) { + setError(err?.response?.data?.error || "Unknown error"); + setInternalLoggedIn(false); + setLoggedIn(false); + setIsAdmin(false); + setUsername(null); + setCookie("jwt", "", -1); + if (err?.response?.data?.error?.includes("Database")) { + setDbError("Could not connect to the database. Please try again later."); + } else { + setDbError(null); + } + } finally { + setLoading(false); + } + } + + const Spinner = ( + + + + + ); + + return ( +
+
+ {dbError && ( + + Error + {dbError} + + )} + {firstUser && !dbError && !internalLoggedIn && ( + + First User + + You are the first user and will be made an admin. You can view admin settings in the sidebar user dropdown. + + + )} + {!registrationAllowed && !internalLoggedIn && ( + + Registration Disabled + + New account registration is currently disabled by an admin. Please log in or contact an administrator. + + + )} + {(internalLoggedIn || (loading && getCookie("jwt"))) && ( +
+
+ + Logged in! + + You are logged in! Use the sidebar to access all tools. + + + +
+ +
+ +
+ +
+
+
+ )} + {(!internalLoggedIn && (!loading || !getCookie("jwt"))) && ( + <> +
+ + +
+
+

+ {tab === "login" ? "Login to your account" : "Create a new account"} +

+
+
+
+ + setLocalUsername(e.target.value)} + disabled={loading || internalLoggedIn} + /> +
+
+ + setPassword(e.target.value)} disabled={loading || internalLoggedIn} /> +
+ +
+ + )} + {error && ( + + Error + {error} + + )} +
+
+ ); +} \ No newline at end of file diff --git a/src/apps/Homepage/HomepageSidebar.tsx b/src/apps/Homepage/HomepageSidebar.tsx index e14d2412..36deb0f9 100644 --- a/src/apps/Homepage/HomepageSidebar.tsx +++ b/src/apps/Homepage/HomepageSidebar.tsx @@ -3,12 +3,12 @@ import { Computer, Server, File, - Hammer + Hammer, ChevronUp, User2 } from "lucide-react"; import { Sidebar, - SidebarContent, + SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, @@ -20,12 +20,66 @@ import { import { Separator, } from "@/components/ui/separator.tsx" +import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from "@radix-ui/react-dropdown-menu"; +import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger, SheetClose } from "@/components/ui/sheet"; +import {Checkbox} from "@/components/ui/checkbox.tsx"; +import axios from "axios"; +import {Button} from "@/components/ui/button.tsx"; interface SidebarProps { onSelectView: (view: string) => void; + disabled?: boolean; + isAdmin?: boolean; + username?: string | null; } -export function HomepageSidebar({ onSelectView }: SidebarProps): React.ReactElement { +function handleLogout() { + document.cookie = 'jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; + window.location.reload(); +} + +function getCookie(name: string) { + return document.cookie.split('; ').reduce((r, v) => { + const parts = v.split('='); + return parts[0] === name ? decodeURIComponent(parts[1]) : r; + }, ""); +} + +const apiBase = + typeof window !== "undefined" && window.location.hostname === "localhost" + ? "http://localhost:8081/users" + : "/users"; + +const API = axios.create({ + baseURL: apiBase, +}); + +export function HomepageSidebar({ onSelectView, disabled, isAdmin, username }: SidebarProps): React.ReactElement { + const [adminSheetOpen, setAdminSheetOpen] = React.useState(false); + const [allowRegistration, setAllowRegistration] = React.useState(true); + const [regLoading, setRegLoading] = React.useState(false); + React.useEffect(() => { + if (adminSheetOpen) { + API.get("/registration-allowed").then(res => { + setAllowRegistration(res.data.allowed); + }); + } + }, [adminSheetOpen]); + const handleToggle = async (checked: boolean) => { + setRegLoading(true); + const jwt = getCookie("jwt"); + try { + await API.patch( + "/registration-allowed", + { allowed: checked }, + { headers: { Authorization: `Bearer ${jwt}` } } + ); + setAllowRegistration(checked); + } catch (e) { + } finally { + setRegLoading(false); + } + }; return ( @@ -37,45 +91,90 @@ export function HomepageSidebar({ onSelectView }: SidebarProps): React.ReactElem - - {/* Sidebar Items */} - onSelectView("ssh")}> -
- - {"SSH"} -
+ onSelectView("ssh")} disabled={disabled}> + + SSH
- onSelectView("ssh_tunnel")}> -
- - {"SSH Tunnel"} -
+ onSelectView("ssh_tunnel")} disabled={disabled}> + + SSH Tunnel
- onSelectView("config_editor")}> -
- - {"Config Editor"} -
+ onSelectView("config_editor")} disabled={disabled}> + + Config Editor
- onSelectView("tools")}> -
- - {"Tools"} -
+ onSelectView("tools")} disabled={disabled}> + + Tools
-
+ + + + + + + + {username ? username : 'Signed out'} + + + + + {isAdmin && ( + setAdminSheetOpen(true)}> + Admin Settings + + )} + + Sign out + + + + + + + {/* Admin Settings Sheet (always rendered, only openable if isAdmin) */} + {isAdmin && ( + + + + Admin Settings + +
+ +
+ + + + + + +
+
+ )}
) diff --git a/src/apps/SSH/SSH.tsx b/src/apps/SSH/SSH.tsx index f10fccfb..fac17ad8 100644 --- a/src/apps/SSH/SSH.tsx +++ b/src/apps/SSH/SSH.tsx @@ -1,8 +1,9 @@ -import React, { useState, useRef } from "react"; +import React, { useState, useRef, useEffect } from "react"; import { SSHSidebar } from "@/apps/SSH/SSHSidebar.tsx"; import { SSHTerminal } from "./SSHTerminal.tsx"; import { SSHTopbar } from "@/apps/SSH/SSHTopbar.tsx"; import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@/components/ui/resizable'; +import * as ResizablePrimitive from "react-resizable-panels"; interface ConfigEditorProps { onSelectView: (view: string) => void; @@ -15,37 +16,20 @@ type Tab = { terminalRef: React.RefObject; }; -function TerminalOverlay({ tabId, splitScreen }: { tabId: number, splitScreen: boolean }) { - React.useEffect(() => { - const el = document.getElementById(`terminal-container-${tabId}`); - if (el) { - el.style.opacity = '1'; - el.style.zIndex = '10'; - el.style.left = splitScreen ? '8px' : '0px'; - el.style.width = splitScreen ? 'calc(100% - 8px)' : '100%'; - } - return () => { - if (el) { - el.style.opacity = '0'; - el.style.zIndex = '1'; - } - }; - }, [tabId, splitScreen]); - return
; -} - export function SSH({ onSelectView }: ConfigEditorProps): React.ReactElement { const [allTabs, setAllTabs] = useState([]); const [currentTab, setCurrentTab] = useState(null); const [allSplitScreenTab, setAllSplitScreenTab] = useState([]); const nextTabId = useRef(1); - const [splitKey, setSplitKey] = useState(0); + + const [panelRects, setPanelRects] = useState>({}); + const panelRefs = useRef>({}); + const panelGroupRefs = useRef<{ [key: string]: any }>({}); const setActiveTab = (tabId: number) => { setCurrentTab(tabId); }; - // Helper to fit all visible terminals const fitVisibleTerminals = () => { allTabs.forEach((terminal) => { const isVisible = @@ -57,7 +41,6 @@ export function SSH({ onSelectView }: ConfigEditorProps): React.ReactElement { }); }; - // Wrap setSplitScreenTab to fit before and after const setSplitScreenTab = (tabId: number) => { fitVisibleTerminals(); setAllSplitScreenTab((prev) => { @@ -75,7 +58,6 @@ export function SSH({ onSelectView }: ConfigEditorProps): React.ReactElement { }; const setCloseTab = (tabId: number) => { - // Find the tab and call disconnect on its terminal const tab = allTabs.find((t) => t.id === tabId); if (tab && tab.terminalRef && tab.terminalRef.current && typeof tab.terminalRef.current.disconnect === "function") { tab.terminalRef.current.disconnect(); @@ -88,290 +70,312 @@ export function SSH({ onSelectView }: ConfigEditorProps): React.ReactElement { } }; - // Render all terminals absolutely positioned, always mounted - const renderAllTerminals = () => ( -
- {allTabs.map((tab) => ( -
- -
- ))} -
- ); + const updatePanelRects = () => { + setPanelRects((prev) => { + const next: Record = { ...prev }; + Object.entries(panelRefs.current).forEach(([id, ref]) => { + if (ref) { + next[id] = ref.getBoundingClientRect(); + } + }); + return next; + }); + }; - // Helper to show a terminal in a panel by toggling zIndex/opacity - const showTerminal = (tab: Tab, splitScreen: boolean) => ( - - ); + useEffect(() => { + const observers: ResizeObserver[] = []; + Object.entries(panelRefs.current).forEach(([id, ref]) => { + if (ref) { + const observer = new ResizeObserver(() => updatePanelRects()); + observer.observe(ref); + observers.push(observer); + } + }); + updatePanelRects(); + return () => { + observers.forEach((observer) => observer.disconnect()); + }; + }, [allSplitScreenTab, currentTab, allTabs.length]); - const renderTerminals = () => { - if (allSplitScreenTab.length === 0) { - return ( - <> - {allTabs.map((tab) => ( -
- -
- ))} - - ); - } - - // Split screen logic + const renderAllTerminals = () => { + const layoutStyles: Record = {}; const splitTabs = allTabs.filter((tab) => allSplitScreenTab.includes(tab.id)); const mainTab = allTabs.find((tab) => tab.id === currentTab); - const layoutTabs = [mainTab, ...splitTabs.filter((t) => t && t.id !== currentTab)].filter((t): t is Tab => !!t); + const layoutTabs = [mainTab, ...splitTabs.filter((t) => t && t.id !== (mainTab && mainTab.id))].filter((t): t is Tab => !!t); + if (allSplitScreenTab.length === 0 && mainTab) { + layoutStyles[mainTab.id] = { + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: '100%', + zIndex: 20, + display: 'block', + pointerEvents: 'auto', + }; + } else { + layoutTabs.forEach((tab) => { + const rect = panelRects[String(tab.id)]; + if (rect) { + const parentRect = panelRefs.current['parent']?.getBoundingClientRect(); + let top = rect.top, left = rect.left, width = rect.width, height = rect.height; + if (parentRect) { + top = rect.top - parentRect.top; + left = rect.left - parentRect.left; + } + layoutStyles[tab.id] = { + position: 'absolute', + top: top + 28, + left, + width, + height: height - 28, + zIndex: 20, + display: 'block', + pointerEvents: 'auto', + }; + } + }); + } + return ( +
{ panelRefs.current['parent'] = el; }} style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', zIndex: 1, overflow: 'hidden' }}> + {allTabs.map((tab) => { + const style = layoutStyles[tab.id] + ? { ...layoutStyles[tab.id], overflow: 'hidden' } + : { display: 'none', overflow: 'hidden' }; + const isVisible = !!layoutStyles[tab.id]; + return ( +
+ 0} + /> +
+ ); + })} +
+ ); + }; + + const renderSplitOverlays = () => { + const splitTabs = allTabs.filter((tab) => allSplitScreenTab.includes(tab.id)); + const mainTab = allTabs.find((tab) => tab.id === currentTab); + const layoutTabs = [mainTab, ...splitTabs.filter((t) => t && t.id !== (mainTab && mainTab.id))].filter((t): t is Tab => !!t); + if (allSplitScreenTab.length === 0) return null; - // 2 splits: horizontal if (layoutTabs.length === 2) { const [tab1, tab2] = layoutTabs; return ( - - -
-
{tab1.title}
-
- {showTerminal(tab1, true)} +
+ { panelGroupRefs.current['main'] = el; }} + direction="horizontal" + className="h-full w-full" + id="main-horizontal" + > + +
{ panelRefs.current[String(tab1.id)] = el; }} style={{height: '100%', width: '100%', display: 'flex', flexDirection: 'column', background: 'transparent', margin: 0, padding: 0, position: 'relative'}}> +
{tab1.title}
-
- - - -
-
{tab2.title}
-
- {showTerminal(tab2, true)} + + + +
{ panelRefs.current[String(tab2.id)] = el; }} style={{height: '100%', width: '100%', display: 'flex', flexDirection: 'column', background: 'transparent', margin: 0, padding: 0, position: 'relative'}}> +
{tab2.title}
-
- - + + +
); } - - // 3 splits: vertical group (top: horizontal with 2, bottom: single) if (layoutTabs.length === 3) { return ( - - - - {/* Left/top panel */} - -
-
{layoutTabs[0].title}
-
- {showTerminal(layoutTabs[0], true)} +
+ { panelGroupRefs.current['main'] = el; }} + direction="vertical" + className="h-full w-full" + id="main-vertical" + > + + { panelGroupRefs.current['top'] = el; }} direction="horizontal" className="h-full w-full" id="top-horizontal"> + +
{ panelRefs.current[String(layoutTabs[0].id)] = el; }} style={{height: '100%', width: '100%', display: 'flex', flexDirection: 'column', background: 'transparent', margin: 0, padding: 0, position: 'relative'}}> +
{layoutTabs[0].title}
-
- - - {/* Right/top panel (no reset button here) */} - -
-
- {layoutTabs[1].title} + + + +
{ panelRefs.current[String(layoutTabs[1].id)] = el; }} style={{height: '100%', width: '100%', display: 'flex', flexDirection: 'column', background: 'transparent', margin: 0, padding: 0, position: 'relative'}}> +
{layoutTabs[1].title}
-
- {showTerminal(layoutTabs[1], true)} -
-
- - - - - -
-
{layoutTabs[2].title}
-
- {showTerminal(layoutTabs[2], true)} + + + + + +
{ panelRefs.current[String(layoutTabs[2].id)] = el; }} style={{height: '100%', width: '100%', display: 'flex', flexDirection: 'column', background: 'transparent', margin: 0, padding: 0, position: 'relative'}}> +
{layoutTabs[2].title}
-
- - + + +
); } - - // 4 splits: 2x2 grid (vertical group with two horizontal groups) if (layoutTabs.length === 4) { return ( - - - - -
-
{layoutTabs[0].title}
-
- {showTerminal(layoutTabs[0], true)} +
+ { panelGroupRefs.current['main'] = el; }} + direction="vertical" + className="h-full w-full" + id="main-vertical" + > + + { panelGroupRefs.current['top'] = el; }} direction="horizontal" className="h-full w-full" id="top-horizontal"> + +
{ panelRefs.current[String(layoutTabs[0].id)] = el; }} style={{height: '100%', width: '100%', display: 'flex', flexDirection: 'column', background: 'transparent', margin: 0, padding: 0, position: 'relative'}}> +
{layoutTabs[0].title}
-
- - - -
-
{layoutTabs[1].title}
-
- {showTerminal(layoutTabs[1], true)} + + + +
{ panelRefs.current[String(layoutTabs[1].id)] = el; }} style={{height: '100%', width: '100%', display: 'flex', flexDirection: 'column', background: 'transparent', margin: 0, padding: 0, position: 'relative'}}> +
{layoutTabs[1].title}
-
- - - - - - - -
-
{layoutTabs[2].title}
-
- {showTerminal(layoutTabs[2], true)} + + + + + + { panelGroupRefs.current['bottom'] = el; }} direction="horizontal" className="h-full w-full" id="bottom-horizontal"> + +
{ panelRefs.current[String(layoutTabs[2].id)] = el; }} style={{height: '100%', width: '100%', display: 'flex', flexDirection: 'column', background: 'transparent', margin: 0, padding: 0, position: 'relative'}}> +
{layoutTabs[2].title}
-
- - - -
-
{layoutTabs[3].title}
-
- {showTerminal(layoutTabs[3], true)} + + + +
{ panelRefs.current[String(layoutTabs[3].id)] = el; }} style={{height: '100%', width: '100%', display: 'flex', flexDirection: 'column', background: 'transparent', margin: 0, padding: 0, position: 'relative'}}> +
{layoutTabs[3].title}
-
- - - - + + + + +
); } return null; @@ -392,16 +396,31 @@ export function SSH({ onSelectView }: ConfigEditorProps): React.ReactElement { setAllSplitScreenTab((prev) => prev.filter((tid) => tid !== id)); }; - const getLayoutStyle = () => { - if (allSplitScreenTab.length === 0) { - return "flex flex-col h-full w-full"; - } else if (allSplitScreenTab.length === 1) { - return "grid grid-cols-2 h-full w-full"; - } else if (allSplitScreenTab.length === 2) { - return "grid grid-cols-2 grid-rows-2 h-full w-full"; - } else { - return "grid grid-cols-2 grid-rows-2 h-full w-full"; + const getUniqueTabTitle = (baseTitle: string) => { + let title = baseTitle; + let count = 1; + const existingTitles = allTabs.map(t => t.title); + while (existingTitles.includes(title)) { + title = `${baseTitle} (${count})`; + count++; } + return title; + }; + + const onHostConnect = (hostConfig: any) => { + const baseTitle = hostConfig.name?.trim() ? hostConfig.name : `${hostConfig.ip || "Host"}:${hostConfig.port || 22}`; + const title = getUniqueTabTitle(baseTitle); + const terminalRef = React.createRef(); + const id = nextTabId.current++; + const newTab: Tab = { + id, + title, + hostConfig, + terminalRef, + }; + setAllTabs((prev) => [...prev, newTab]); + setCurrentTab(id); + setAllSplitScreenTab((prev) => prev.filter((tid) => tid !== id)); }; return ( @@ -411,6 +430,7 @@ export function SSH({ onSelectView }: ConfigEditorProps): React.ReactElement {
{/* Main area: fills the rest */} @@ -439,7 +459,31 @@ export function SSH({ onSelectView }: ConfigEditorProps): React.ReactElement {
{/* Split area below the topbar */}
- {/* Absolutely render all terminals for persistence */} + {/* Show alert when no terminals are rendered */} + {allTabs.length === 0 && ( +
+
+ Welcome to Termix SSH +
+
+ Click on any host title in the sidebar to open a terminal connection, or use the "Add Host" button to create a new connection. +
+
+ )} + {/* Absolutely render all terminals for persistence and layout */} {allSplitScreenTab.length > 0 && (
)} {renderAllTerminals()} - {renderTerminals()} + {renderSplitOverlays()}
diff --git a/src/apps/SSH/SSHSidebar.tsx b/src/apps/SSH/SSHSidebar.tsx index b90388eb..cade8ce9 100644 --- a/src/apps/SSH/SSHSidebar.tsx +++ b/src/apps/SSH/SSHSidebar.tsx @@ -1,8 +1,10 @@ -import React from 'react'; +import React, { useState, useMemo } from 'react'; +import { useForm, Controller } from "react-hook-form"; import { CornerDownLeft, - Plus + Plus, + MoreVertical } from "lucide-react" import { @@ -26,6 +28,7 @@ import { Sheet, SheetClose, SheetContent, + SheetDescription, SheetFooter, SheetHeader, SheetTitle, @@ -39,24 +42,470 @@ import { FormLabel, FormMessage, } from "@/components/ui/form.tsx"; -import { useForm } from "react-hook-form" import { Input } from "@/components/ui/input.tsx"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs.tsx"; +import { Switch } from "@/components/ui/switch.tsx"; +import axios from "axios"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; interface SidebarProps { onSelectView: (view: string) => void; onAddHostSubmit: (data: any) => void; + onHostConnect: (hostConfig: any) => void; } -export function SSHSidebar({ onSelectView, onAddHostSubmit }: SidebarProps): React.ReactElement { - const addHostForm = useForm({ +interface AuthPromptFormData { + password: string; + authMethod: string; + sshKeyFile: File | null; + sshKeyContent?: string; +} + +interface AddHostFormData { + name: string; + folder: string; + tags: string[]; + tagsInput?: string; + ip: string; + port: number; + username: string; + password: string; + authMethod: string; + sshKeyFile: File | null; + sshKeyContent?: string; + saveAuthMethod: boolean; + isPinned: boolean; +} + +export function SSHSidebar({ onSelectView, onAddHostSubmit, onHostConnect }: SidebarProps): React.ReactElement { + const addHostForm = useForm({ defaultValues: { + name: '', + folder: '', + tags: [], + tagsInput: '', + ip: '', + port: 22, + username: '', + password: '', + authMethod: 'password', + sshKeyFile: null, + saveAuthMethod: true, + isPinned: false } }) - const onAddHostSubmitReset = (data: any) => { + const [folders, setFolders] = useState([]); + const [foldersLoading, setFoldersLoading] = useState(false); + const [foldersError, setFoldersError] = useState(null); + React.useEffect(() => { + async function fetchFolders() { + setFoldersLoading(true); + setFoldersError(null); + try { + const jwt = document.cookie.split('; ').find(row => row.startsWith('jwt='))?.split('=')[1]; + const res = await axios.get( + (window.location.hostname === 'localhost' ? 'http://localhost:8081' : '') + '/ssh/folders', + { headers: { Authorization: `Bearer ${jwt}` } } + ); + setFolders(res.data || []); + } catch (err: any) { + setFoldersError('Failed to load folders'); + } finally { + setFoldersLoading(false); + } + } + fetchFolders(); + }, []); + + const folderValue = addHostForm.watch('folder'); + const filteredFolders = React.useMemo(() => { + if (!folderValue) return folders; + return folders.filter(f => f.toLowerCase().includes(folderValue.toLowerCase())); + }, [folderValue, folders]); + + const tags = addHostForm.watch('tags') || []; + const tagsInput = addHostForm.watch('tagsInput') || ''; + + const handleTagsInputChange = (e: React.ChangeEvent) => { + const value = e.target.value; + if (value.endsWith(' ')) { + const tag = value.trim(); + if (tag && !tags.includes(tag)) { + addHostForm.setValue('tags', [...tags, tag]); + } + addHostForm.setValue('tagsInput', ''); + } else { + addHostForm.setValue('tagsInput', value); + } + }; + + const handleRemoveTag = (tag: string) => { + addHostForm.setValue('tags', tags.filter((t) => t !== tag)); + }; + + const [folderDropdownOpen, setFolderDropdownOpen] = useState(false); + const [editFolderDropdownOpen, setEditFolderDropdownOpen] = useState(false); + const folderInputRef = React.useRef(null); + const folderDropdownRef = React.useRef(null); + const editFolderInputRef = React.useRef(null); + const editFolderDropdownRef = React.useRef(null); + + React.useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if ( + folderDropdownRef.current && + !folderDropdownRef.current.contains(event.target as Node) && + folderInputRef.current && + !folderInputRef.current.contains(event.target as Node) + ) { + setFolderDropdownOpen(false); + } + if ( + editFolderDropdownRef.current && + !editFolderDropdownRef.current.contains(event.target as Node) && + editFolderInputRef.current && + !editFolderInputRef.current.contains(event.target as Node) + ) { + setEditFolderDropdownOpen(false); + } + } + if (folderDropdownOpen || editFolderDropdownOpen) { + document.addEventListener('mousedown', handleClickOutside); + } else { + document.removeEventListener('mousedown', handleClickOutside); + } + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [folderDropdownOpen, editFolderDropdownOpen]); + + const [submitting, setSubmitting] = useState(false); + const [submitError, setSubmitError] = useState(null); + const [sheetOpen, setSheetOpen] = useState(false); + + React.useEffect(() => { + if (!sheetOpen) { + setFolderDropdownOpen(false); + } + }, [sheetOpen]); + + React.useEffect(() => { + if (!sheetOpen) { + setSubmitError(null); + } + }, [sheetOpen]); + + const onAddHostSubmitReset = async (data: AddHostFormData) => { + setSubmitting(true); + setSubmitError(null); + try { + let sshKeyContent = data.sshKeyContent; + if (data.sshKeyFile instanceof File) { + sshKeyContent = await data.sshKeyFile.text(); + } + const jwt = document.cookie.split('; ').find(row => row.startsWith('jwt='))?.split('=')[1]; + await axios.post( + (window.location.hostname === 'localhost' ? 'http://localhost:8081' : '') + '/ssh/host', + { + name: data.name, + folder: data.folder, + tags: data.tags, + ip: data.ip, + port: data.port, + username: data.username, + password: data.password, + authMethod: data.authMethod, + key: sshKeyContent, + saveAuthMethod: data.saveAuthMethod, + isPinned: data.isPinned + }, + { headers: { Authorization: `Bearer ${jwt}` } } + ); + setSheetOpen(false); addHostForm.reset(); - onAddHostSubmit(data); - } + if (data.folder && !folders.includes(data.folder)) { + setFolders(prev => [...prev, data.folder]); + } + } catch (err: any) { + setSubmitError(err?.response?.data?.error || 'Failed to add SSH host'); + } finally { + setSubmitting(false); + } + }; + + const handleFolderClick = (folder: string) => { + addHostForm.setValue('folder', folder); + setFolderDropdownOpen(false); + }; + + const [hosts, setHosts] = useState([]); + const [hostsLoading, setHostsLoading] = useState(false); + const [hostsError, setHostsError] = useState(null); + const prevHostsRef = React.useRef([]); + const fetchHosts = React.useCallback(async () => { + setHostsLoading(true); + setHostsError(null); + try { + const jwt = document.cookie.split('; ').find(row => row.startsWith('jwt='))?.split('=')[1]; + const res = await axios.get( + (window.location.hostname === 'localhost' ? 'http://localhost:8081' : '') + '/ssh/host', + { headers: { Authorization: `Bearer ${jwt}` } } + ); + const newHosts = res.data || []; + const prevHosts = prevHostsRef.current; + const isSame = + newHosts.length === prevHosts.length && + newHosts.every((h: any, i: number) => { + const prev = prevHosts[i]; + if (!prev) return false; + return ( + h.id === prev.id && + h.name === prev.name && + h.folder === prev.folder && + h.ip === prev.ip && + h.port === prev.port && + h.username === prev.username && + h.password === prev.password && + h.authMethod === prev.authMethod && + h.key === prev.key && + h.saveAuthMethod === prev.saveAuthMethod && + h.isPinned === prev.isPinned && + (Array.isArray(h.tags) ? h.tags.join(',') : h.tags) === (Array.isArray(prev.tags) ? prev.tags.join(',') : prev.tags) + ); + }); + if (!isSame) { + setHosts(newHosts); + prevHostsRef.current = newHosts; + } + } catch (err: any) { + setHostsError('Failed to load hosts'); + } finally { + setHostsLoading(false); + } + }, []); + React.useEffect(() => { + fetchHosts(); + const interval = setInterval(fetchHosts, 10000); + return () => clearInterval(interval); + }, [fetchHosts]); + React.useEffect(() => { + if (!submitting && !sheetOpen) { + fetchHosts(); + } + }, [submitting, sheetOpen, fetchHosts]); + + const [search, setSearch] = useState(""); + const [debouncedSearch, setDebouncedSearch] = useState(""); + React.useEffect(() => { + const handler = setTimeout(() => setDebouncedSearch(search), 200); + return () => clearTimeout(handler); + }, [search]); + + const filteredHosts = React.useMemo(() => { + if (!debouncedSearch.trim()) return hosts; + const q = debouncedSearch.trim().toLowerCase(); + return hosts.filter(h => { + const name = (h.name || "").toLowerCase(); + const ip = (h.ip || "").toLowerCase(); + const tags = Array.isArray(h.tags) ? h.tags : (typeof h.tags === 'string' ? h.tags.split(',').map((t: string) => t.trim().toLowerCase()) : []); + return name.includes(q) || ip.includes(q) || tags.some((tag: string) => tag.includes(q)); + }); + }, [hosts, debouncedSearch]); + + const hostsByFolder = React.useMemo(() => { + const map: Record = {}; + filteredHosts.forEach(h => { + const folder = h.folder && h.folder.trim() ? h.folder : 'No Folder'; + if (!map[folder]) map[folder] = []; + map[folder].push(h); + }); + return map; + }, [filteredHosts]); + const sortedFolders = React.useMemo(() => { + const folders = Object.keys(hostsByFolder); + folders.sort((a, b) => { + if (a === 'No Folder') return -1; + if (b === 'No Folder') return 1; + return a.localeCompare(b); + }); + return folders; + }, [hostsByFolder]); + const getSortedHosts = (arr: any[]) => { + const pinned = arr.filter(h => h.isPinned).sort((a, b) => (a.name || '').localeCompare(b.name || '')); + const rest = arr.filter(h => !h.isPinned).sort((a, b) => (a.name || '').localeCompare(b.name || '')); + return [...pinned, ...rest]; + }; + + const [editHostOpen, setEditHostOpen] = useState(false); + + React.useEffect(() => { + if (!editHostOpen) { + setEditFolderDropdownOpen(false); + } + }, [editHostOpen]); + + const [editHostData, setEditHostData] = useState(null); + const editHostForm = useForm({ + defaultValues: { + name: '', + folder: '', + tags: [], + tagsInput: '', + ip: '', + port: 22, + username: '', + password: '', + authMethod: 'password', + sshKeyFile: null, + saveAuthMethod: false, + isPinned: false + } + }); + React.useEffect(() => { + if (editHostData) { + editHostForm.reset({ + ...editHostData, + tags: editHostData.tags ? (Array.isArray(editHostData.tags) ? editHostData.tags : (typeof editHostData.tags === 'string' ? editHostData.tags.split(',').map((t: string) => t.trim()).filter(Boolean) : [])) : [], + tagsInput: '', + sshKeyFile: null, + sshKeyContent: editHostData.key || '', + }); + } + }, [editHostData]); + + const editTags = editHostForm.watch('tags') || []; + const editTagsInput = editHostForm.watch('tagsInput') || ''; + + const handleEditTagsInputChange = (e: React.ChangeEvent) => { + const value = e.target.value; + if (value.endsWith(' ')) { + const tag = value.trim(); + if (tag && !editTags.includes(tag)) { + editHostForm.setValue('tags', [...editTags, tag]); + } + editHostForm.setValue('tagsInput', ''); + } else { + editHostForm.setValue('tagsInput', value); + } + }; + + const handleRemoveEditTag = (tag: string) => { + editHostForm.setValue('tags', editTags.filter((t) => t !== tag)); + }; + const onEditHostSubmit = async (data: AddHostFormData) => { + let sshKeyContent = data.sshKeyContent; + if (data.sshKeyFile instanceof File) { + sshKeyContent = await data.sshKeyFile.text(); + } + try { + const jwt = document.cookie.split('; ').find(row => row.startsWith('jwt='))?.split('=')[1]; + if (!editHostData?.id) { + throw new Error('No host ID found for editing'); + } + const response = await axios.put( + (window.location.hostname === 'localhost' ? 'http://localhost:8081' : '') + `/ssh/host/${editHostData.id}`, + { + name: data.name, + folder: data.folder, + tags: data.tags, + ip: data.ip, + port: data.port, + username: data.username, + password: data.password, + authMethod: data.authMethod, + key: sshKeyContent, + saveAuthMethod: data.saveAuthMethod, + isPinned: data.isPinned + }, + { headers: { Authorization: `Bearer ${jwt}` } } + ); + setEditHostOpen(false); + fetchHosts(); + } catch (err: any) { + } + }; + const onDeleteHost = async (host: any) => { + try { + const jwt = document.cookie.split('; ').find(row => row.startsWith('jwt='))?.split('=')[1]; + await axios.delete( + (window.location.hostname === 'localhost' ? 'http://localhost:8081' : '') + `/ssh/host/${host.id}`, + { headers: { Authorization: `Bearer ${jwt}` } } + ); + fetchHosts(); + } catch (err) { + } + }; + + const [hostPopoverOpen, setHostPopoverOpen] = useState>({}); + const handlePopoverOpenChange = (hostId: string, open: boolean) => { + setHostPopoverOpen(prev => ({ ...prev, [hostId]: open })); + }; + + const [authPromptOpen, setAuthPromptOpen] = useState(false); + const [authPromptHost, setAuthPromptHost] = useState(null); + const authPromptForm = useForm({ + defaultValues: { + password: '', + authMethod: 'password', + sshKeyFile: null, + } + }); + + const handleHostConnect = (host: any) => { + const hasSavedAuth = host.saveAuthMethod && ((host.authMethod === 'password' && host.password) || (host.authMethod === 'key' && host.key)); + + const hasUsername = host.username && host.username.trim() !== ''; + + if (hasSavedAuth && hasUsername) { + onHostConnect(host); + } else { + setAuthPromptHost(host); + setAuthPromptOpen(true); + } + }; + + const onAuthPromptSubmit = async (data: AuthPromptFormData) => { + let sshKeyContent = data.sshKeyContent; + if (data.sshKeyFile instanceof File) { + sshKeyContent = await data.sshKeyFile.text(); + } + + const hostConfig = { + ...authPromptHost, + password: data.authMethod === 'password' ? data.password : undefined, + key: data.authMethod === 'key' ? sshKeyContent : undefined, + authMethod: data.authMethod, + }; + + if (!hostConfig.username || !hostConfig.ip || !hostConfig.port) { + return; + } + + setAuthPromptOpen(false); + onHostConnect(hostConfig); + }; + + React.useEffect(() => { + if (!authPromptOpen) { + setTimeout(() => { + authPromptForm.reset(); + setAuthPromptHost(null); + }, 100); + } else { + } + }, [authPromptOpen, authPromptForm]); return ( @@ -83,11 +532,13 @@ export function SSHSidebar({ onSelectView, onAddHostSubmit }: SidebarProps): Rea - + { if (!submitting) setSheetOpen(open); }}> + ))} +
+
+ )} + {foldersLoading &&
Loading folders...
} + {foldersError &&
{foldersError}
} + + + )} + /> + + {/* Tags */} + ( + + Tags + + + + {/* Tag chips */} + {tags.length > 0 && ( +
+ {tags.map((tag) => ( + + ))} +
+ )} + +
+ )} + /> + + Connection Details + + Port - + field.onChange(Number(e.target.value) || 22)} + /> @@ -151,14 +721,105 @@ export function SSHSidebar({ onSelectView, onAddHostSubmit }: SidebarProps): Rea )} /> + Authentication + + ( + + + Password + SSH Key + + + + ( + + Password + + + + + + )} + /> + + + + ( + + SSH Private Key + +
+ { + const file = e.target.files?.[0]; + field.onChange(file || null); + }} + className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" + /> + +
+
+
+ )} + /> +
+
+ )} + /> + + ( - Password - +
+ + Save Auth Method +
+
+ +
+ )} + /> + + Other + + ( + + +
+ + Pin Connection +
@@ -171,12 +832,33 @@ export function SSHSidebar({ onSelectView, onAddHostSubmit }: SidebarProps): Rea - - + + + + @@ -184,8 +866,57 @@ export function SSHSidebar({ onSelectView, onAddHostSubmit }: SidebarProps): Rea -
- +
+ {/* Search bar */} +
+ setSearch(e.target.value)} + placeholder="Search hosts..." + className="w-full h-8 text-sm bg-background border border-border rounded" + autoComplete="off" + /> +
+ + {/* Error and status messages */} + {hostsError && ( +
+
{hostsError}
+
+ )} + {!hostsLoading && !hostsError && hosts.length === 0 && ( +
+
No hosts found.
+
+ )} +
+ + 0 ? sortedFolders : undefined}> + {sortedFolders.map((folder, idx) => ( + + {folder} + + {getSortedHosts(hostsByFolder[folder]).map(host => ( +
+ { + setEditHostData(host); + setEditHostOpen(true); + }} + popoverOpen={!!hostPopoverOpen[host.id]} + setPopoverOpen={(open: boolean) => handlePopoverOpenChange(host.id, open)} + /> +
+ ))} +
+
+ ))} +
+
+
@@ -193,6 +924,444 @@ export function SSHSidebar({ onSelectView, onAddHostSubmit }: SidebarProps): Rea + {/* Edit Host Sheet */} + { + if (!open) { + setTimeout(() => { + setEditHostData(null); + editHostForm.reset(); + }, 100); + } + setEditHostOpen(open); + }}> + + + Edit Host + + Modify the SSH host connection settings and authentication details. + + +
+ + + ( + + Name + + + + + + )} + /> + ( + + Folder + + { + if (typeof field.ref === 'function') field.ref(el); + (editFolderInputRef as React.MutableRefObject).current = el; + }} + onFocus={() => setEditFolderDropdownOpen(true)} + onChange={e => { + field.onChange(e); + setEditFolderDropdownOpen(true); + }} + disabled={foldersLoading} + /> + + {editFolderDropdownOpen && filteredFolders.length > 0 && ( +
+
+ {filteredFolders.map((folder) => ( + + ))} +
+
+ )} + {foldersLoading &&
Loading folders...
} + {foldersError &&
{foldersError}
} + +
+ )} + /> + ( + + Tags + + + + {/* Tag chips */} + {editTags.length > 0 && ( +
+ {editTags.map((tag) => ( + + ))} +
+ )} + +
+ )} + /> + Connection Details + + ( + + IP + + + + + + )} + /> + ( + + Port + + field.onChange(Number(e.target.value) || 22)} + /> + + + + )} + /> + ( + + Username + + + + + + )} + /> + Authentication + + ( + + + Password + SSH Key + + + ( + + Password + + + + + + )} + /> + + + ( + + SSH Private Key + +
+ { + const file = e.target.files?.[0]; + field.onChange(file || null); + }} + className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" + /> + +
+
+
+ )} + /> +
+
+ )} + /> + ( + + +
+ + Save Auth Method +
+
+ +
+ )} + /> + Other + + ( + + +
+ + Pin Connection +
+
+ +
+ )} + /> + + +
+ + + + + + + + + +
+
+ {/* Auth Prompt Sheet */} + { + setAuthPromptOpen(open); + }}> + + + Enter Credentials + + Provide authentication credentials to connect to the SSH host. + + +
+
+ + ( + + + Password + SSH Key + + + + ( + + Password + + + + + + )} + /> + + + + ( + + SSH Private Key + +
+ { + const file = e.target.files?.[0]; + field.onChange(file || null); + }} + className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" + /> + +
+
+
+ )} + /> +
+
+ )} + /> + + +
+ + + + + + + + + +
+
); -} \ No newline at end of file +} + +const HostMenuItem = React.memo(function HostMenuItem({ host, onHostConnect, onDeleteHost, onEditHost, popoverOpen, setPopoverOpen }: any) { + const tags = Array.isArray(host.tags) ? host.tags : (typeof host.tags === 'string' ? host.tags.split(',').map((t: string) => t.trim()).filter(Boolean) : []); + const hasTags = tags.length > 0; + return ( +
+
+
+ {/* Left: Name + Star - Horizontal scroll only */} +
onHostConnect(host)} + > +
+ {host.isPinned && } + {host.name || host.ip} +
+
+
+ + + + + + + + + +
+
+ {hasTags && ( +
+ {tags.map((tag: string) => ( + + {tag} + + ))} +
+ )} +
+
+ ); +}); \ No newline at end of file diff --git a/src/apps/SSH/SSHTerminal.tsx b/src/apps/SSH/SSHTerminal.tsx index 27b64fe8..899b6e6b 100644 --- a/src/apps/SSH/SSHTerminal.tsx +++ b/src/apps/SSH/SSHTerminal.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState, useImperativeHandle, forwardRef } from 'react'; +import { useEffect, useRef, useState, useImperativeHandle, forwardRef } from 'react'; import { useXTerm } from 'react-xtermjs'; import { FitAddon } from '@xterm/addon-fit'; import { ClipboardAddon } from '@xterm/addon-clipboard'; @@ -15,7 +15,6 @@ export const SSHTerminal = forwardRef(function SSHTermina { hostConfig, isVisible, splitScreen = false }, ref ) { - console.log('Rendering SSHTerminal', { hostConfig, isVisible }); const { instance: terminal, ref: xtermRef } = useXTerm(); const fitAddonRef = useRef(null); const webSocketRef = useRef(null); @@ -75,7 +74,6 @@ export const SSHTerminal = forwardRef(function SSHTermina resizeTimeout.current = setTimeout(() => { fitAddonRef.current?.fit(); - // Always send cols + 1 const cols = terminal.cols + 1; const rows = terminal.rows; @@ -93,7 +91,6 @@ export const SSHTerminal = forwardRef(function SSHTermina fitAddon.fit(); setVisible(true); - // Always send cols + 1 const cols = terminal.cols + 1; const rows = terminal.rows; @@ -101,8 +98,6 @@ export const SSHTerminal = forwardRef(function SSHTermina webSocketRef.current = ws; ws.addEventListener('open', () => { - terminal.writeln('WebSocket opened'); - ws.send(JSON.stringify({ type: 'connectToHost', data: { @@ -123,16 +118,13 @@ export const SSHTerminal = forwardRef(function SSHTermina ws.addEventListener('message', (event) => { try { const msg = JSON.parse(event.data); - console.log('WS message received:', msg); // Debug log if (msg.type === 'data') { terminal.write(msg.data); } else if (msg.type === 'error') { terminal.writeln(`\r\n[ERROR] ${msg.message}`); } else if (msg.type === 'connected') { - terminal.writeln('[SSH connected. Waiting for prompt...]'); - } else { - console.log('Unhandled message:', msg); + /* nothing for now */ } } catch (err) { console.error('Failed to parse message', err); @@ -166,13 +158,34 @@ export const SSHTerminal = forwardRef(function SSHTermina ref={xtermRef} style={{ position: 'absolute', - top: splitScreen ? 0 : 48, + top: splitScreen ? 0 : 0, left: 0, - right: '-1ch', + right: 0, bottom: 0, marginLeft: 2, opacity: visible && isVisible ? 1 : 0, + overflow: 'hidden', }} /> ); -}); \ No newline at end of file +}); + +const style = document.createElement('style'); +style.innerHTML = ` +.xterm .xterm-viewport::-webkit-scrollbar { + width: 8px; + background: transparent; +} +.xterm .xterm-viewport::-webkit-scrollbar-thumb { + background: rgba(180,180,180,0.7); + border-radius: 4px; +} +.xterm .xterm-viewport::-webkit-scrollbar-thumb:hover { + background: rgba(120,120,120,0.9); +} +.xterm .xterm-viewport { + scrollbar-width: thin; + scrollbar-color: rgba(180,180,180,0.7) transparent; +} +`; +document.head.appendChild(style); \ No newline at end of file diff --git a/src/backend/db/database.ts b/src/backend/db/database.ts new file mode 100644 index 00000000..b95f63dd --- /dev/null +++ b/src/backend/db/database.ts @@ -0,0 +1,58 @@ +import express from 'express'; +import bodyParser from 'body-parser'; +import userRoutes from './routes/users.js'; +import sshRoutes from './routes/ssh.js'; +import chalk from 'chalk'; +import cors from 'cors'; + +// Custom logger (adapted from starter.ts, with a database icon) +const dbIconSymbol = '🗄️'; +const getTimeStamp = (): string => chalk.gray(`[${new Date().toLocaleTimeString()}]`); +const formatMessage = (level: string, colorFn: chalk.Chalk, message: string): string => { + return `${getTimeStamp()} ${colorFn(`[${level.toUpperCase()}]`)} ${chalk.hex('#1e3a8a')(`[${dbIconSymbol}]`)} ${message}`; +}; +const logger = { + info: (msg: string): void => { + console.log(formatMessage('info', chalk.cyan, msg)); + }, + warn: (msg: string): void => { + console.warn(formatMessage('warn', chalk.yellow, msg)); + }, + error: (msg: string, err?: unknown): void => { + console.error(formatMessage('error', chalk.redBright, msg)); + if (err) console.error(err); + }, + success: (msg: string): void => { + console.log(formatMessage('success', chalk.greenBright, msg)); + }, + debug: (msg: string): void => { + if (process.env.NODE_ENV !== 'production') { + console.debug(formatMessage('debug', chalk.magenta, msg)); + } + } +}; + +const app = express(); + +app.use(cors({ + origin: 'http://localhost:5173', + credentials: true +})); + +app.use(bodyParser.json()); + +app.get('/health', (req, res) => { + res.json({ status: 'ok' }); +}); + +app.use('/users', userRoutes); +app.use('/ssh', sshRoutes); + +app.use((err: unknown, req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.error('Unhandled error:', err); + res.status(500).json({ error: 'Internal Server Error' }); +}); + +// Start server +const PORT = 8081; +app.listen(PORT); \ No newline at end of file diff --git a/src/backend/db/db/index.ts b/src/backend/db/db/index.ts new file mode 100644 index 00000000..affe5e98 --- /dev/null +++ b/src/backend/db/db/index.ts @@ -0,0 +1,83 @@ +import { drizzle } from 'drizzle-orm/better-sqlite3'; +import Database from 'better-sqlite3'; +import * as schema from './schema.js'; +import chalk from 'chalk'; +import fs from 'fs'; +import path from 'path'; + +const dbIconSymbol = '🗄️'; +const getTimeStamp = (): string => chalk.gray(`[${new Date().toLocaleTimeString()}]`); +const formatMessage = (level: string, colorFn: chalk.Chalk, message: string): string => { + return `${getTimeStamp()} ${colorFn(`[${level.toUpperCase()}]`)} ${chalk.hex('#1e3a8a')(`[${dbIconSymbol}]`)} ${message}`; +}; +const logger = { + info: (msg: string): void => { + console.log(formatMessage('info', chalk.cyan, msg)); + }, + warn: (msg: string): void => { + console.warn(formatMessage('warn', chalk.yellow, msg)); + }, + error: (msg: string, err?: unknown): void => { + console.error(formatMessage('error', chalk.redBright, msg)); + if (err) console.error(err); + }, + success: (msg: string): void => { + console.log(formatMessage('success', chalk.greenBright, msg)); + }, + debug: (msg: string): void => { + if (process.env.NODE_ENV !== 'production') { + console.debug(formatMessage('debug', chalk.magenta, msg)); + } + } +}; + +const dbDir = path.resolve('./db/data'); +if (!fs.existsSync(dbDir)) { + fs.mkdirSync(dbDir, { recursive: true }); +} + +const sqlite = new Database('./db/data/db.sqlite'); +logger.success('Database connection established'); + +sqlite.exec(` +CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + username TEXT NOT NULL, + password_hash TEXT NOT NULL, + is_admin INTEGER NOT NULL DEFAULT 0 +); +CREATE TABLE IF NOT EXISTS ssh_data ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + name TEXT, + folder TEXT, + tags TEXT, + ip TEXT NOT NULL, + port INTEGER NOT NULL, + username TEXT, + password TEXT, + auth_method TEXT, + key TEXT, + save_auth_method INTEGER, + is_pinned INTEGER, + FOREIGN KEY(user_id) REFERENCES users(id) +); +CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL +); +`); +try { + sqlite.prepare('SELECT is_admin FROM users LIMIT 1').get(); +} catch (e) { + sqlite.exec('ALTER TABLE users ADD COLUMN is_admin INTEGER NOT NULL DEFAULT 0;'); +} +try { + const row = sqlite.prepare("SELECT value FROM settings WHERE key = 'allow_registration'").get(); + if (!row) { + sqlite.prepare("INSERT INTO settings (key, value) VALUES ('allow_registration', 'true')").run(); + } +} catch (e) { +} + +export const db = drizzle(sqlite, { schema }); \ No newline at end of file diff --git a/src/backend/db/db/schema.ts b/src/backend/db/db/schema.ts new file mode 100644 index 00000000..1ec9b8d7 --- /dev/null +++ b/src/backend/db/db/schema.ts @@ -0,0 +1,29 @@ +import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'; + +export const users = sqliteTable('users', { + id: text('id').primaryKey(), // Unique user ID (nanoid) + username: text('username').notNull(), // Username + password_hash: text('password_hash').notNull(), // Hashed password + is_admin: integer('is_admin', { mode: 'boolean' }).notNull().default(false), // Admin flag +}); + +export const sshData = sqliteTable('ssh_data', { + id: integer('id').primaryKey({ autoIncrement: true }), + userId: text('user_id').notNull().references(() => users.id), + name: text('name'), + folder: text('folder'), + tags: text('tags'), + ip: text('ip').notNull(), + port: integer('port').notNull(), + username: text('username'), + password: text('password'), + authMethod: text('auth_method'), + key: text('key', { length: 2048 }), + saveAuthMethod: integer('save_auth_method', { mode: 'boolean' }), + isPinned: integer('is_pinned', { mode: 'boolean' }), +}); + +export const settings = sqliteTable('settings', { + key: text('key').primaryKey(), + value: text('value').notNull(), +}); \ No newline at end of file diff --git a/src/backend/db/routes/ssh.ts b/src/backend/db/routes/ssh.ts new file mode 100644 index 00000000..ea10cc98 --- /dev/null +++ b/src/backend/db/routes/ssh.ts @@ -0,0 +1,231 @@ +import express from 'express'; +import { db } from '../db/index.js'; +import { sshData } from '../db/schema.js'; +import { eq, and } from 'drizzle-orm'; +import chalk from 'chalk'; +import jwt from 'jsonwebtoken'; +import type { Request, Response, NextFunction } from 'express'; + +const dbIconSymbol = '🗄️'; +const getTimeStamp = (): string => chalk.gray(`[${new Date().toLocaleTimeString()}]`); +const formatMessage = (level: string, colorFn: chalk.Chalk, message: string): string => { + return `${getTimeStamp()} ${colorFn(`[${level.toUpperCase()}]`)} ${chalk.hex('#1e3a8a')(`[${dbIconSymbol}]`)} ${message}`; +}; +const logger = { + info: (msg: string): void => { + console.log(formatMessage('info', chalk.cyan, msg)); + }, + warn: (msg: string): void => { + console.warn(formatMessage('warn', chalk.yellow, msg)); + }, + error: (msg: string, err?: unknown): void => { + console.error(formatMessage('error', chalk.redBright, msg)); + if (err) console.error(err); + }, + success: (msg: string): void => { + console.log(formatMessage('success', chalk.greenBright, msg)); + }, + debug: (msg: string): void => { + if (process.env.NODE_ENV !== 'production') { + console.debug(formatMessage('debug', chalk.magenta, msg)); + } + } +}; + +const router = express.Router(); + +function isNonEmptyString(val: any): val is string { + return typeof val === 'string' && val.trim().length > 0; +} +function isValidPort(val: any): val is number { + return typeof val === 'number' && val > 0 && val < 65536; +} + +interface JWTPayload { + userId: string; + iat?: number; + exp?: number; +} + +// JWT authentication middleware +function authenticateJWT(req: Request, res: Response, next: NextFunction) { + const authHeader = req.headers['authorization']; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + logger.warn('Missing or invalid Authorization header'); + return res.status(401).json({ error: 'Missing or invalid Authorization header' }); + } + const token = authHeader.split(' ')[1]; + const jwtSecret = process.env.JWT_SECRET || 'secret'; + try { + const payload = jwt.verify(token, jwtSecret) as JWTPayload; + (req as any).userId = payload.userId; + next(); + } catch (err) { + logger.warn('Invalid or expired token'); + return res.status(401).json({ error: 'Invalid or expired token' }); + } +} + +// Route: Create SSH data (requires JWT) +// POST /ssh/host +router.post('/host', authenticateJWT, async (req: Request, res: Response) => { + const { name, folder, tags, ip, port, username, password, authMethod, key, saveAuthMethod, isPinned } = req.body; + const userId = (req as any).userId; + if (!isNonEmptyString(userId) || !isNonEmptyString(ip) || !isValidPort(port)) { + logger.warn('Invalid SSH data input'); + return res.status(400).json({ error: 'Invalid SSH data' }); + } + + const sshDataObj: any = { + userId: userId, + name, + folder, + tags: Array.isArray(tags) ? tags.join(',') : tags, + ip, + port, + username, + authMethod, + saveAuthMethod: saveAuthMethod ? 1 : 0, + isPinned: isPinned ? 1 : 0, + }; + + if (saveAuthMethod) { + if (authMethod === 'password') { + sshDataObj.password = password; + sshDataObj.key = null; + } else if (authMethod === 'key') { + sshDataObj.key = key; + sshDataObj.password = null; + } + } else { + sshDataObj.password = null; + sshDataObj.key = null; + } + + try { + await db.insert(sshData).values(sshDataObj); + res.json({ message: 'SSH data created' }); + } catch (err) { + logger.error('Failed to save SSH data', err); + res.status(500).json({ error: 'Failed to save SSH data' }); + } +}); + +// Route: Update SSH data (requires JWT) +// PUT /ssh/host/:id +router.put('/host/:id', authenticateJWT, async (req: Request, res: Response) => { + const { name, folder, tags, ip, port, username, password, authMethod, key, saveAuthMethod, isPinned } = req.body; + const { id } = req.params; + const userId = (req as any).userId; + + if (!isNonEmptyString(userId) || !isNonEmptyString(ip) || !isValidPort(port) || !id) { + logger.warn('Invalid SSH data input for update'); + return res.status(400).json({ error: 'Invalid SSH data' }); + } + + const sshDataObj: any = { + name, + folder, + tags: Array.isArray(tags) ? tags.join(',') : tags, + ip, + port, + username, + authMethod, + saveAuthMethod: saveAuthMethod ? 1 : 0, + isPinned: isPinned ? 1 : 0, + }; + + if (saveAuthMethod) { + if (authMethod === 'password') { + sshDataObj.password = password; + sshDataObj.key = null; + } else if (authMethod === 'key') { + sshDataObj.key = key; + sshDataObj.password = null; + } + } else { + sshDataObj.password = null; + sshDataObj.key = null; + } + + try { + const result = await db.update(sshData) + .set(sshDataObj) + .where(and(eq(sshData.id, Number(id)), eq(sshData.userId, userId))); + res.json({ message: 'SSH data updated' }); + } catch (err) { + logger.error('Failed to update SSH data', err); + res.status(500).json({ error: 'Failed to update SSH data' }); + } +}); + +// Route: Get SSH data for the authenticated user (requires JWT) +// GET /ssh/host +router.get('/host', authenticateJWT, async (req: Request, res: Response) => { + const userId = (req as any).userId; + if (!isNonEmptyString(userId)) { + logger.warn('Invalid userId for SSH data fetch'); + return res.status(400).json({ error: 'Invalid userId' }); + } + try { + const data = await db + .select() + .from(sshData) + .where(eq(sshData.userId, userId)); + res.json(data); + } catch (err) { + logger.error('Failed to fetch SSH data', err); + res.status(500).json({ error: 'Failed to fetch SSH data' }); + } +}); + +// Route: Get all unique folders for the authenticated user (requires JWT) +// GET /ssh/folders +router.get('/folders', authenticateJWT, async (req: Request, res: Response) => { + const userId = (req as any).userId; + if (!isNonEmptyString(userId)) { + logger.warn('Invalid userId for SSH folder fetch'); + return res.status(400).json({ error: 'Invalid userId' }); + } + try { + const data = await db + .select({ folder: sshData.folder }) + .from(sshData) + .where(eq(sshData.userId, userId)); + + const folderCounts: Record = {}; + data.forEach(d => { + if (d.folder && d.folder.trim() !== '') { + folderCounts[d.folder] = (folderCounts[d.folder] || 0) + 1; + } + }); + + const folders = Object.keys(folderCounts).filter(folder => folderCounts[folder] > 0); + + res.json(folders); + } catch (err) { + logger.error('Failed to fetch SSH folders', err); + res.status(500).json({ error: 'Failed to fetch SSH folders' }); + } +}); + +// Route: Delete SSH host by id (requires JWT) +// DELETE /ssh/host/:id +router.delete('/host/:id', authenticateJWT, async (req: Request, res: Response) => { + const userId = (req as any).userId; + const { id } = req.params; + if (!isNonEmptyString(userId) || !id) { + logger.warn('Invalid userId or id for SSH host delete'); + return res.status(400).json({ error: 'Invalid userId or id' }); + } + try { + const result = await db.delete(sshData) + .where(and(eq(sshData.id, Number(id)), eq(sshData.userId, userId))); + res.json({ message: 'SSH host deleted' }); + } catch (err) { + logger.error('Failed to delete SSH host', err); + res.status(500).json({ error: 'Failed to delete SSH host' }); + } +}); + +export default router; \ No newline at end of file diff --git a/src/backend/db/routes/users.ts b/src/backend/db/routes/users.ts new file mode 100644 index 00000000..d2433dd1 --- /dev/null +++ b/src/backend/db/routes/users.ts @@ -0,0 +1,226 @@ +import express from 'express'; +import { db } from '../db/index.js'; +import { users, settings } from '../db/schema.js'; +import { eq } from 'drizzle-orm'; +import chalk from 'chalk'; +import bcrypt from 'bcryptjs'; +import { nanoid } from 'nanoid'; +import jwt from 'jsonwebtoken'; +import type { Request, Response, NextFunction } from 'express'; + +const dbIconSymbol = '🗄️'; +const getTimeStamp = (): string => chalk.gray(`[${new Date().toLocaleTimeString()}]`); +const formatMessage = (level: string, colorFn: chalk.Chalk, message: string): string => { + return `${getTimeStamp()} ${colorFn(`[${level.toUpperCase()}]`)} ${chalk.hex('#1e3a8a')(`[${dbIconSymbol}]`)} ${message}`; +}; +const logger = { + info: (msg: string): void => { + console.log(formatMessage('info', chalk.cyan, msg)); + }, + warn: (msg: string): void => { + console.warn(formatMessage('warn', chalk.yellow, msg)); + }, + error: (msg: string, err?: unknown): void => { + console.error(formatMessage('error', chalk.redBright, msg)); + if (err) console.error(err); + }, + success: (msg: string): void => { + console.log(formatMessage('success', chalk.greenBright, msg)); + }, + debug: (msg: string): void => { + if (process.env.NODE_ENV !== 'production') { + console.debug(formatMessage('debug', chalk.magenta, msg)); + } + } +}; + +const router = express.Router(); + +function isNonEmptyString(val: any): val is string { + return typeof val === 'string' && val.trim().length > 0; +} + +interface JWTPayload { + userId: string; + iat?: number; + exp?: number; +} + +// JWT authentication middleware +function authenticateJWT(req: Request, res: Response, next: NextFunction) { + const authHeader = req.headers['authorization']; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + logger.warn('Missing or invalid Authorization header'); + return res.status(401).json({ error: 'Missing or invalid Authorization header' }); + } + const token = authHeader.split(' ')[1]; + const jwtSecret = process.env.JWT_SECRET || 'secret'; + try { + const payload = jwt.verify(token, jwtSecret) as JWTPayload; + (req as any).userId = payload.userId; + next(); + } catch (err) { + logger.warn('Invalid or expired token'); + return res.status(401).json({ error: 'Invalid or expired token' }); + } +} + +// Route: Create user +// POST /users/create +router.post('/create', async (req, res) => { + try { + const row = db.$client.prepare("SELECT value FROM settings WHERE key = 'allow_registration'").get(); + if (row && (row as any).value !== 'true') { + return res.status(403).json({ error: 'Registration is currently disabled' }); + } + } catch (e) { + } + const { username, password } = req.body; + if (!isNonEmptyString(username) || !isNonEmptyString(password)) { + logger.warn('Invalid user creation attempt'); + return res.status(400).json({ error: 'Invalid username or password' }); + } + try { + const existing = await db + .select() + .from(users) + .where(eq(users.username, username)); + if (existing && existing.length > 0) { + logger.warn(`Attempt to create duplicate username: ${username}`); + return res.status(409).json({ error: 'Username already exists' }); + } + let isFirstUser = false; + try { + const countResult = db.$client.prepare('SELECT COUNT(*) as count FROM users').get(); + isFirstUser = ((countResult as any)?.count || 0) === 0; + } catch (e) { + isFirstUser = true; + } + const saltRounds = parseInt(process.env.SALT || '10', 10); + const password_hash = await bcrypt.hash(password, saltRounds); + const id = nanoid(); + await db.insert(users).values({ id, username, password_hash, is_admin: isFirstUser }); + logger.success(`User created: ${username} (is_admin: ${isFirstUser})`); + res.json({ message: 'User created', is_admin: isFirstUser }); + } catch (err) { + logger.error('Failed to create user', err); + res.status(500).json({ error: 'Failed to create user' }); + } +}); + +// Route: Get user JWT by username and password +// POST /users/get +router.post('/get', async (req, res) => { + const { username, password } = req.body; + if (!isNonEmptyString(username) || !isNonEmptyString(password)) { + logger.warn('Invalid get user attempt'); + return res.status(400).json({ error: 'Invalid username or password' }); + } + try { + const user = await db + .select() + .from(users) + .where(eq(users.username, username)); + if (!user || user.length === 0) { + logger.warn(`User not found: ${username}`); + return res.status(404).json({ error: 'User not found' }); + } + const userRecord = user[0]; + const isMatch = await bcrypt.compare(password, userRecord.password_hash); + if (!isMatch) { + logger.warn(`Incorrect password for user: ${username}`); + return res.status(401).json({ error: 'Incorrect password' }); + } + const jwtSecret = process.env.JWT_SECRET || 'secret'; + const token = jwt.sign({ userId: userRecord.id }, jwtSecret, { expiresIn: '50d' }); + logger.success(`User authenticated: ${username}`); + res.json({ token }); + } catch (err) { + logger.error('Failed to get user', err); + res.status(500).json({ error: 'Failed to get user' }); + } +}); + +// Route: Get current user's username using JWT +// GET /users/me +router.get('/me', authenticateJWT, async (req: Request, res: Response) => { + const userId = (req as any).userId; + if (!isNonEmptyString(userId)) { + logger.warn('Invalid userId in JWT for /users/me'); + return res.status(401).json({ error: 'Invalid userId' }); + } + try { + const user = await db + .select() + .from(users) + .where(eq(users.id, userId)); + if (!user || user.length === 0) { + logger.warn(`User not found for /users/me: ${userId}`); + return res.status(401).json({ error: 'User not found' }); + } + res.json({ username: user[0].username, is_admin: !!user[0].is_admin }); + } catch (err) { + logger.error('Failed to get username', err); + res.status(500).json({ error: 'Failed to get username' }); + } +}); + +// Route: Count users +// GET /users/count +router.get('/count', async (req, res) => { + try { + const countResult = db.$client.prepare('SELECT COUNT(*) as count FROM users').get(); + const count = (countResult as any)?.count || 0; + res.json({ count }); + } catch (err) { + logger.error('Failed to count users', err); + res.status(500).json({ error: 'Failed to count users' }); + } +}); + +// Route: DB health check (actually queries DB) +// GET /users/db-health +router.get('/db-health', async (req, res) => { + try { + db.$client.prepare('SELECT 1').get(); + res.json({ status: 'ok' }); + } catch (err) { + logger.error('DB health check failed', err); + res.status(500).json({ error: 'Database not accessible' }); + } +}); + +// Route: Get registration allowed status +// GET /users/registration-allowed +router.get('/registration-allowed', async (req, res) => { + try { + const row = db.$client.prepare("SELECT value FROM settings WHERE key = 'allow_registration'").get(); + res.json({ allowed: row ? (row as any).value === 'true' : true }); + } catch (err) { + logger.error('Failed to get registration allowed', err); + res.status(500).json({ error: 'Failed to get registration allowed' }); + } +}); + +// Route: Set registration allowed status (admin only) +// PATCH /users/registration-allowed +router.patch('/registration-allowed', authenticateJWT, async (req, res) => { + const userId = (req as any).userId; + try { + const user = await db.select().from(users).where(eq(users.id, userId)); + if (!user || user.length === 0 || !user[0].is_admin) { + return res.status(403).json({ error: 'Not authorized' }); + } + const { allowed } = req.body; + if (typeof allowed !== 'boolean') { + return res.status(400).json({ error: 'Invalid value for allowed' }); + } + db.$client.prepare("UPDATE settings SET value = ? WHERE key = 'allow_registration'").run(allowed ? 'true' : 'false'); + res.json({ allowed }); + } catch (err) { + logger.error('Failed to set registration allowed', err); + res.status(500).json({ error: 'Failed to set registration allowed' }); + } +}); + +export default router; \ No newline at end of file diff --git a/src/backend/ssh.cjs b/src/backend/ssh.cjs deleted file mode 100644 index 15bd8927..00000000 --- a/src/backend/ssh.cjs +++ /dev/null @@ -1,142 +0,0 @@ -const WebSocket = require('ws'); -const { Client } = require('ssh2'); - -const wss = new WebSocket.Server({ port: 8082 }); - -wss.on('connection', (ws) => { - let sshConn = null; - let sshStream = null; - - ws.on('close', () => { - cleanupSSH(); - }); - - ws.on('message', (msg) => { - let parsed; - try { - parsed = JSON.parse(msg); - } catch (e) { - console.error('Invalid JSON received:', msg); - ws.send(JSON.stringify({ type: 'error', message: 'Invalid JSON' })); - return; - } - - const { type, data } = parsed; - - switch (type) { - case 'connectToHost': - handleConnectToHost(data); - break; - - case 'resize': - handleResize(data); - break; - - case 'disconnect': - cleanupSSH(); - break; - - case 'input': - if (sshStream) sshStream.write(data); - break; - - default: - console.warn('Unknown message type:', type); - } - }); - - function handleConnectToHost({ cols, rows, hostConfig }) { - const { ip, port, username, password } = hostConfig; - - sshConn = new Client(); - - sshConn.on('ready', () => { - sshConn.shell({ - term: "xterm-256color", - cols, - rows, - modes: { - ECHO: 1, - ECHOCTL: 0, - ICANON: 1, - TTY_OP_OSWRAP: 1 - } - }, (err, stream) => { - if (err) { - console.error('Shell error:', err); - ws.send(JSON.stringify({ type: 'error', message: 'Shell error: ' + err.message })); - return; - } - - sshStream = stream; - - stream.on('data', (chunk) => { - ws.send(JSON.stringify({ type: 'data', data: chunk.toString() })); - }); - - stream.on('close', () => { - cleanupSSH(); - }); - - stream.on('error', (err) => { - console.error('SSH stream error:', err.message); - ws.send(JSON.stringify({ type: 'error', message: 'SSH stream error: ' + err.message })); - }); - - ws.send(JSON.stringify({ type: 'connected', message: 'SSH connected' })); - // stream.write('\n'); // Force prompt to appear (removed to avoid double prompt) - console.log('Sent connected message and newline to SSH stream'); - }); - }); - - sshConn.on('error', (err) => { - console.error('SSH connection error:', err.message); - ws.send(JSON.stringify({ type: 'error', message: 'SSH error: ' + err.message })); - cleanupSSH(); - }); - - sshConn.on('close', () => { - cleanupSSH(); - }); - - sshConn.connect({ - host: ip, - port, - username, - password, - keepaliveInterval: 5000, - keepaliveCountMax: 10, - readyTimeout: 10000, - tcpKeepAlive: true, - }); - } - - function handleResize({ cols, rows }) { - if (sshStream && sshStream.setWindow) { - sshStream.setWindow(rows, cols, rows, cols); - ws.send(JSON.stringify({ type: 'resized', cols, rows })); - } - } - - function cleanupSSH() { - if (sshStream) { - try { - sshStream.end(); - } catch (e) { - console.error('Error closing stream:', e.message); - } - sshStream = null; - } - - if (sshConn) { - try { - sshConn.end(); - } catch (e) { - console.error('Error closing connection:', e.message); - } - sshConn = null; - } - } -}); - -console.log('WebSocket server running on ws://localhost:8082'); \ No newline at end of file diff --git a/src/backend/ssh/ssh.ts b/src/backend/ssh/ssh.ts new file mode 100644 index 00000000..5044caa5 --- /dev/null +++ b/src/backend/ssh/ssh.ts @@ -0,0 +1,200 @@ +import { WebSocketServer, WebSocket, type RawData } from 'ws'; +import { Client, type ClientChannel, type PseudoTtyOptions } from 'ssh2'; +import chalk from 'chalk'; + +const wss = new WebSocketServer({ port: 8082 }); + +const sshIconSymbol = '🖥️'; +const getTimeStamp = (): string => chalk.gray(`[${new Date().toLocaleTimeString()}]`); +const formatMessage = (level: string, colorFn: chalk.Chalk, message: string): string => { + return `${getTimeStamp()} ${colorFn(`[${level.toUpperCase()}]`)} ${chalk.hex('#1e3a8a')(`[${sshIconSymbol}]`)} ${message}`; +}; +const logger = { + info: (msg: string): void => { + console.log(formatMessage('info', chalk.cyan, msg)); + }, + warn: (msg: string): void => { + console.warn(formatMessage('warn', chalk.yellow, msg)); + }, + error: (msg: string, err?: unknown): void => { + console.error(formatMessage('error', chalk.redBright, msg)); + if (err) console.error(err); + }, + success: (msg: string): void => { + console.log(formatMessage('success', chalk.greenBright, msg)); + }, + debug: (msg: string): void => { + if (process.env.NODE_ENV !== 'production') { + console.debug(formatMessage('debug', chalk.magenta, msg)); + } + } +}; + +wss.on('connection', (ws: WebSocket) => { + let sshConn: Client | null = null; + let sshStream: ClientChannel | null = null; + + ws.on('close', () => { + cleanupSSH(); + }); + + ws.on('message', (msg: RawData) => { + let parsed: any; + try { + parsed = JSON.parse(msg.toString()); + } catch (e) { + logger.error('Invalid JSON received: ' + msg.toString()); + ws.send(JSON.stringify({ type: 'error', message: 'Invalid JSON' })); + return; + } + + const { type, data } = parsed; + + switch (type) { + case 'connectToHost': + handleConnectToHost(data); + break; + + case 'resize': + handleResize(data); + break; + + case 'disconnect': + cleanupSSH(); + break; + + case 'input': + if (sshStream) sshStream.write(data); + break; + + default: + logger.warn('Unknown message type: ' + type); + } + }); + + function handleConnectToHost(data: { + cols: number; + rows: number; + hostConfig: { + ip: string; + port: number; + username: string; + password?: string; + key?: string; + authMethod?: string; + }; + }) { + const { cols, rows, hostConfig } = data; + const { ip, port, username, password, key, authMethod } = hostConfig; + + if (!username || typeof username !== 'string' || username.trim() === '') { + logger.error('Invalid username provided'); + ws.send(JSON.stringify({ type: 'error', message: 'Invalid username provided' })); + return; + } + + if (!ip || typeof ip !== 'string' || ip.trim() === '') { + logger.error('Invalid IP provided'); + ws.send(JSON.stringify({ type: 'error', message: 'Invalid IP provided' })); + return; + } + + if (!port || typeof port !== 'number' || port <= 0) { + logger.error('Invalid port provided'); + ws.send(JSON.stringify({ type: 'error', message: 'Invalid port provided' })); + return; + } + + sshConn = new Client(); + + sshConn.on('ready', () => { + const pseudoTtyOpts: PseudoTtyOptions = { + term: 'xterm-256color', + cols, + rows, + modes: { + ECHO: 1, + ECHOCTL: 0, + ICANON: 1, + } + }; + + sshConn!.shell(pseudoTtyOpts, (err, stream) => { + if (err) { + logger.error('Shell error: ' + err.message); + ws.send(JSON.stringify({ type: 'error', message: 'Shell error: ' + err.message })); + return; + } + + sshStream = stream; + + stream.on('data', (chunk: Buffer) => { + ws.send(JSON.stringify({ type: 'data', data: chunk.toString() })); + }); + + stream.on('close', () => { + cleanupSSH(); + }); + + stream.on('error', (err: Error) => { + logger.error('SSH stream error: ' + err.message); + ws.send(JSON.stringify({ type: 'error', message: 'SSH stream error: ' + err.message })); + }); + + ws.send(JSON.stringify({ type: 'connected', message: 'SSH connected' })); + }); + }); + + sshConn.on('error', (err: Error) => { + logger.error('SSH connection error: ' + err.message); + ws.send(JSON.stringify({ type: 'error', message: 'SSH error: ' + err.message })); + cleanupSSH(); + }); + + sshConn.on('close', () => { + cleanupSSH(); + }); + + const connectConfig: any = { + host: ip, + port, + username, + keepaliveInterval: 5000, + keepaliveCountMax: 10, + readyTimeout: 10000, + }; + if (authMethod === 'key' && key) { + connectConfig.privateKey = key; + } else { + connectConfig.password = password; + } + sshConn.connect(connectConfig); + } + + function handleResize(data: { cols: number; rows: number }) { + if (sshStream && sshStream.setWindow) { + sshStream.setWindow(data.rows, data.cols, data.rows, data.cols); + ws.send(JSON.stringify({ type: 'resized', cols: data.cols, rows: data.rows })); + } + } + + function cleanupSSH() { + if (sshStream) { + try { + sshStream.end(); + } catch (e: any) { + logger.error('Error closing stream: ' + e.message); + } + sshStream = null; + } + + if (sshConn) { + try { + sshConn.end(); + } catch (e: any) { + logger.error('Error closing connection: ' + e.message); + } + sshConn = null; + } + } +}); \ No newline at end of file diff --git a/src/backend/starter.ts b/src/backend/starter.ts new file mode 100644 index 00000000..14f708bb --- /dev/null +++ b/src/backend/starter.ts @@ -0,0 +1,53 @@ +// npx tsc -p tsconfig.node.json +// node ./dist/backend/starter.js + +import './db/database.js' +import './ssh/ssh.js'; +import chalk from 'chalk'; + +const fixedIconSymbol = '🚀'; + +const getTimeStamp = (): string => { + return chalk.gray(`[${new Date().toLocaleTimeString()}]`); +}; + +const formatMessage = (level: string, colorFn: chalk.Chalk, message: string): string => { + return `${getTimeStamp()} ${colorFn(`[${level.toUpperCase()}]`)} ${chalk.hex('#1e3a8a')(`[${fixedIconSymbol}]`)} ${message}`; +}; + +const logger = { + info: (msg: string): void => { + console.log(formatMessage('info', chalk.cyan, msg)); + }, + warn: (msg: string): void => { + console.warn(formatMessage('warn', chalk.yellow, msg)); + }, + error: (msg: string, err?: unknown): void => { + console.error(formatMessage('error', chalk.redBright, msg)); + if (err) console.error(err); + }, + success: (msg: string): void => { + console.log(formatMessage('success', chalk.greenBright, msg)); + }, + debug: (msg: string): void => { + if (process.env.NODE_ENV !== 'production') { + console.debug(formatMessage('debug', chalk.magenta, msg)); + } + } +}; + +(async () => { + try { + logger.info("Starting all backend servers..."); + + logger.success("All servers started successfully"); + + process.on('SIGINT', () => { + logger.info("Shutting down servers..."); + process.exit(0); + }); + } catch (error) { + logger.error("Failed to start servers:", error); + process.exit(1); + } +})(); \ No newline at end of file diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx new file mode 100644 index 00000000..d21b65f7 --- /dev/null +++ b/src/components/ui/accordion.tsx @@ -0,0 +1,64 @@ +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDownIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Accordion({ + ...props +}: React.ComponentProps) { + return +} + +function AccordionItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + + ) +} + +function AccordionContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + +
{children}
+
+ ) +} + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx new file mode 100644 index 00000000..14213546 --- /dev/null +++ b/src/components/ui/alert.tsx @@ -0,0 +1,66 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription } diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 00000000..d05bbc6c --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx new file mode 100644 index 00000000..defeb01f --- /dev/null +++ b/src/components/ui/checkbox.tsx @@ -0,0 +1,30 @@ +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { CheckIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Checkbox({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ) +} + +export { Checkbox } diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx new file mode 100644 index 00000000..6d51b6ce --- /dev/null +++ b/src/components/ui/popover.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "@/lib/utils" + +function Popover({ + ...props +}: React.ComponentProps) { + return +} + +function PopoverTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function PopoverContent({ + className, + align = "center", + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function PopoverAnchor({ + ...props +}: React.ComponentProps) { + return +} + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } diff --git a/src/components/ui/scroll-area.tsx b/src/components/ui/scroll-area.tsx new file mode 100644 index 00000000..9376f594 --- /dev/null +++ b/src/components/ui/scroll-area.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" + +import { cn } from "@/lib/utils" + +function ScrollArea({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + + ) +} + +function ScrollBar({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { ScrollArea, ScrollBar } diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx new file mode 100644 index 00000000..b0363e3f --- /dev/null +++ b/src/components/ui/switch.tsx @@ -0,0 +1,29 @@ +import * as React from "react" +import * as SwitchPrimitive from "@radix-ui/react-switch" + +import { cn } from "@/lib/utils" + +function Switch({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { Switch } diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx new file mode 100644 index 00000000..3d6f3acf --- /dev/null +++ b/src/components/ui/tabs.tsx @@ -0,0 +1,64 @@ +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +function Tabs({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsList({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsTrigger({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/tsconfig.json b/tsconfig.json index afa1ec61..1989a7ce 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,9 @@ ], "compilerOptions": { "baseUrl": ".", + "module": "module", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, "paths": { "@/*": ["./src/*"] } diff --git a/tsconfig.node.json b/tsconfig.node.json index 661cbaaa..9d2aee90 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -3,17 +3,13 @@ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", "target": "ES2023", "lib": ["ES2023"], - "module": "ESNext", + "module": "nodenext", "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, + "moduleResolution": "nodenext", "verbatimModuleSyntax": true, "moduleDetection": "force", - "noEmit": true, - - /* Linting */ + "noEmit": false, + "outDir": "./dist/backend", "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, @@ -21,5 +17,5 @@ "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, - "include": ["vite.config.ts"] + "include": ["src/backend/**/*.ts"] } \ No newline at end of file