From 4d336136795c506344204a17e6d21802605a1843 Mon Sep 17 00:00:00 2001 From: LukeGus Date: Sun, 23 Mar 2025 21:16:51 -0500 Subject: [PATCH] Optimized pasting, fixed host naming. --- package-lock.json | 234 +++++++++++++------------- package.json | 4 +- src/App.jsx | 146 +++++++++++------ src/apps/ssh/Terminal.jsx | 235 +++++++++------------------ src/apps/user/User.jsx | 33 ++++ src/backend/database.cjs | 62 ++++++- src/modals/AddHostModal.jsx | 40 ++++- src/modals/EditHostModal.jsx | 165 +++++++++++-------- src/modals/NoAuthenticationModal.jsx | 35 ++-- 9 files changed, 540 insertions(+), 414 deletions(-) diff --git a/package-lock.json b/package-lock.json index d54d6ee6..43a23c8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@fontsource/inter": "^5.1.1", "@mui/icons-material": "^6.4.7", "@mui/joy": "^5.0.0-beta.51", - "@tailwindcss/vite": "^4.0.8", + "@tailwindcss/vite": "^4.0.15", "@tiptap/extension-link": "^2.11.5", "@tiptap/pm": "^2.11.5", "@tiptap/react": "^2.11.5", @@ -42,7 +42,7 @@ "socket.io": "^4.8.1", "socket.io-client": "^4.8.1", "ssh2": "^1.16.0", - "tailwindcss": "^4.0.8" + "tailwindcss": "^4.0.15" }, "devDependencies": { "@eslint/js": "^9.17.0", @@ -1226,15 +1226,6 @@ "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", @@ -2012,42 +2003,42 @@ "license": "MIT" }, "node_modules/@tailwindcss/node": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.8.tgz", - "integrity": "sha512-FKArQpbrbwv08TNT0k7ejYXpF+R8knZFAatNc0acOxbgeqLzwb86r+P3LGOjIeI3Idqe9CVkZrh4GlsJLJKkkw==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.15.tgz", + "integrity": "sha512-IODaJjNmiasfZX3IoS+4Em3iu0fD2HS0/tgrnkYfW4hyUor01Smnr5eY3jc4rRgaTDrJlDmBTHbFO0ETTDaxWA==", "license": "MIT", "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", - "tailwindcss": "4.0.8" + "tailwindcss": "4.0.15" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.0.8.tgz", - "integrity": "sha512-KfMcuAu/Iw+DcV1e8twrFyr2yN8/ZDC/odIGta4wuuJOGkrkHZbvJvRNIbQNhGh7erZTYV6Ie0IeD6WC9Y8Hcw==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.0.15.tgz", + "integrity": "sha512-e0uHrKfPu7JJGMfjwVNyt5M0u+OP8kUmhACwIRlM+JNBuReDVQ63yAD1NWe5DwJtdaHjugNBil76j+ks3zlk6g==", "license": "MIT", "engines": { "node": ">= 10" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.0.8", - "@tailwindcss/oxide-darwin-arm64": "4.0.8", - "@tailwindcss/oxide-darwin-x64": "4.0.8", - "@tailwindcss/oxide-freebsd-x64": "4.0.8", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.8", - "@tailwindcss/oxide-linux-arm64-gnu": "4.0.8", - "@tailwindcss/oxide-linux-arm64-musl": "4.0.8", - "@tailwindcss/oxide-linux-x64-gnu": "4.0.8", - "@tailwindcss/oxide-linux-x64-musl": "4.0.8", - "@tailwindcss/oxide-win32-arm64-msvc": "4.0.8", - "@tailwindcss/oxide-win32-x64-msvc": "4.0.8" + "@tailwindcss/oxide-android-arm64": "4.0.15", + "@tailwindcss/oxide-darwin-arm64": "4.0.15", + "@tailwindcss/oxide-darwin-x64": "4.0.15", + "@tailwindcss/oxide-freebsd-x64": "4.0.15", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.15", + "@tailwindcss/oxide-linux-arm64-gnu": "4.0.15", + "@tailwindcss/oxide-linux-arm64-musl": "4.0.15", + "@tailwindcss/oxide-linux-x64-gnu": "4.0.15", + "@tailwindcss/oxide-linux-x64-musl": "4.0.15", + "@tailwindcss/oxide-win32-arm64-msvc": "4.0.15", + "@tailwindcss/oxide-win32-x64-msvc": "4.0.15" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.8.tgz", - "integrity": "sha512-We7K79+Sm4mwJHk26Yzu/GAj7C7myemm7PeXvpgMxyxO70SSFSL3uCcqFbz9JA5M5UPkrl7N9fkBe/Y0iazqpA==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.15.tgz", + "integrity": "sha512-EBuyfSKkom7N+CB3A+7c0m4+qzKuiN0WCvzPvj5ZoRu4NlQadg/mthc1tl5k9b5ffRGsbDvP4k21azU4VwVk3Q==", "cpu": [ "arm64" ], @@ -2061,9 +2052,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.8.tgz", - "integrity": "sha512-Lv9Isi2EwkCTG1sRHNDi0uRNN1UGFdEThUAGFrydRmQZnraGLMjN8gahzg2FFnOizDl7LB2TykLUuiw833DSNg==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.15.tgz", + "integrity": "sha512-ObVAnEpLepMhV9VoO0JSit66jiN5C4YCqW3TflsE9boo2Z7FIjV80RFbgeL2opBhtxbaNEDa6D0/hq/EP03kgQ==", "cpu": [ "arm64" ], @@ -2077,9 +2068,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.8.tgz", - "integrity": "sha512-fWfywfYIlSWtKoqWTjukTHLWV3ARaBRjXCC2Eo0l6KVpaqGY4c2y8snUjp1xpxUtpqwMvCvFWFaleMoz1Vhzlw==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.15.tgz", + "integrity": "sha512-IElwoFhUinOr9MyKmGTPNi1Rwdh68JReFgYWibPWTGuevkHkLWKEflZc2jtI5lWZ5U9JjUnUfnY43I4fEXrc4g==", "cpu": [ "x64" ], @@ -2093,9 +2084,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.8.tgz", - "integrity": "sha512-SO+dyvjJV9G94bnmq2288Ke0BIdvrbSbvtPLaQdqjqHR83v5L2fWADyFO+1oecHo9Owsk8MxcXh1agGVPIKIqw==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.15.tgz", + "integrity": "sha512-6BLLqyx7SIYRBOnTZ8wgfXANLJV5TQd3PevRJZp0vn42eO58A2LykRKdvL1qyPfdpmEVtF+uVOEZ4QTMqDRAWA==", "cpu": [ "x64" ], @@ -2109,9 +2100,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.8.tgz", - "integrity": "sha512-ZSHggWiEblQNV69V0qUK5vuAtHP+I+S2eGrKGJ5lPgwgJeAd6GjLsVBN+Mqn2SPVfYM3BOpS9jX/zVg9RWQVDQ==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.15.tgz", + "integrity": "sha512-Zy63EVqO9241Pfg6G0IlRIWyY5vNcWrL5dd2WAKVJZRQVeolXEf1KfjkyeAAlErDj72cnyXObEZjMoPEKHpdNw==", "cpu": [ "arm" ], @@ -2125,9 +2116,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.8.tgz", - "integrity": "sha512-xWpr6M0OZLDNsr7+bQz+3X7zcnDJZJ1N9gtBWCtfhkEtDjjxYEp+Lr5L5nc/yXlL4MyCHnn0uonGVXy3fhxaVA==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.15.tgz", + "integrity": "sha512-2NemGQeaTbtIp1Z2wyerbVEJZTkAWhMDOhhR5z/zJ75yMNf8yLnE+sAlyf6yGDNr+1RqvWrRhhCFt7i0CIxe4Q==", "cpu": [ "arm64" ], @@ -2141,9 +2132,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.8.tgz", - "integrity": "sha512-5tz2IL7LN58ssGEq7h/staD7pu/izF/KeMWdlJ86WDe2Ah46LF3ET6ZGKTr5eZMrnEA0M9cVFuSPprKRHNgjeg==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.15.tgz", + "integrity": "sha512-342GVnhH/6PkVgKtEzvNVuQ4D+Q7B7qplvuH20Cfz9qEtydG6IQczTZ5IT4JPlh931MG1NUCVxg+CIorr1WJyw==", "cpu": [ "arm64" ], @@ -2157,9 +2148,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.8.tgz", - "integrity": "sha512-KSzMkhyrxAQyY2o194NKVKU9j/c+NFSoMvnHWFaNHKi3P1lb+Vq1UC19tLHrmxSkKapcMMu69D7+G1+FVGNDXQ==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.15.tgz", + "integrity": "sha512-g76GxlKH124RuGqacCEFc2nbzRl7bBrlC8qDQMiUABkiifDRHOIUjgKbLNG4RuR9hQAD/MKsqZ7A8L08zsoBrw==", "cpu": [ "x64" ], @@ -2173,9 +2164,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.8.tgz", - "integrity": "sha512-yFYKG5UtHTRimjtqxUWXBgI4Tc6NJe3USjRIVdlTczpLRxq/SFwgzGl5JbatCxgSRDPBFwRrNPxq+ukfQFGdrw==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.15.tgz", + "integrity": "sha512-Gg/Y1XrKEvKpq6WeNt2h8rMIKOBj/W3mNa5NMvkQgMC7iO0+UNLrYmt6zgZufht66HozNpn+tJMbbkZ5a3LczA==", "cpu": [ "x64" ], @@ -2189,9 +2180,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.8.tgz", - "integrity": "sha512-tndGujmCSba85cRCnQzXgpA2jx5gXimyspsUYae5jlPyLRG0RjXbDshFKOheVXU4TLflo7FSG8EHCBJ0EHTKdQ==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.15.tgz", + "integrity": "sha512-7QtSSJwYZ7ZK1phVgcNZpuf7c7gaCj8Wb0xjliligT5qCGCp79OV2n3SJummVZdw4fbTNKUOYMO7m1GinppZyA==", "cpu": [ "arm64" ], @@ -2205,9 +2196,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.8.tgz", - "integrity": "sha512-T77jroAc0p4EHVVgTUiNeFn6Nj3jtD3IeNId2X+0k+N1XxfNipy81BEkYErpKLiOkNhpNFjPee8/ZVas29b2OQ==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.15.tgz", + "integrity": "sha512-JQ5H+5MLhOjpgNp6KomouE0ZuKmk3hO5h7/ClMNAQ8gZI2zkli3IH8ZqLbd2DVfXDbdxN2xvooIEeIlkIoSCqw==", "cpu": [ "x64" ], @@ -2221,15 +2212,15 @@ } }, "node_modules/@tailwindcss/vite": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.0.8.tgz", - "integrity": "sha512-+SAq44yLzYlzyrb7QTcFCdU8Xa7FOA0jp+Xby7fPMUie+MY9HhJysM7Vp+vL8qIp8ceQJfLD+FjgJuJ4lL6nyg==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.0.15.tgz", + "integrity": "sha512-JRexava80NijI8cTcLXNM3nQL5A0ptTHI8oJLLe8z1MpNB6p5J4WCdJJP8RoyHu8/eB1JzEdbpH86eGfbuaezQ==", "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.0.8", - "@tailwindcss/oxide": "4.0.8", - "lightningcss": "^1.29.1", - "tailwindcss": "4.0.8" + "@tailwindcss/node": "4.0.15", + "@tailwindcss/oxide": "4.0.15", + "lightningcss": "1.29.2", + "tailwindcss": "4.0.15" }, "peerDependencies": { "vite": "^5.2.0 || ^6" @@ -3933,15 +3924,12 @@ } }, "node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "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", - "bin": { - "detect-libc": "bin/detect-libc.js" - }, "engines": { - "node": ">=0.10" + "node": ">=8" } }, "node_modules/doctrine": { @@ -5881,12 +5869,12 @@ } }, "node_modules/lightningcss": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.1.tgz", - "integrity": "sha512-FmGoeD4S05ewj+AkhTY+D+myDvXI6eL27FjHIjoyUkO/uw7WZD1fBVs0QxeYWa7E17CUHJaYX/RUGISCtcrG4Q==", + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz", + "integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==", "license": "MPL-2.0", "dependencies": { - "detect-libc": "^1.0.3" + "detect-libc": "^2.0.3" }, "engines": { "node": ">= 12.0.0" @@ -5896,22 +5884,22 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-darwin-arm64": "1.29.1", - "lightningcss-darwin-x64": "1.29.1", - "lightningcss-freebsd-x64": "1.29.1", - "lightningcss-linux-arm-gnueabihf": "1.29.1", - "lightningcss-linux-arm64-gnu": "1.29.1", - "lightningcss-linux-arm64-musl": "1.29.1", - "lightningcss-linux-x64-gnu": "1.29.1", - "lightningcss-linux-x64-musl": "1.29.1", - "lightningcss-win32-arm64-msvc": "1.29.1", - "lightningcss-win32-x64-msvc": "1.29.1" + "lightningcss-darwin-arm64": "1.29.2", + "lightningcss-darwin-x64": "1.29.2", + "lightningcss-freebsd-x64": "1.29.2", + "lightningcss-linux-arm-gnueabihf": "1.29.2", + "lightningcss-linux-arm64-gnu": "1.29.2", + "lightningcss-linux-arm64-musl": "1.29.2", + "lightningcss-linux-x64-gnu": "1.29.2", + "lightningcss-linux-x64-musl": "1.29.2", + "lightningcss-win32-arm64-msvc": "1.29.2", + "lightningcss-win32-x64-msvc": "1.29.2" } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.1.tgz", - "integrity": "sha512-HtR5XJ5A0lvCqYAoSv2QdZZyoHNttBpa5EP9aNuzBQeKGfbyH5+UipLWvVzpP4Uml5ej4BYs5I9Lco9u1fECqw==", + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz", + "integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==", "cpu": [ "arm64" ], @@ -5929,9 +5917,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.1.tgz", - "integrity": "sha512-k33G9IzKUpHy/J/3+9MCO4e+PzaFblsgBjSGlpAaFikeBFm8B/CkO3cKU9oI4g+fjS2KlkLM/Bza9K/aw8wsNA==", + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz", + "integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==", "cpu": [ "x64" ], @@ -5949,9 +5937,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.1.tgz", - "integrity": "sha512-0SUW22fv/8kln2LnIdOCmSuXnxgxVC276W5KLTwoehiO0hxkacBxjHOL5EtHD8BAXg2BvuhsJPmVMasvby3LiQ==", + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz", + "integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==", "cpu": [ "x64" ], @@ -5969,9 +5957,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.1.tgz", - "integrity": "sha512-sD32pFvlR0kDlqsOZmYqH/68SqUMPNj+0pucGxToXZi4XZgZmqeX/NkxNKCPsswAXU3UeYgDSpGhu05eAufjDg==", + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz", + "integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==", "cpu": [ "arm" ], @@ -5989,9 +5977,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.1.tgz", - "integrity": "sha512-0+vClRIZ6mmJl/dxGuRsE197o1HDEeeRk6nzycSy2GofC2JsY4ifCRnvUWf/CUBQmlrvMzt6SMQNMSEu22csWQ==", + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz", + "integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==", "cpu": [ "arm64" ], @@ -6009,9 +5997,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.1.tgz", - "integrity": "sha512-UKMFrG4rL/uHNgelBsDwJcBqVpzNJbzsKkbI3Ja5fg00sgQnHw/VrzUTEc4jhZ+AN2BvQYz/tkHu4vt1kLuJyw==", + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz", + "integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==", "cpu": [ "arm64" ], @@ -6029,9 +6017,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.1.tgz", - "integrity": "sha512-u1S+xdODy/eEtjADqirA774y3jLcm8RPtYztwReEXoZKdzgsHYPl0s5V52Tst+GKzqjebkULT86XMSxejzfISw==", + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz", + "integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==", "cpu": [ "x64" ], @@ -6049,9 +6037,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.1.tgz", - "integrity": "sha512-L0Tx0DtaNUTzXv0lbGCLB/c/qEADanHbu4QdcNOXLIe1i8i22rZRpbT3gpWYsCh9aSL9zFujY/WmEXIatWvXbw==", + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz", + "integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==", "cpu": [ "x64" ], @@ -6069,9 +6057,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.1.tgz", - "integrity": "sha512-QoOVnkIEFfbW4xPi+dpdft/zAKmgLgsRHfJalEPYuJDOWf7cLQzYg0DEh8/sn737FaeMJxHZRc1oBreiwZCjog==", + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz", + "integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==", "cpu": [ "arm64" ], @@ -6089,9 +6077,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.1.tgz", - "integrity": "sha512-NygcbThNBe4JElP+olyTI/doBNGJvLs3bFCRPdvuCcxZCcCZ71B858IHpdm7L1btZex0FvCmM17FK98Y9MRy1Q==", + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.2.tgz", + "integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==", "cpu": [ "x64" ], @@ -8225,9 +8213,9 @@ } }, "node_modules/tailwindcss": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.8.tgz", - "integrity": "sha512-Me7N5CKR+D2A1xdWA5t5+kjjT7bwnxZOE6/yDI/ixJdJokszsn2n++mdU5yJwrsTpqFX2B9ZNMBJDwcqk9C9lw==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.15.tgz", + "integrity": "sha512-6ZMg+hHdMJpjpeCCFasX7K+U615U9D+7k5/cDK/iRwl6GptF24+I/AbKgOnXhVKePzrEyIXutLv36n4cRsq3Sg==", "license": "MIT" }, "node_modules/tapable": { diff --git a/package.json b/package.json index 5a14a389..0d46cd7b 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "@fontsource/inter": "^5.1.1", "@mui/icons-material": "^6.4.7", "@mui/joy": "^5.0.0-beta.51", - "@tailwindcss/vite": "^4.0.8", + "@tailwindcss/vite": "^4.0.15", "@tiptap/extension-link": "^2.11.5", "@tiptap/pm": "^2.11.5", "@tiptap/react": "^2.11.5", @@ -44,7 +44,7 @@ "socket.io": "^4.8.1", "socket.io-client": "^4.8.1", "ssh2": "^1.16.0", - "tailwindcss": "^4.0.8" + "tailwindcss": "^4.0.15" }, "devDependencies": { "@eslint/js": "^9.17.0", diff --git a/src/App.jsx b/src/App.jsx index 6cb4befb..36185043 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -39,6 +39,12 @@ function App() { authMethod: "Select Auth", rememberHost: true, storePassword: true, + connectionType: "ssh", + rdpDomain: "", + rdpWindowsAuthentication: true, + rdpConsole: false, + vncScaling: "100%", + vncQuality: "High" }); const [editHostForm, setEditHostForm] = useState({ name: "", @@ -223,34 +229,53 @@ function App() { }, []); const handleAddHost = () => { - if (addHostForm.ip && addHostForm.user && addHostForm.port) { + if (addHostForm.ip && addHostForm.port) { + if (addHostForm.connectionType === 'ssh' && !addHostForm.user) { + setErrorMessage("Please fill out all required fields (IP, User, Port)."); + setIsErrorHidden(false); + return; + } + if (!addHostForm.rememberHost) { connectToHost(); setIsAddHostHidden(true); return; } - if (addHostForm.authMethod === 'Select Auth') { - alert("Please select an authentication method."); - return; + if (addHostForm.connectionType === 'ssh') { + if (addHostForm.authMethod === 'Select Auth') { + setErrorMessage("Please select an authentication method."); + setIsErrorHidden(false); + return; + } + if (addHostForm.authMethod === 'password' && !addHostForm.password) { + setIsNoAuthHidden(false); + return; + } + if (addHostForm.authMethod === 'sshKey' && !addHostForm.sshKey) { + setIsNoAuthHidden(false); + return; + } } - if (addHostForm.authMethod === 'password' && !addHostForm.password) { - setIsNoAuthHidden(false); - return; - } - if (addHostForm.authMethod === 'sshKey' && !addHostForm.sshKey) { + else if (!addHostForm.password) { setIsNoAuthHidden(false); return; } - connectToHost(); - if (!addHostForm.storePassword) { - addHostForm.password = ''; + try { + connectToHost(); + if (!addHostForm.storePassword) { + addHostForm.password = ''; + } + handleSaveHost(); + setIsAddHostHidden(true); + } catch (error) { + setErrorMessage(error.message || "Failed to add host"); + setIsErrorHidden(false); } - handleSaveHost(); - setIsAddHostHidden(true); } else { - alert("Please fill out all required fields (IP, User, Port)."); + setErrorMessage("Please fill out all required fields."); + setIsErrorHidden(false); } }; @@ -275,25 +300,42 @@ function App() { setActiveTab(nextId); setNextId(nextId + 1); setIsAddHostHidden(true); - setAddHostForm({ name: "", folder: "", ip: "", user: "", password: "", sshKey: "", port: 22, authMethod: "Select Auth", rememberHost: true, storePassword: true }); + setAddHostForm({ name: "", folder: "", ip: "", user: "", password: "", sshKey: "", port: 22, authMethod: "Select Auth", rememberHost: true, storePassword: true, connectionType: "ssh", rdpDomain: "", rdpWindowsAuthentication: true, rdpConsole: false, vncScaling: "100%", vncQuality: "High" }); } const handleAuthSubmit = (form) => { - const updatedTerminals = terminals.map((terminal) => { - if (terminal.id === activeTab) { - return { - ...terminal, - hostConfig: { - ...terminal.hostConfig, - password: form.password, - sshKey: form.sshKey + try { + setIsNoAuthHidden(true); + + setTimeout(() => { + const updatedTerminals = terminals.map((terminal) => { + if (terminal.id === activeTab) { + return { + ...terminal, + hostConfig: { + ...terminal.hostConfig, + password: form.authMethod === 'password' ? form.password : undefined, + sshKey: form.authMethod === 'sshKey' ? form.sshKey : undefined + } + }; } - }; - } - return terminal; - }); - setTerminals(updatedTerminals); - setIsNoAuthHidden(true); + return terminal; + }); + + setTerminals(updatedTerminals); + + setNoAuthenticationForm({ + authMethod: 'Select Auth', + password: '', + sshKey: '', + keyType: '', + }); + }, 100); + } catch (error) { + console.error("Authentication error:", error); + setErrorMessage("Failed to authenticate: " + (error.message || "Unknown error")); + setIsErrorHidden(false); + } }; const connectToHostWithConfig = (hostConfig) => { @@ -327,20 +369,30 @@ function App() { setIsLaunchpadOpen(false); } - const handleSaveHost = () => { - let hostConfig = { - name: addHostForm.name || addHostForm.ip, - folder: addHostForm.folder, - ip: addHostForm.ip, - user: addHostForm.user, - password: addHostForm.authMethod === 'password' ? addHostForm.password : undefined, - sshKey: addHostForm.authMethod === 'sshKey' ? addHostForm.sshKey : undefined, - port: String(addHostForm.port), - } - if (userRef.current) { - userRef.current.saveHost({ - hostConfig, - }); + const handleSaveHost = async () => { + try { + let hostConfig = { + name: addHostForm.name || addHostForm.ip, + folder: addHostForm.folder, + ip: addHostForm.ip, + user: addHostForm.user, + password: (addHostForm.authMethod === 'password' || addHostForm.connectionType === 'vnc' || addHostForm.connectionType === 'rdp') ? addHostForm.password : undefined, + sshKey: addHostForm.connectionType === 'ssh' && addHostForm.authMethod === 'sshKey' ? addHostForm.sshKey : undefined, + port: String(addHostForm.port), + connectionType: addHostForm.connectionType, + rdpDomain: addHostForm.connectionType === 'rdp' ? addHostForm.rdpDomain : undefined, + rdpWindowsAuthentication: addHostForm.connectionType === 'rdp' ? addHostForm.rdpWindowsAuthentication : undefined, + rdpConsole: addHostForm.connectionType === 'rdp' ? addHostForm.rdpConsole : undefined, + vncScaling: addHostForm.connectionType === 'vnc' ? addHostForm.vncScaling : undefined, + vncQuality: addHostForm.connectionType === 'vnc' ? addHostForm.vncQuality : undefined + } + if (userRef.current) { + await userRef.current.saveHost({ + hostConfig, + }); + } + } catch (error) { + throw error; } } @@ -455,9 +507,11 @@ function App() { }); await new Promise(resolve => setTimeout(resolve, 3000)); + setIsEditHostHidden(true); + } catch (error) { + throw error; } finally { setIsEditing(false); - setIsEditHostHidden(true); } return; } @@ -465,7 +519,7 @@ function App() { updateEditHostForm(oldConfig); } catch (error) { console.error('Edit failed:', error); - setErrorMessage(`Edit failed: ${error}`); + setErrorMessage(`Edit failed: ${error.message || error}`); setIsErrorHidden(false); setIsEditing(false); } diff --git a/src/apps/ssh/Terminal.jsx b/src/apps/ssh/Terminal.jsx index 41f0c2d9..cec0ba1e 100644 --- a/src/apps/ssh/Terminal.jsx +++ b/src/apps/ssh/Terminal.jsx @@ -94,20 +94,15 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde }); socket.on("connect", () => { - console.log("Socket connected, attempting SSH connection..."); - fitAddon.current.fit(); resizeTerminal(); const { cols, rows } = terminalInstance.current; - // Check for authentication details if (!hostConfig.password?.trim() && !hostConfig.sshKey?.trim()) { - console.log("No authentication provided, showing modal"); setIsNoAuthHidden(false); return; } - // Ensure we have proper SSH config with both key field names for backward compatibility const sshConfig = { ip: hostConfig.ip, user: hostConfig.user, @@ -121,9 +116,11 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde }); setTimeout(() => { - fitAddon.current.fit(); - resizeTerminal(); - terminalInstance.current.focus(); + if (terminalInstance.current) { + fitAddon.current.fit(); + resizeTerminal(); + terminalInstance.current.focus(); + } }, 50); socket.on("data", (data) => { @@ -133,124 +130,49 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde let isPasting = false; - terminalInstance.current.onData((data) => { - if (socketRef.current && socketRef.current.connected) { - socketRef.current.emit("data", data); - } - }); + if (terminalInstance.current) { + terminalInstance.current.onData((data) => { + if (socketRef.current && socketRef.current.connected) { + socketRef.current.emit("data", data); + } + }); - terminalInstance.current.attachCustomKeyEventHandler((event) => { - if ((event.ctrlKey || event.metaKey) && event.key === "v") { - if (isPasting) return false; - isPasting = true; - - event.preventDefault(); - - // Use a multi-layered approach for clipboard access - const pasteFromClipboard = async () => { - try { - // Try modern Clipboard API first - if (navigator.clipboard && navigator.clipboard.readText) { - try { - const text = await navigator.clipboard.readText(); - if (text && socketRef.current?.connected) { - const processedText = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n/g, "\r"); - socketRef.current.emit("data", processedText); - return true; - } - } catch (clipboardErr) { - console.warn("Clipboard API failed:", clipboardErr); - // Continue to fallbacks + terminalInstance.current.attachCustomKeyEventHandler((event) => { + if ((event.ctrlKey || event.metaKey) && event.key === "v") { + event.preventDefault(); + + navigator.clipboard.readText() + .then(text => { + if (text && socketRef.current?.connected) { + const processedText = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n/g, "\r"); + socketRef.current.emit("data", processedText); } - } - - // Try execCommand fallback - if (document.queryCommandSupported && document.queryCommandSupported('paste')) { - const textarea = document.createElement('textarea'); - textarea.style.position = 'fixed'; - textarea.style.opacity = '0'; - document.body.appendChild(textarea); - textarea.focus(); - - try { - const successful = document.execCommand('paste'); - if (successful) { - const text = textarea.value; - if (text && socketRef.current?.connected) { - const processedText = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n/g, "\r"); - socketRef.current.emit("data", processedText); - document.body.removeChild(textarea); - return true; - } - } - } catch (execErr) { - console.warn("execCommand paste failed:", execErr); + }) + .catch(() => { + if (terminalInstance.current) { + terminalInstance.current.write("\r\n*** Paste failed: Clipboard access denied. Please check browser permissions. ***\r\n"); } - document.body.removeChild(textarea); - } + }); - // Show permissions warning and instructions - terminalInstance.current.write("\r\n*** To paste: Right-click in terminal and select Paste from context menu ***\r\n"); - return false; - } finally { - setTimeout(() => { - isPasting = false; - }, 100); - } - }; + return false; + } + return true; + }); - pasteFromClipboard(); - return false; - } - - return true; - }); - - terminalInstance.current.onKey(({ domEvent }) => { - if (domEvent.key === "c" && (domEvent.ctrlKey || domEvent.metaKey)) { - const selection = terminalInstance.current.getSelection(); - if (selection) { - // Use a try-catch to handle clipboard failures - try { - if (navigator.clipboard && navigator.clipboard.writeText) { - navigator.clipboard.writeText(selection) - .catch(err => { - console.warn("Clipboard write failed:", err); - terminalInstance.current.write("\r\n*** Copy failed: Text copied to internal buffer ***\r\n"); - // Store selection in a variable as fallback - window.termixInternalClipboard = selection; - }); - } else { - // Fallback for browsers without clipboard API - const textarea = document.createElement('textarea'); - textarea.value = selection; - textarea.style.position = 'fixed'; - textarea.style.opacity = '0'; - document.body.appendChild(textarea); - textarea.select(); - - try { - const successful = document.execCommand('copy'); - if (!successful) { - terminalInstance.current.write("\r\n*** Copy failed: Text copied to internal buffer ***\r\n"); - window.termixInternalClipboard = selection; + terminalInstance.current.onKey(({ domEvent }) => { + if (domEvent.key === "c" && (domEvent.ctrlKey || domEvent.metaKey)) { + const selection = terminalInstance.current.getSelection(); + if (selection) { + navigator.clipboard.writeText(selection) + .catch(() => { + if (terminalInstance.current) { + terminalInstance.current.write("\r\n*** Copy failed: Clipboard access denied. Please check browser permissions. ***\r\n"); } - } catch (err) { - console.warn("execCommand copy failed:", err); - terminalInstance.current.write("\r\n*** Copy failed: Text copied to internal buffer ***\r\n"); - window.termixInternalClipboard = selection; - } - - document.body.removeChild(textarea); - } - } catch (err) { - console.error("Copy failed:", err); - terminalInstance.current.write("\r\n*** Copy failed: Text copied to internal buffer ***\r\n"); - window.termixInternalClipboard = selection; + }); } } - } - }); + }); + } let authModalShown = false; @@ -262,18 +184,22 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde }); socket.on("disconnect", (reason) => { - console.log("Socket disconnected:", reason); - terminalInstance.current.write(`\r\n*** Socket disconnected: ${reason} ***\r\n`); + if (terminalInstance.current) { + terminalInstance.current.write(`\r\n*** Socket disconnected: ${reason} ***\r\n`); + } }); socket.on("reconnect", (attemptNumber) => { - console.log("Socket reconnected after", attemptNumber, "attempts"); - terminalInstance.current.write(`\r\n*** Socket reconnected after ${attemptNumber} attempts ***\r\n`); + if (terminalInstance.current) { + terminalInstance.current.write(`\r\n*** Socket reconnected after ${attemptNumber} attempts ***\r\n`); + } }); socket.on("reconnect_error", (error) => { console.error("Socket reconnect error:", error); - terminalInstance.current.write(`\r\n*** Socket reconnect error: ${error.message} ***\r\n`); + if (terminalInstance.current) { + terminalInstance.current.write(`\r\n*** Socket reconnect error: ${error.message} ***\r\n`); + } }); const pingInterval = setInterval(() => { @@ -284,13 +210,11 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde socketRef.current.on("pong", () => {}); - // Add right-click context menu for paste - const element = terminalInstance.current.element; - if (element) { + if (terminalInstance.current && terminalInstance.current.element) { + const element = terminalInstance.current.element; element.addEventListener('contextmenu', (event) => { event.preventDefault(); - - // Create and show context menu + const contextMenu = document.createElement('div'); contextMenu.className = 'terminal-context-menu'; contextMenu.style.position = 'fixed'; @@ -302,8 +226,7 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde contextMenu.style.padding = '4px 0'; contextMenu.style.boxShadow = '0 2px 10px rgba(0,0,0,0.2)'; contextMenu.style.zIndex = '1000'; - - // Create copy option + const copyOption = document.createElement('div'); copyOption.innerText = 'Copy'; copyOption.className = 'terminal-context-menu-item'; @@ -317,29 +240,31 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde copyOption.onmouseout = () => { copyOption.style.backgroundColor = 'transparent'; }; - - // Handle copy action + copyOption.onclick = () => { - const selection = terminalInstance.current.getSelection(); - if (selection) { - // Try to copy using clipboard API - if (navigator.clipboard && navigator.clipboard.writeText) { - navigator.clipboard.writeText(selection) - .catch(err => { - console.warn("Clipboard write failed:", err); - window.termixInternalClipboard = selection; + if (terminalInstance.current) { + const selection = terminalInstance.current.getSelection(); + if (selection) { + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(selection) + .catch(err => { + console.warn("Clipboard write failed:", err); + window.termixInternalClipboard = selection; + if (terminalInstance.current) { + terminalInstance.current.write("\r\n*** Copied to internal clipboard ***\r\n"); + } + }); + } else { + window.termixInternalClipboard = selection; + if (terminalInstance.current) { terminalInstance.current.write("\r\n*** Copied to internal clipboard ***\r\n"); - }); - } else { - // Store in internal clipboard - window.termixInternalClipboard = selection; - terminalInstance.current.write("\r\n*** Copied to internal clipboard ***\r\n"); + } + } } } document.body.removeChild(contextMenu); }; - - // Create paste option + const pasteOption = document.createElement('div'); pasteOption.innerText = 'Paste'; pasteOption.className = 'terminal-context-menu-item'; @@ -353,11 +278,9 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde pasteOption.onmouseout = () => { pasteOption.style.backgroundColor = 'transparent'; }; - - // Handle paste action + pasteOption.onclick = async () => { try { - // Try clipboard API first if (navigator.clipboard && navigator.clipboard.readText) { try { const text = await navigator.clipboard.readText(); @@ -366,32 +289,28 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde socketRef.current.emit("data", processedText); } } catch (err) { - // Use fallback or internal clipboard if (window.termixInternalClipboard) { const processedText = window.termixInternalClipboard.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n/g, "\r"); socketRef.current.emit("data", processedText); - } else { + } else if (terminalInstance.current) { terminalInstance.current.write("\r\n*** Paste failed: No clipboard content available ***\r\n"); } } } else if (window.termixInternalClipboard) { - // Use internal clipboard if available const processedText = window.termixInternalClipboard.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n/g, "\r"); socketRef.current.emit("data", processedText); - } else { + } else if (terminalInstance.current) { terminalInstance.current.write("\r\n*** Paste failed: No clipboard content available ***\r\n"); } } finally { document.body.removeChild(contextMenu); } }; - - // Add options to menu + contextMenu.appendChild(copyOption); contextMenu.appendChild(pasteOption); document.body.appendChild(contextMenu); - - // Remove menu when clicking elsewhere + const removeMenu = (e) => { if (!contextMenu.contains(e.target)) { document.body.removeChild(contextMenu); diff --git a/src/apps/user/User.jsx b/src/apps/user/User.jsx index ab22ec4f..534ee227 100644 --- a/src/apps/user/User.jsx +++ b/src/apps/user/User.jsx @@ -149,6 +149,27 @@ export const User = forwardRef(({ onLoginSuccess, onCreateSuccess, onDeleteSucce if (!currentUser.current) return onFailure("Not authenticated"); try { + const existingHosts = await getAllHosts(); + + const duplicateNameHost = existingHosts.find(host => + host.config.name && + host.config.name.toLowerCase() === hostConfig.hostConfig.name.toLowerCase() + ); + + if (duplicateNameHost) { + return onFailure("A host with this name already exists. Please choose a different name."); + } + + if (!hostConfig.hostConfig.name) { + const duplicateIpHost = existingHosts.find(host => + host.config.ip.toLowerCase() === hostConfig.hostConfig.ip.toLowerCase() + ); + + if (duplicateIpHost) { + return onFailure("A host with this IP already exists. Please provide a unique name."); + } + } + const response = await new Promise((resolve) => { socketRef.current.emit("saveHostConfig", { userId: currentUser.current.id, @@ -222,6 +243,18 @@ export const User = forwardRef(({ onLoginSuccess, onCreateSuccess, onDeleteSucce if (!currentUser.current) return onFailure("Not authenticated"); try { + const existingHosts = await getAllHosts(); + + const duplicateNameHost = existingHosts.find(host => + host.config.name && + host.config.name.toLowerCase() === newHostConfig.name.toLowerCase() && + host.config.ip.toLowerCase() !== oldHostConfig.ip.toLowerCase() + ); + + if (duplicateNameHost) { + return onFailure("A host with this name already exists. Please choose a different name."); + } + const response = await new Promise((resolve) => { socketRef.current.emit("editHost", { userId: currentUser.current.id, diff --git a/src/backend/database.cjs b/src/backend/database.cjs index 6d99d81a..2e3e4bb4 100644 --- a/src/backend/database.cjs +++ b/src/backend/database.cjs @@ -190,14 +190,31 @@ io.of('/database.io').on('connection', (socket) => { const finalName = cleanConfig.name || cleanConfig.ip; - const existingHost = await Host.findOne({ - name: finalName, - createdBy: userId + // Check for hosts with the same name (case insensitive) + const existingHostByName = await Host.findOne({ + createdBy: userId, + name: { $regex: new RegExp('^' + finalName + '$', 'i') } }); - if (existingHost) { + if (existingHostByName) { logger.warn(`Host with name ${finalName} already exists for user: ${userId}`); - return callback({ error: 'Host with this name already exists' }); + return callback({ error: `Host with name "${finalName}" already exists. Please choose a different name.` }); + } + + // Prevent duplicate IPs if using IP as name + if (!cleanConfig.name) { + const existingHostByIp = await Host.findOne({ + createdBy: userId, + config: { $regex: new RegExp(cleanConfig.ip, 'i') } + }); + + if (existingHostByIp) { + const decryptedConfig = decryptData(existingHostByIp.config, userId, sessionToken); + if (decryptedConfig && decryptedConfig.ip.toLowerCase() === cleanConfig.ip.toLowerCase()) { + logger.warn(`Host with IP ${cleanConfig.ip} already exists for user: ${userId}`); + return callback({ error: `Host with IP "${cleanConfig.ip}" already exists. Please provide a unique name.` }); + } + } } const encryptedConfig = encryptData(cleanConfig, userId, sessionToken); @@ -397,6 +414,7 @@ io.of('/database.io').on('connection', (socket) => { return callback({ error: 'Invalid session' }); } + // Find the host to be edited const hosts = await Host.find({ createdBy: userId }); const host = hosts.find(h => { const decryptedConfig = decryptData(h.config, userId, sessionToken); @@ -408,6 +426,37 @@ io.of('/database.io').on('connection', (socket) => { return callback({ error: 'Host not found' }); } + const finalName = newHostConfig.name?.trim() || newHostConfig.ip.trim(); + + // If the name is being changed, check for duplicates using case-insensitive comparison + if (finalName.toLowerCase() !== host.name.toLowerCase()) { + // Check for duplicate name using regex for case-insensitive comparison + const duplicateNameHost = await Host.findOne({ + createdBy: userId, + _id: { $ne: host._id }, // Exclude the current host + name: { $regex: new RegExp('^' + finalName + '$', 'i') } + }); + + if (duplicateNameHost) { + logger.warn(`Host with name ${finalName} already exists for user: ${userId}`); + return callback({ error: `Host with name "${finalName}" already exists. Please choose a different name.` }); + } + } + + // If IP is changed and no custom name provided, check for duplicate IP + if (newHostConfig.ip !== oldHostConfig.ip && !newHostConfig.name) { + const duplicateIpHost = hosts.find(h => { + if (h._id.toString() === host._id.toString()) return false; + const decryptedConfig = decryptData(h.config, userId, sessionToken); + return decryptedConfig && decryptedConfig.ip.toLowerCase() === newHostConfig.ip.toLowerCase(); + }); + + if (duplicateIpHost) { + logger.warn(`Host with IP ${newHostConfig.ip} already exists for user: ${userId}`); + return callback({ error: `Host with IP "${newHostConfig.ip}" already exists. Please provide a unique name.` }); + } + } + const cleanConfig = { name: newHostConfig.name?.trim(), folder: newHostConfig.folder?.trim() || null, @@ -424,6 +473,7 @@ io.of('/database.io').on('connection', (socket) => { return callback({ error: 'Configuration encryption failed' }); } + host.name = finalName; host.config = encryptedConfig; host.folder = cleanConfig.folder; await host.save(); @@ -432,7 +482,7 @@ io.of('/database.io').on('connection', (socket) => { callback({ success: true }); } catch (error) { logger.error('Host edit error:', error); - callback({ error: 'Failed to edit host' }); + callback({ error: `Failed to edit host: ${error.message}` }); } }); diff --git a/src/modals/AddHostModal.jsx b/src/modals/AddHostModal.jsx index d8bdf8c7..b65cc54f 100644 --- a/src/modals/AddHostModal.jsx +++ b/src/modals/AddHostModal.jsx @@ -25,6 +25,8 @@ import VisibilityOff from '@mui/icons-material/VisibilityOff'; const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidden }) => { const [showPassword, setShowPassword] = useState(false); const [activeTab, setActiveTab] = useState(0); + const [errorMessage, setErrorMessage] = useState(""); + const [showError, setShowError] = useState(false); const handleFileChange = (e) => { const file = e.target.files[0]; @@ -102,12 +104,30 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd const handleSubmit = (event) => { event.preventDefault(); - if (!form.ip?.trim() || !form.user?.trim() || !form.port) { - alert("Please fill out all required fields (IP, User, Port)."); + + setErrorMessage(""); + setShowError(false); + + if (!form.ip?.trim()) { + setErrorMessage("Please provide an IP address."); + setShowError(true); return; } - handleAddHost(); - setActiveTab(0); + + if (form.connectionType === 'ssh' && !form.user?.trim()) { + setErrorMessage("Please provide a username for SSH connection."); + setShowError(true); + return; + } + + try { + handleAddHost(); + setActiveTab(0); + } catch (error) { + console.error("Add host error:", error); + setErrorMessage(error.message || "Failed to add host. The host name or IP may already exist."); + setShowError(true); + } }; return ( @@ -138,6 +158,18 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd mx: 2, }} > + {showError && ( +
+ {errorMessage} +
+ )} setActiveTab(val)} diff --git a/src/modals/EditHostModal.jsx b/src/modals/EditHostModal.jsx index 25810fe7..02aafb40 100644 --- a/src/modals/EditHostModal.jsx +++ b/src/modals/EditHostModal.jsx @@ -24,21 +24,23 @@ import VisibilityOff from '@mui/icons-material/VisibilityOff'; const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHost }) => { const [form, setForm] = useState({ - name: hostConfig?.name || '', - folder: hostConfig?.folder || '', - ip: hostConfig?.ip || '', - user: hostConfig?.user || '', - port: hostConfig?.port || '', + name: '', + folder: '', + ip: '', + user: '', + port: '', password: '', - sshKey: hostConfig?.sshKey || '', - keyType: hostConfig?.keyType || '', - authMethod: hostConfig?.authMethod || 'Select Auth', + sshKey: '', + keyType: '', + authMethod: 'Select Auth', storePassword: true, rememberHost: true }); const [showPassword, setShowPassword] = useState(false); const [activeTab, setActiveTab] = useState(0); const [isLoading, setIsLoading] = useState(false); + const [errorMessage, setErrorMessage] = useState(""); + const [showError, setShowError] = useState(false); useEffect(() => { if (!isHidden && hostConfig) { @@ -106,17 +108,10 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo const handleAuthChange = (newMethod) => { setForm((prev) => ({ ...prev, - authMethod: newMethod - })); - }; - - const handleStorePasswordChange = (checked) => { - setForm((prev) => ({ - ...prev, - storePassword: Boolean(checked), - password: checked ? prev.password : "", - sshKey: checked ? prev.sshKey : "", - authMethod: checked ? prev.authMethod : "Select Auth" + authMethod: newMethod, + password: "", + sshKey: "", + keyType: "", })); }; @@ -131,7 +126,7 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo if (form.storePassword) { if (authMethod === 'Select Auth') return false; if (authMethod === 'password' && !password?.trim()) return false; - if (authMethod === 'sshKey' && !sshKey?.trim()) return false; + if (authMethod === 'key' && !sshKey?.trim()) return false; } return true; @@ -143,6 +138,23 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo setIsLoading(true); try { + setErrorMessage(""); + setShowError(false); + + if (!form.ip || !form.user) { + setErrorMessage("IP and Username are required fields"); + setShowError(true); + setIsLoading(false); + return; + } + + if (!form.port) { + setErrorMessage("Port is required"); + setShowError(true); + setIsLoading(false); + return; + } + const newConfig = { name: form.name || form.ip, folder: form.folder, @@ -161,6 +173,11 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo } await handleEditHost(hostConfig, newConfig); + setActiveTab(0); + } catch (error) { + console.error("Edit host error:", error); + setErrorMessage(error.message || "Failed to edit host. The host name may already exist."); + setShowError(true); } finally { setIsLoading(false); } @@ -196,10 +213,22 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo mx: 2, }} > - + {errorMessage} + + )} + setActiveTab(val)} - sx={{ + sx={{ width: '100%', mb: 0, backgroundColor: theme.palette.general.tertiary, @@ -241,22 +270,21 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo Host Name setForm((prev) => ({ ...prev, name: e.target.value }))} + onChange={(e) => setForm({ ...form, name: e.target.value })} sx={{ backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary + color: theme.palette.text.primary, }} /> - Folder setForm((prev) => ({ ...prev, folder: e.target.value }))} + value={form.folder || ''} + onChange={(e) => setForm({ ...form, folder: e.target.value })} sx={{ backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary + color: theme.palette.text.primary, }} /> @@ -269,35 +297,38 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo Host IP setForm((prev) => ({ ...prev, ip: e.target.value }))} + onChange={(e) => setForm({ ...form, ip: e.target.value })} + required sx={{ backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary + color: theme.palette.text.primary, + }} + /> + + + Host User + setForm({ ...form, user: e.target.value })} + required + sx={{ + backgroundColor: theme.palette.general.primary, + color: theme.palette.text.primary, }} /> - 65535}> Host Port setForm((prev) => ({ ...prev, port: e.target.value }))} + onChange={(e) => setForm({ ...form, port: e.target.value })} + min={1} + max={65535} + required sx={{ backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary - }} - /> - - - - Host User - setForm((prev) => ({ ...prev, user: e.target.value }))} - sx={{ - backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary + color: theme.palette.text.primary, }} /> @@ -306,23 +337,9 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo - - Store Password - handleStorePasswordChange(e.target.checked)} - sx={{ - color: theme.palette.text.primary, - '&.Mui-checked': { - color: theme.palette.text.primary, - }, - }} - /> - - {form.storePassword && ( <> - + Authentication Method setForm(prev => ({ ...prev, password: e.target.value }))} + onChange={(e) => setForm({ ...form, password: e.target.value })} sx={{ backgroundColor: theme.palette.general.primary, color: theme.palette.text.primary, @@ -367,7 +384,7 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo {form.authMethod === 'key' && ( - + SSH Key