diff --git a/README.md b/README.md index 8d1999c6..01c893f1 100644 --- a/README.md +++ b/README.md @@ -32,16 +32,22 @@ Termix is an open-source forever free self-hosted SSH (other protocols planned, # Features - SSH - Split Screen (Up to 4) & Tab System +- User Authentication +- Data Persistence # Planned Features -- Database to Store Connection Details +- Organize Hosts (folders, tags, etc.) - VNC - RDP - SFTP (build in file transfer) - ChatGPT/Ollama Integration (for commands) -- Login Screen -- User Management - Apps (like notes, AI, etc) +- Terminal Themes +- User Management (roles, permissions, etc.) +- SSH Tunneling +- More Authentication Methods +- Share Hosts +- More Security Features (like 2FA, optionally store host passwords, etc.) # Installation Visit the Termix [Wiki](https://github.com/LukeGus/Termix/wiki) for information on how to install Termix. You can also use these links to go directly to guide. [Docker](https://github.com/LukeGus/Termix/wiki/Docker) or [Manual](https://github.com/LukeGus/Termix/wiki/Manual). diff --git a/package-lock.json b/package-lock.json index b4e379ab..7c67313c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@tiptap/starter-kit": "^2.11.5", "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.5.0", + "bcrypt": "^5.1.1", "cors": "^2.8.5", "crypto": "^1.0.1", "dayjs": "^1.11.13", @@ -1203,6 +1204,71 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@mongodb-js/saslprep": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.0.tgz", @@ -2590,6 +2656,12 @@ "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", "license": "MIT" }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -2626,6 +2698,18 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2643,6 +2727,15 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -2659,6 +2752,26 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2881,7 +2994,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/base64id": { @@ -2893,6 +3005,20 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -2945,7 +3071,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -3107,6 +3232,15 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -3136,13 +3270,27 @@ "dev": true, "license": "MIT" }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -3519,6 +3667,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -3630,6 +3784,12 @@ "react": "^16.8.0 || ^17.0.1 || ^18.0.0" } }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -4433,6 +4593,42 @@ "node": ">= 0.6" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4487,6 +4683,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -4552,6 +4769,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -4693,6 +4931,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -4730,6 +4974,19 @@ "node": ">= 0.8" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -4778,6 +5035,17 @@ "node": ">=0.8.19" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -4983,6 +5251,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-generator-function": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", @@ -5777,7 +6054,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -5786,6 +6062,58 @@ "node": "*" } }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/mongodb": { "version": "6.14.2", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.14.2.tgz", @@ -5932,6 +6260,54 @@ "node": ">= 0.6" } }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -5983,6 +6359,34 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -6100,6 +6504,15 @@ "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", @@ -6223,6 +6636,15 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -6656,6 +7078,20 @@ "react-dom": ">=16.6.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/recharts": { "version": "2.15.1", "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.1.tgz", @@ -6771,6 +7207,22 @@ "node": ">=4" } }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/rollup": { "version": "4.32.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.32.1.tgz", @@ -6998,6 +7450,12 @@ "node": ">= 0.8.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -7160,6 +7618,12 @@ "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", "license": "MIT" }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, "node_modules/socket.io": { "version": "4.8.1", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", @@ -7337,6 +7801,29 @@ "node": ">= 0.8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", @@ -7435,6 +7922,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -7494,6 +7993,29 @@ "node": ">=6" } }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -7730,6 +8252,12 @@ "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/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -7973,6 +8501,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -7983,6 +8520,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.17.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", diff --git a/package.json b/package.json index 45c61e4b..4b432af5 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@tiptap/starter-kit": "^2.11.5", "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.5.0", + "bcrypt": "^5.1.1", "cors": "^2.8.5", "crypto": "^1.0.1", "dayjs": "^1.11.13", diff --git a/src/App.jsx b/src/App.jsx index 2a1c665e..7dc8af8d 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -181,7 +181,7 @@ function App() { const handleSaveHost = () => { let hostConfig = { - name: addHostForm.name, + name: addHostForm.name || addHostForm.ip, ip: addHostForm.ip, user: addHostForm.user, password: addHostForm.authMethod === 'password' ? addHostForm.password : undefined, @@ -273,10 +273,10 @@ function App() { const deleteHost = (hostConfig) => { if (userRef.current) { userRef.current.deleteHost({ - hostConfig, + hostId: hostConfig._id, }); } - } + }; const updateEditHostForm = (hostConfig) => { if (hostConfig) { @@ -289,18 +289,16 @@ function App() { const handleEditHost = () => { if (editHostForm.ip && editHostForm.user && ((editHostForm.authMethod === 'password' && editHostForm.password) || (editHostForm.authMethod === 'rsaKey' && editHostForm.rsaKey)) && editHostForm.port && editHostForm.authMethod !== 'Select Auth') { - const user = getUser(); editHostForm.rememberHost = true; - if (user && currentHostConfig) { - userRef.current.editExistingHost({ - userId: user.id, + if (currentHostConfig) { + userRef.current.editHost({ oldHostConfig: currentHostConfig, newHostConfig: editHostForm, }); setIsEditHostHidden(true); } else { - console.error("User or currentHostConfig is null"); + alert("Host not found"); } } else { alert("Please fill out all fields."); diff --git a/src/Launchpad.jsx b/src/Launchpad.jsx index 76408d4e..c42d6d50 100644 --- a/src/Launchpad.jsx +++ b/src/Launchpad.jsx @@ -27,13 +27,12 @@ function Launchpad({ useEffect(() => { const handleClickOutside = (event) => { - // Close the launchpad when neither form is visible and no error is showing if ( launchpadRef.current && !launchpadRef.current.contains(event.target) && - isAddHostHidden && // Only close if addHost form is hidden - isEditHostHidden && // Only close if editHost form is hidden - isErrorHidden // Only close if error is hidden + isAddHostHidden && + isEditHostHidden && + isErrorHidden ) { onClose(); } @@ -47,8 +46,8 @@ function Launchpad({ }, [onClose, isAddHostHidden, isEditHostHidden, isErrorHidden]); const handleEditHostClick = () => { - setIsAddHostHidden(false); // Open the form for editing - setActiveApp('hostViewer'); // Set active app to HostViewer + setIsAddHostHidden(false); + setActiveApp('hostViewer'); }; return ( @@ -174,10 +173,10 @@ function Launchpad({ connectToHost={connectToHost} setIsAddHostHidden={setIsAddHostHidden} deleteHost={deleteHost} - editHost={editHost} // Pass editHost here + editHost={editHost} createFolder={createFolder} moveHostToFolder={moveHostToFolder} - onEditHostClick={handleEditHostClick} // Pass the handler to the form + onEditHostClick={handleEditHostClick} /> )} diff --git a/src/Terminal.jsx b/src/Terminal.jsx index fa985aa3..32f08215 100644 --- a/src/Terminal.jsx +++ b/src/Terminal.jsx @@ -63,10 +63,10 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible }, ref) => { const socket = io( window.location.hostname === "localhost" - ? "http://localhost:8081" // Modified path here + ? "http://localhost:8081" : "/", { - path: "/ssh.io/socket.io", // Same path, no need to modify + path: "/ssh.io/socket.io", transports: ["websocket", "polling"], } ); diff --git a/src/User.jsx b/src/User.jsx index 9dbcca2a..2c9fef84 100644 --- a/src/User.jsx +++ b/src/User.jsx @@ -1,219 +1,237 @@ -import { useRef, forwardRef, useImperativeHandle } from "react"; +import { useRef, forwardRef, useImperativeHandle, useEffect } from "react"; import io from "socket.io-client"; import PropTypes from "prop-types"; -let socket; +const SOCKET_URL = window.location.hostname === "localhost" + ? "http://localhost:8082/database.io" + : "/database.io"; -if (!socket) { - socket = io( - window.location.hostname === "localhost" - ? "http://localhost:8082/database.io" - : "/database.io", - { - path: "/database.io/socket.io", - transports: ["websocket", "polling"], - } - ); -} +const socket = io(SOCKET_URL, { + path: "/database.io/socket.io", + transports: ["websocket", "polling"], + autoConnect: false, +}); -export const User = forwardRef(({ onLoginSuccess, onCreateSuccess, onDeleteSuccess, onFailure }, ref) => { +export const User = forwardRef(({ + onLoginSuccess, + onCreateSuccess, + onDeleteSuccess, + onFailure + }, ref) => { const socketRef = useRef(socket); const currentUser = useRef(null); - const createUser = (userConfig) => { - if (socketRef.current) { - socketRef.current.emit("createUser", { - username: userConfig.username, - password: userConfig.password, + useEffect(() => { + socketRef.current.connect(); + return () => socketRef.current.disconnect(); + }, []); + + useEffect(() => { + const verifySession = async () => { + const storedSession = localStorage.getItem("sessionToken"); + if (!storedSession || storedSession === "undefined") return; + + try { + const response = await new Promise((resolve) => { + socketRef.current.emit("verifySession", { sessionToken: storedSession }, resolve); + }); + + if (response?.success) { + currentUser.current = { + id: response.user.id, + username: response.user.username, + sessionToken: storedSession, + }; + onLoginSuccess(response.user); + } else { + localStorage.removeItem("sessionToken"); + onFailure("Session expired"); + } + } catch (error) { + onFailure(error.message); + } + }; + + verifySession(); + }, []); + + const createUser = async (userConfig) => { + try { + const response = await new Promise((resolve) => { + socketRef.current.emit("createUser", userConfig, resolve); }); - socketRef.current.once("userCreated", (data) => { + if (response?.user?.sessionToken) { currentUser.current = { - id: data.user._id, - username: data.user.username, - sessionToken: data.user.sessionToken, + id: response.user.id, + username: response.user.username, + sessionToken: response.user.sessionToken, }; - localStorage.setItem('sessionToken', data.user.sessionToken); - onCreateSuccess(data); - }); - - socketRef.current.once("error", (error) => { - console.error(error); - const errorMsg = (error && typeof error === 'object' && error !== null) - ? error.error || error.message || 'An error occurred' - : String(error); - onFailure(errorMsg); - }); + localStorage.setItem("sessionToken", response.user.sessionToken); + onCreateSuccess(response.user); + } else { + throw new Error(response?.error || "User creation failed"); + } + } catch (error) { + onFailure(error.message); } }; - const loginUser = (userConfig) => { - if (socketRef.current) { - setTimeout(() => { - socketRef.current.emit("loginUser", { - username: userConfig.username, - password: userConfig.password, - sessionToken: userConfig.sessionToken, - }); + const loginUser = async ({ username, password, sessionToken }) => { + try { + const response = await new Promise((resolve) => { + const credentials = sessionToken ? { sessionToken } : { username, password }; + socketRef.current.emit("loginUser", credentials, resolve); + }); - socketRef.current.once("userFound", (data) => { - currentUser.current = { - id: data._id, - username: data.username, - sessionToken: data.sessionToken, - }; - localStorage.setItem('sessionToken', data.sessionToken); - onLoginSuccess(data); - }); - - socketRef.current.once("error", (error) => { - console.error(error); - const errorMsg = (error && typeof error === 'object' && error !== null) - ? error.error || error.message || 'An error occurred' - : String(error); - onFailure(errorMsg); - }); - }, 500); + if (response?.success) { + currentUser.current = { + id: response.user.id, + username: response.user.username, + sessionToken: response.user.sessionToken, + }; + localStorage.setItem("sessionToken", response.user.sessionToken); + onLoginSuccess(response.user); + } else { + throw new Error(response?.error || "Login failed"); + } + } catch (error) { + onFailure(error.message); } }; const logoutUser = () => { - localStorage.removeItem('sessionToken'); + localStorage.removeItem("sessionToken"); currentUser.current = null; + onLoginSuccess(null); }; - const deleteUser = () => { - if (currentUser.current?.id && socketRef.current) { - socketRef.current.emit("deleteUser", { - userId: currentUser.current.id, + const deleteUser = async () => { + if (!currentUser.current) return onFailure("No user logged in"); + + try { + const response = await new Promise((resolve) => { + socketRef.current.emit("deleteUser", { + userId: currentUser.current.id, + sessionToken: currentUser.current.sessionToken, + }, resolve); }); - socketRef.current.once("userDeleted", (data) => { - onDeleteSuccess(data); - currentUser.current = null; - localStorage.removeItem('sessionToken'); - }); - - socketRef.current.once("error", (error) => { - console.error(error); - const errorMsg = (error && typeof error === 'object' && error !== null) - ? error.error || error.message || 'An error occurred' - : String(error); - onFailure(errorMsg); - }); - } else { - onFailure("No user is currently logged in."); + if (response?.success) { + logoutUser(); + onDeleteSuccess(response); + } else { + throw new Error(response?.error || "User deletion failed"); + } + } catch (error) { + onFailure(error.message); } }; - const saveHost = (hostConfig) => { - if (currentUser.current?.id && socketRef.current) { - socketRef.current.emit("saveHostConfig", { - userId: currentUser.current.id, - hostConfig: hostConfig, + const saveHost = async (hostConfig) => { + if (!currentUser.current) return onFailure("Not authenticated"); + + try { + const response = await new Promise((resolve) => { + socketRef.current.emit("saveHostConfig", { + userId: currentUser.current.id, + sessionToken: currentUser.current.sessionToken, + ...hostConfig + }, resolve); }); - socketRef.current.once("error", (error) => { - onFailure(error); - }); - } else { - onFailure("No user is currently logged in."); + if (!response?.success) { + throw new Error(response?.error || "Failed to save host"); + } + } catch (error) { + onFailure(error.message); } - } + }; - const getUser = () => { - return currentUser.current; - } + const getAllHosts = async () => { + if (!currentUser.current) return []; - const getAllHosts = () => { - return new Promise((resolve, reject) => { - if (currentUser.current?.id && socketRef.current) { + try { + const response = await new Promise((resolve) => { socketRef.current.emit("getHosts", { userId: currentUser.current.id, - }); + sessionToken: currentUser.current.sessionToken, + }, resolve); + }); - socketRef.current.once("hostsFound", (data) => { - if (data && Array.isArray(data)) { - resolve(data); - } else { - reject("Invalid data received."); - } - }); - - socketRef.current.once("error", (error) => { - console.error(error); - const errorMsg = (error && typeof error === 'object' && error !== null) - ? error.error || error.message || 'An error occurred' - : String(error); - reject(errorMsg); - }); + if (response?.success) { + return response.hosts; } else { - reject("No user is currently logged in."); + throw new Error(response?.error || "Failed to fetch hosts"); } - }); - }; - - const deleteHost = (hostConfig) => { - if (currentUser.current?.id && socketRef.current) { - socketRef.current.emit("deleteHost", { - userId: currentUser.current.id, - hostConfig: hostConfig, - }); - - socketRef.current.once("error", (error) => { - onFailure(error); - }); - } else { - onFailure("No user is currently logged in."); - } - } - - const editExistingHost = ({ userId, oldHostConfig, newHostConfig }) => { - if (currentUser.current?.id && socketRef.current) { - socketRef.current.emit("editHost", { - userId: userId, - oldHostConfig: oldHostConfig, - newHostConfig: newHostConfig, - }); - - socketRef.current.once("error", (error) => { - onFailure(error); - }); - } else { - onFailure("No user is currently logged in."); + } catch (error) { + onFailure(error.message); + return []; } }; - const createFolder = (folderName) => { - if (currentUser.current?.id && socketRef.current) { - socketRef.current.emit("createFolder", { - userId: currentUser.current.id, - folderName: folderName, + const deleteHost = async ({ hostId }) => { + if (!currentUser.current) return onFailure("Not authenticated"); + + try { + const response = await new Promise((resolve) => { + socketRef.current.emit("deleteHost", { + userId: currentUser.current.id, + sessionToken: currentUser.current.sessionToken, + hostId: hostId, + }, resolve); }); - socketRef.current.once("error", (error) => { - onFailure(error); - }); - } else { - onFailure("No user is currently logged in."); + if (!response?.success) { + throw new Error(response?.error || "Failed to delete host"); + } + } catch (error) { + onFailure(error.message); } - } + }; - const moveHostToFolder = (folderName, hostConfig) => { - if (currentUser.current?.id && socketRef.current) { - socketRef.current.emit("moveHostToFolder", { - userId: currentUser.current.id, - folderName: folderName, - hostConfig: hostConfig, + const editHost = async ({ oldHostConfig, newHostConfig }) => { + if (!currentUser.current) return onFailure("Not authenticated"); + + try { + console.log('Editing host with configs:', { oldHostConfig, newHostConfig }); + const response = await new Promise((resolve) => { + socketRef.current.emit("editHost", { + userId: currentUser.current.id, + sessionToken: currentUser.current.sessionToken, + oldHostConfig, + newHostConfig, + }, resolve); }); - socketRef.current.once("error", (error) => { - onFailure(error); - }); - } else { - onFailure("No user is currently logged in."); + if (!response?.success) { + throw new Error(response?.error || "Failed to edit host"); + } + } catch (error) { + onFailure(error.message); } - } + }; + + const shareHost = async (hostId, targetUsername) => { + if (!currentUser.current) return onFailure("Not authenticated"); + + try { + const response = await new Promise((resolve) => { + socketRef.current.emit("shareHost", { + userId: currentUser.current.id, + sessionToken: currentUser.current.sessionToken, + hostId, + targetUsername, + }, resolve); + }); + + if (!response?.success) { + throw new Error(response?.error || "Failed to share host"); + } + } catch (error) { + onFailure(error.message); + } + }; useImperativeHandle(ref, () => ({ createUser, @@ -221,15 +239,14 @@ export const User = forwardRef(({ onLoginSuccess, onCreateSuccess, onDeleteSucce logoutUser, deleteUser, saveHost, - getUser, getAllHosts, deleteHost, - editExistingHost, - createFolder, - moveHostToFolder, + shareHost, + editHost, + getUser: () => currentUser.current, })); - return
; + return null; }); User.displayName = "User"; diff --git a/src/apps/HostViewer.jsx b/src/apps/HostViewer.jsx index 8bae1e6f..a38dd7dd 100644 --- a/src/apps/HostViewer.jsx +++ b/src/apps/HostViewer.jsx @@ -4,46 +4,38 @@ import { Button } from "@mui/joy"; function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, editHost }) { const [hosts, setHosts] = useState([]); - const [initialLoadComplete, setInitialLoadComplete] = useState(false); + const [isLoading, setIsLoading] = useState(true); const isMounted = useRef(true); + const fetchHosts = async () => { + try { + const savedHosts = await getHosts(); + if (isMounted.current) { + setHosts(savedHosts || []); + setIsLoading(false); + } + } catch (error) { + console.error("Host fetch failed:", error); + if (isMounted.current) { + setHosts([]); + setIsLoading(false); + } + } + }; + useEffect(() => { isMounted.current = true; + fetchHosts(); - async function fetchInitialHosts() { - try { - const savedHosts = await getHosts(); - if (isMounted.current) { - setHosts(savedHosts || []); - setInitialLoadComplete(true); - } - } catch (error) { - console.error("Initial host fetch failed:", error); - if (isMounted.current) { - setHosts([]); - setInitialLoadComplete(true); - } - } - } - - fetchInitialHosts(); - - const intervalId = setInterval(async () => { - try { - const savedHosts = await getHosts(); - if (isMounted.current) { - setHosts(savedHosts || []); - } - } catch (error) { - console.error("Periodic host update failed:", error); - } + const intervalId = setInterval(() => { + fetchHosts(); }, 2000); return () => { isMounted.current = false; clearInterval(intervalId); }; - }, [getHosts]); + }, []); return (
@@ -61,25 +53,29 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
- {hosts.length > 0 ? ( + {isLoading ? ( +

Loading hosts...

+ ) : hosts.length > 0 ? (
{hosts.map((hostWrapper, index) => { - const hostConfig = hostWrapper.hostConfig || {}; + const hostConfig = hostWrapper.config || {}; + + if (!hostConfig) { + return null; + } return (

{hostConfig.name || hostConfig.ip}

- {hostConfig.user ? `${hostConfig.user}@${hostConfig.ip}` : hostConfig.ip}:{hostConfig.port} + {hostConfig.user ? `${hostConfig.user}@${hostConfig.ip}` : `${hostConfig.ip}:${hostConfig.port}`}

) : ( -

Hosts are either loading or do not exist...

+

No hosts available...

)}
diff --git a/src/backend/database.cjs b/src/backend/database.cjs index c2c3cf2e..c8dbbaed 100644 --- a/src/backend/database.cjs +++ b/src/backend/database.cjs @@ -1,341 +1,385 @@ -const http = require("http"); -const socketIo = require("socket.io"); -const mongoose = require("mongoose"); +const http = require('http'); +const socketIo = require('socket.io'); +const mongoose = require('mongoose'); +const bcrypt = require('bcrypt'); const crypto = require('crypto'); require('dotenv').config(); +const logger = { + info: (...args) => console.log(`🔧 [${new Date().toISOString()}] INFO:`, ...args), + error: (...args) => console.error(`❌ [${new Date().toISOString()}] ERROR:`, ...args), + warn: (...args) => console.warn(`⚠️ [${new Date().toISOString()}] WARN:`, ...args), + debug: (...args) => console.debug(`🔍 [${new Date().toISOString()}] DEBUG:`, ...args) +}; + const server = http.createServer(); const io = socketIo(server, { - path: "/database.io/socket.io", - cors: { - origin: "*", - methods: ["GET", "POST"], - credentials: true - }, - allowEIO3: true + path: '/database.io/socket.io', + cors: { origin: '*', methods: ['GET', 'POST'] } }); -const dbNamespace = io.of("/database.io"); - -async function connectToMongoDB() { - try { - const mongoUrl = process.env.MONGO_URL || 'mongodb://mongodb:27017/termix'; - await mongoose.connect(mongoUrl, {}); - console.log('Connected to MongoDB'); - - const db = mongoose.connection.db; - - // Create the 'users' collection if it doesn't exist - const collections = await db.listCollections().toArray(); - if (!collections.find(col => col.name === 'users')) { - await db.createCollection('users'); - console.log('Successfully created collection: users'); - } - } catch (error) { - console.error('Error connecting to MongoDB:', error); - } -} - const userSchema = new mongoose.Schema({ username: { type: String, required: true, unique: true }, password: { type: String, required: true }, - sessionToken: { type: String, required: true }, - sshConnections: { type: [Object], default: [] }, + sessionToken: { type: String, required: true } +}); + +const hostSchema = new mongoose.Schema({ + name: { type: String, required: true }, + config: { type: String, required: true }, + users: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }], + createdBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' } }); const User = mongoose.model('User', userSchema); +const Host = mongoose.model('Host', hostSchema); -async function createUser(username, password) { +const getEncryptionKey = (userId, sessionToken) => { + return crypto.scryptSync(`${userId}-${sessionToken}`, 'salt', 32); +}; + +const encryptData = (data, userId, sessionToken) => { try { - const userExists = await User.findOne({ username }); - if (userExists) { - return { error: "User already exists for username" }; - } - - const sessionToken = crypto.randomBytes(64).toString('hex'); - const newUser = new User({ username, password, sessionToken }); - await newUser.save(); - return { success: true, user: { _id: newUser._id, username: newUser.username, sessionToken: newUser.sessionToken } }; - } catch (err) { - return { error: 'Error creating user: ' + err.message }; + const iv = crypto.randomBytes(16); + const cipher = crypto.createCipheriv('aes-256-gcm', getEncryptionKey(userId, sessionToken), iv); + const encrypted = Buffer.concat([cipher.update(JSON.stringify(data)), cipher.final()]); + return `${iv.toString('hex')}:${encrypted.toString('hex')}:${cipher.getAuthTag().toString('hex')}`; + } catch (error) { + logger.error('Encryption failed:', error); + return null; } -} +}; -async function loginUser(username, password) { +const decryptData = (encryptedData, userId, sessionToken) => { try { - const user = await User.findOne({ username, password }); - if (user) { - if (!user.sessionToken) { - user.sessionToken = crypto.randomBytes(64).toString('hex'); - await user.save(); + const [ivHex, contentHex, authTagHex] = encryptedData.split(':'); + const iv = Buffer.from(ivHex, 'hex'); + const content = Buffer.from(contentHex, 'hex'); + const authTag = Buffer.from(authTagHex, 'hex'); + + const decipher = crypto.createDecipheriv('aes-256-gcm', getEncryptionKey(userId, sessionToken), iv); + decipher.setAuthTag(authTag); + + return JSON.parse(Buffer.concat([decipher.update(content), decipher.final()]).toString()); + } catch (error) { + logger.error('Decryption failed:', error); + return null; + } +}; + +mongoose.connect(process.env.MONGO_URL || 'mongodb://localhost:27017/termix') + .then(() => logger.info('Connected to MongoDB')) + .catch(err => logger.error('MongoDB connection error:', err)); + +io.of('/database.io').on('connection', (socket) => { + socket.on('createUser', async ({ username, password }, callback) => { + try { + logger.debug(`Creating user: ${username}`); + + if (await User.exists({ username })) { + logger.warn(`Username already exists: ${username}`); + return callback({ error: 'Username already exists' }); } - return { - _id: user._id, - username: user.username, - sessionToken: user.sessionToken, - }; - } else { - return { error: 'User not found or incorrect credentials for username' }; - } - } catch (err) { - return { error: 'Error checking user: ' + err.message }; - } -} -async function loginWithToken(sessionToken) { - try { - const user = await User.findOne({ sessionToken }); - if (user) { - return { - _id: user._id, - username: user.username, - sessionToken: user.sessionToken, - }; - } else { - return { error: 'Invalid session token' }; - } - } catch (err) { - return { error: 'Error checking session token: ' + err.message }; - } -} - -async function deleteUser(userId) { - try { - const user = await User.findById(userId); - if (user) { - await User.deleteOne({ _id: userId }); - return { success: true }; - } else { - return { error: 'User not found' }; - } - } catch (err) { - return { error: 'Error removing user: ' + err.message }; - } -} - -async function saveHostConfig(userId, hostConfig) { - try { - const user = await User.findById(userId); - if (user) { - user.sshConnections.push(hostConfig); - await user.save(); - return { success: true }; - } else { - return { error: 'User not found' }; - } - } catch (err) { - return { error: 'Error saving host config: ' + err.message }; - } -} - -async function getHosts(userId) { - try { - const user = await User.findById(userId); - if (user) { - return user.sshConnections; - } else { - return { error: 'User not found' }; - } - } catch (err) { - return { error: 'Error getting hosts: ' + err.message }; - } -} - -async function deleteHost(userId, hostConfig) { - try { - const user = await User.findById(userId); - if (user) { - user.sshConnections = user.sshConnections.filter(connection => { - const matches = - connection.name === hostConfig.name && - connection.ip === hostConfig.ip && - connection.port === hostConfig.port && - connection.user === hostConfig.user; - - return !matches; + const sessionToken = crypto.randomBytes(64).toString('hex'); + const user = await User.create({ + username, + password: await bcrypt.hash(password, 10), + sessionToken }); - await user.save(); - return { success: true }; - } else { - return { error: 'User not found' }; + logger.info(`User created: ${username}`); + callback({ success: true, user: { + id: user._id, + username: user.username, + sessionToken + }}); + } catch (error) { + logger.error('User creation error:', error); + callback({ error: 'User creation failed' }); } - } catch (err) { - return { error: 'Error deleting host: ' + err.message }; - } -} + }); -async function editHost(userId, oldHostConfig, newHostConfig) { - try { - const user = await User.findById(userId); - if (user) { - user.sshConnections = user.sshConnections.map(connection => { - const matches = - connection.hostConfig.name === oldHostConfig.name && - connection.hostConfig.ip === oldHostConfig.ip && - connection.hostConfig.port === oldHostConfig.port && - connection.hostConfig.user === oldHostConfig.user; - - if (matches) { - return { hostConfig: newHostConfig }; - } else { - return connection; - } - }); - - await user.save(); - return { success: true }; - } else { - return { error: 'User not found' }; - } - } catch (err) { - return { error: 'Error editing host: ' + err.message }; - } -} - -async function createFolder(userId, folderName) { - try { - const user = await User.findById(userId); - if (user) { - user.sshConnections.push({ folderName, connections: [] }); - await user.save(); - return { success: true }; - } else { - return { error: 'User not found' }; - } - } catch (err) { - return { error: 'Error creating folder: ' + err.message }; - } -} - -async function moveHostToFolder(userId, hostConfig, folderName) { - try { - const user = await User.findById(userId); - if (user) { - const folder = user.sshConnections.find(folder => folder.folderName === folderName); - if (folder) { - folder.connections.push(hostConfig); - await user.save(); - return { success: true }; + socket.on('loginUser', async ({ username, password, sessionToken }, callback) => { + try { + let user; + if (sessionToken) { + user = await User.findOne({ sessionToken }); } else { - return { error: 'Folder not found' }; + user = await User.findOne({ username }); + if (!user || !(await bcrypt.compare(password, user.password))) { + logger.warn(`Invalid credentials for: ${username}`); + return callback({ error: 'Invalid credentials' }); + } } - } else { - return { error: 'User not found' }; - } - } catch (err) { - return { error: 'Error moving host to folder: ' + err.message }; - } -} -dbNamespace.on("connection", (socket) => { - console.log("New socket connection established on"); + if (!user) { + logger.warn('Login failed - user not found'); + return callback({ error: 'Invalid credentials' }); + } - socket.on("createUser", async (data) => { - const { username, password } = data; - if (!username || !password) { - socket.emit("error", "Please provide both username and password"); - return; + logger.info(`User logged in: ${user.username}`); + callback({ success: true, user: { + id: user._id, + username: user.username, + sessionToken: user.sessionToken + }}); + } catch (error) { + logger.error('Login error:', error); + callback({ error: 'Login failed' }); } - const result = await createUser(username, password); - socket.emit(result.error ? "error" : "userCreated", result); - console.log(result.error || `User created`); }); - socket.on("loginUser", async (data) => { - const { username, password, sessionToken } = data; - let result; - if (sessionToken) { - result = await loginWithToken(sessionToken); - } else if (username && password) { - result = await loginUser(username, password); - } else { - socket.emit("error", "Please provide both username and password or a session token"); - return; + socket.on('saveHostConfig', async ({ userId, sessionToken, hostConfig }, callback) => { + try { + if (!userId || !sessionToken) { + logger.warn('Missing authentication parameters'); + return callback({ error: 'Authentication required' }); + } + + if (!hostConfig || typeof hostConfig !== 'object') { + logger.warn('Invalid host config format'); + return callback({ error: 'Invalid host configuration' }); + } + + if (!hostConfig.ip || !hostConfig.user) { + logger.warn('Missing required fields:', hostConfig); + return callback({ error: 'IP and User are required' }); + } + + const user = await User.findOne({ _id: userId, sessionToken }); + if (!user) { + logger.warn(`Invalid session for user: ${userId}`); + return callback({ error: 'Invalid session' }); + } + + const cleanConfig = { + name: hostConfig.name.trim(), + ip: hostConfig.ip.trim(), + user: hostConfig.user.trim(), + port: hostConfig.port || 22, + password: hostConfig.password?.trim() || undefined, + rsaKey: hostConfig.rsaKey?.trim() || undefined + }; + + const finalName = cleanConfig.name || cleanConfig.ip; + + const existingHost = await Host.findOne({ + name: finalName, + createdBy: userId + }); + + if (existingHost) { + logger.warn(`Host with name ${finalName} already exists for user: ${userId}`); + return callback({ error: 'Host with this name already exists' }); + } + + const encryptedConfig = encryptData(cleanConfig, userId, sessionToken); + if (!encryptedConfig) { + logger.error('Encryption failed for host config'); + return callback({ error: 'Configuration encryption failed' }); + } + + await Host.create({ + name: finalName, + config: encryptedConfig, + users: [userId], + createdBy: userId + }); + + logger.info(`Host created successfully: ${finalName}`); + callback({ success: true }); + } catch (error) { + logger.error('Host save error:', error); + callback({ error: `Host save failed: ${error.message}` }); } - socket.emit(result.error ? "error" : "userFound", result); - console.log(result.error || `User logged in`); }); - socket.on("deleteUser", async (data) => { - const { userId } = data; - if (!userId) { - socket.emit("error", "User ID is required"); - return; + socket.on('getHosts', async ({ userId, sessionToken }, callback) => { + try { + const user = await User.findOne({ _id: userId, sessionToken }); + if (!user) { + logger.warn(`Invalid session for user: ${userId}`); + return callback({ error: 'Invalid session' }); + } + + const hosts = await Host.find({ users: userId }); + const decryptedHosts = hosts.map(host => ({ + ...host.toObject(), + config: decryptData(host.config, userId, sessionToken) + })).filter(host => host.config); + + callback({ success: true, hosts: decryptedHosts }); + } catch (error) { + logger.error('Get hosts error:', error); + callback({ error: 'Failed to fetch hosts' }); } - const result = await deleteUser(userId); - socket.emit(result.error ? "error" : "userDeleted", result); - console.log(result.error || `User deleted`); }); - socket.on("saveHostConfig", async (data) => { - const { userId, hostConfig } = data; - if (!userId || !hostConfig) { - socket.emit("error", "User ID and host config are required"); - return; + socket.on('deleteHost', async ({ userId, sessionToken, hostId }, callback) => { + try { + logger.debug(`Deleting host: ${hostId} for user: ${userId}`); + + if (!userId || !sessionToken) { + logger.warn('Missing authentication parameters'); + return callback({ error: 'Authentication required' }); + } + + if (!hostId || typeof hostId !== 'string') { + logger.warn('Invalid host ID format'); + return callback({ error: 'Invalid host ID' }); + } + + const user = await User.findOne({ _id: userId, sessionToken }); + if (!user) { + logger.warn(`Invalid session for user: ${userId}`); + return callback({ error: 'Invalid session' }); + } + + const result = await Host.deleteOne({ _id: hostId, createdBy: userId }); + if (result.deletedCount === 0) { + logger.warn(`Host not found or not authorized: ${hostId}`); + return callback({ error: 'Host not found or not authorized' }); + } + + logger.info(`Host deleted: ${hostId}`); + callback({ success: true }); + } catch (error) { + logger.error('Host deletion error:', error); + callback({ error: `Host deletion failed: ${error.message}` }); } - const result = await saveHostConfig(userId, hostConfig); - socket.emit(result.error ? "error" : "hostConfigSaved", result); - console.log(result.error || `Host config saved`); }); - socket.on("getHosts", async (data) => { - const { userId } = data; - if (!userId) { - socket.emit("error", "User ID is required"); - return; + socket.on('shareHost', async ({ userId, sessionToken, hostId, targetUsername }, callback) => { + try { + logger.debug(`Sharing host ${hostId} with ${targetUsername}`); + + const user = await User.findOne({ _id: userId, sessionToken }); + if (!user) { + logger.warn(`Invalid session for user: ${userId}`); + return callback({ error: 'Invalid session' }); + } + + const targetUser = await User.findOne({ username: targetUsername }); + if (!targetUser) { + logger.warn(`Target user not found: ${targetUsername}`); + return callback({ error: 'User not found' }); + } + + const host = await Host.findOne({ _id: hostId, createdBy: userId }); + if (!host) { + logger.warn(`Host not found or unauthorized: ${hostId}`); + return callback({ error: 'Host not found' }); + } + + if (host.users.includes(targetUser._id)) { + logger.warn(`Host already shared with user: ${targetUsername}`); + return callback({ error: 'Already shared' }); + } + + host.users.push(targetUser._id); + await host.save(); + + logger.info(`Host shared successfully: ${hostId} -> ${targetUsername}`); + callback({ success: true }); + } catch (error) { + logger.error('Host sharing error:', error); + callback({ error: 'Failed to share host' }); } - const result = await getHosts(userId); - socket.emit(result.error ? "error" : "hostsFound", result); - console.log(result.error || `Hosts found`); }); - socket.on("deleteHost", async (data) => { - const { userId, hostConfig } = data; - if (!userId || !hostConfig) { - socket.emit("error", "User ID and host config are required"); - return; + socket.on('deleteUser', async ({ userId, sessionToken }, callback) => { + try { + logger.debug(`Deleting user: ${userId}`); + + const user = await User.findOne({ _id: userId, sessionToken }); + if (!user) { + logger.warn(`Invalid session for user: ${userId}`); + return callback({ error: 'Invalid session' }); + } + + await Host.deleteMany({ createdBy: userId }); + await User.deleteOne({ _id: userId }); + + logger.info(`User deleted: ${userId}`); + callback({ success: true }); + } catch (error) { + logger.error('User deletion error:', error); + callback({ error: 'Failed to delete user' }); } - const result = await deleteHost(userId, hostConfig); - socket.emit(result.error ? "error" : "hostDeleted", result); - console.log(result.error || `Host deleted`); }); - socket.on("editHost", async (data) => { - const { userId, oldHostConfig, newHostConfig } = data; - if (!userId || !oldHostConfig || !newHostConfig) { - socket.emit("error", "User ID, old host config, and new host config are required"); - return; + socket.on("editHost", async ({ userId, sessionToken, oldHostConfig, newHostConfig }, callback) => { + try { + logger.debug(`Editing host for user: ${userId}`); + + if (!oldHostConfig || !newHostConfig) { + logger.warn('Missing host configurations'); + return callback({ error: 'Missing host configurations' }); + } + + const user = await User.findOne({ _id: userId, sessionToken }); + if (!user) { + logger.warn(`Invalid session for user: ${userId}`); + return callback({ error: 'Invalid session' }); + } + + const hosts = await Host.find({ createdBy: userId }); + const host = hosts.find(h => { + const decryptedConfig = decryptData(h.config, userId, sessionToken); + return decryptedConfig && decryptedConfig.ip === oldHostConfig.ip; + }); + + if (!host) { + logger.warn(`Host not found or unauthorized`); + return callback({ error: 'Host not found' }); + } + + const cleanConfig = { + ip: newHostConfig.ip.trim(), + user: newHostConfig.user.trim(), + port: newHostConfig.port || 22, + name: newHostConfig.name.trim(), + password: newHostConfig.password?.trim() || undefined, + rsaKey: newHostConfig.rsaKey?.trim() || undefined + }; + + const encryptedConfig = encryptData(cleanConfig, userId, sessionToken); + if (!encryptedConfig) { + logger.error('Encryption failed for host config'); + return callback({ error: 'Configuration encryption failed' }); + } + + host.config = encryptedConfig; + await host.save(); + + logger.info(`Host edited successfully`); + callback({ success: true }); + } catch (error) { + logger.error('Host edit error:', error); + callback({ error: 'Failed to edit host' }); } - const result = await editHost(userId, oldHostConfig, newHostConfig); - socket.emit(result.error ? "error" : "hostEdited", result); - console.log(result.error || `Host edited`); }); - socket.on("createFolder", async (data) => { - const { userId, folderName } = data; - if (!userId || !folderName) { - socket.emit("error", "User ID and folder name are required"); - return; - } - const result = await createFolder(userId, folderName); - socket.emit(result.error ? "error" : "folderCreated", result); - console.log(result.error || `Folder created`); - }); + socket.on('verifySession', async ({ sessionToken }, callback) => { + try { + const user = await User.findOne({ sessionToken }); + if (!user) { + logger.warn(`Invalid session token: ${sessionToken}`); + return callback({ error: 'Invalid session' }); + } - socket.on("moveHostToFolder", async (data) => { - const { userId, hostConfig, folderName } = data; - if (!userId || !hostConfig || !folderName) { - socket.emit("error", "User ID, host config, and folder name are required"); - return; + callback({ success: true, user: { + id: user._id, + username: user.username + }}); + } catch (error) { + logger.error('Session verification error:', error); + callback({ error: 'Session verification failed' }); } - const result = await moveHostToFolder(userId, hostConfig, folderName); - socket.emit(result.error ? "error" : "hostMoved", result); - console.log(result.error || `Host moved to folder`); }); }); -server.listen(8082, '0.0.0.0', async () => { - console.log("Server is running on port 8082"); - await connectToMongoDB(); +server.listen(8082, () => { + logger.info('Server running on port 8082'); }); \ No newline at end of file diff --git a/src/backend/ssh.cjs b/src/backend/ssh.cjs index 643136d3..8a6c542d 100644 --- a/src/backend/ssh.cjs +++ b/src/backend/ssh.cjs @@ -4,27 +4,33 @@ const SSHClient = require("ssh2").Client; const server = http.createServer(); const io = socketIo(server, { - path: "/ssh.io/socket.io", // Corrected path for socket.io + path: "/ssh.io/socket.io", cors: { - origin: "*", // Temporarily set to '*' to allow all origins. Change to specific URLs if needed. + origin: "*", methods: ["GET", "POST"], credentials: true }, allowEIO3: true }); +const logger = { + info: (...args) => console.log(`🔧 [${new Date().toISOString()}] INFO:`, ...args), + error: (...args) => console.error(`❌ [${new Date().toISOString()}] ERROR:`, ...args), + warn: (...args) => console.warn(`⚠️ [${new Date().toISOString()}] WARN:`, ...args), + debug: (...args) => console.debug(`🔍 [${new Date().toISOString()}] DEBUG:`, ...args) +}; + io.on("connection", (socket) => { - console.log("New socket connection established"); + logger.info("New socket connection established"); let stream = null; socket.on("connectToHost", (cols, rows, hostConfig) => { if (!hostConfig || !hostConfig.ip || !hostConfig.user || (!hostConfig.password && !hostConfig.rsaKey) || !hostConfig.port) { - console.error("Invalid hostConfig received:", hostConfig); + logger.error("Invalid hostConfig received:", hostConfig); return; } - // Redact only sensitive info for logging const safeHostConfig = { ip: hostConfig.ip, port: hostConfig.port, @@ -33,57 +39,52 @@ io.on("connection", (socket) => { rsaKey: hostConfig.rsaKey ? '***REDACTED***' : undefined, }; - console.log("Received hostConfig:", safeHostConfig); + logger.info("Received hostConfig:", safeHostConfig); const { ip, port, user, password, rsaKey } = hostConfig; const conn = new SSHClient(); conn .on("ready", function () { - console.log("SSH connection established"); + logger.info("SSH connection established"); conn.shell({ term: "xterm-256color" }, function (err, newStream) { if (err) { - console.error("Error:", err.message); + logger.error("Error:", err.message); socket.emit("error", err.message); return; } stream = newStream; - // Set initial terminal size stream.setWindow(rows, cols, rows * 100, cols * 100); - // Pipe SSH output to client stream.on("data", function (data) { socket.emit("data", data); }); stream.on("close", function () { - console.log("SSH stream closed"); + logger.info("SSH stream closed"); conn.end(); }); - // Send keystrokes from terminal to SSH socket.on("data", function (data) { stream.write(data); }); - // Resize SSH terminal when client resizes socket.on("resize", ({ cols, rows }) => { if (stream && stream.setWindow) { stream.setWindow(rows, cols, rows * 100, cols * 100); } }); - // Auto-send initial terminal size to backend socket.emit("resize", { cols, rows }); }); }) .on("close", function () { - console.log("SSH connection closed"); + logger.info("SSH connection closed"); socket.emit("error", "SSH connection closed"); }) .on("error", function (err) { - console.error("Error:", err.message); + logger.error("Error:", err.message); socket.emit("error", err.message); }) .connect({ @@ -96,10 +97,10 @@ io.on("connection", (socket) => { }); socket.on("disconnect", () => { - console.log("Client disconnected"); + logger.info("Client disconnected"); }); }); server.listen(8081, '0.0.0.0', () => { - console.log("Server is running on port 8081"); + logger.info("Server is running on port 8081"); }); \ No newline at end of file diff --git a/src/modals/AddHostModal.jsx b/src/modals/AddHostModal.jsx index 41296f84..665504f2 100644 --- a/src/modals/AddHostModal.jsx +++ b/src/modals/AddHostModal.jsx @@ -20,7 +20,7 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd const handleFileChange = (e) => { const file = e.target.files[0]; if (file) { - if (file.name.endsWith('.rsa') || file.name.endsWith('.key') || file.name.endsWith('.pem') || file.name.endsWith('.der') || file.name.endsWith('.p8') || file.name.endsWith('.ssh')) { + if (file.name.endsWith('.rsa') || file.name.endsWith('.key') || file.name.endsWith('.pem') || file.name.endsWith('.der') || file.name.endsWith('.p8') || file.name.endsWith('.ssh') || file.name.endsWith('.pub')) { const reader = new FileReader(); reader.onload = (event) => { setForm({ ...form, rsaKey: event.target.result }); @@ -65,7 +65,9 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
{ event.preventDefault(); - if (isFormValid()) handleAddHost(); + if (isFormValid()) { + handleAddHost(); + } }} > @@ -176,10 +178,13 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd Remember Host setForm({ ...form, rememberHost: e.target.checked })} sx={{ color: theme.palette.text.primary, + '&.Mui-checked': { + color: theme.palette.text.primary, + }, }} /> diff --git a/src/modals/EditHostModal.jsx b/src/modals/EditHostModal.jsx index 4fefa74c..f8aecab1 100644 --- a/src/modals/EditHostModal.jsx +++ b/src/modals/EditHostModal.jsx @@ -12,8 +12,7 @@ import { DialogContent, ModalDialog, Select, - Option, - Checkbox + Option } from '@mui/joy'; import theme from '/src/theme'; @@ -41,6 +40,11 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH return true; }; + const handleEditHostInternal = (form) => { + const updatedForm = { ...form, name: form.name || form.ip }; + handleEditHost(updatedForm); + }; + useEffect(() => { if (hostConfig) { setForm({ @@ -81,7 +85,7 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH { event.preventDefault(); - if (isFormValid()) handleEditHost(); + if (isFormValid()) handleEditHostInternal(form); }} >