Optimized pasting, fixed host naming.

This commit is contained in:
LukeGus
2025-03-23 21:16:51 -05:00
parent ea631bd023
commit 4d33613679
9 changed files with 540 additions and 414 deletions

234
package-lock.json generated
View File

@@ -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": {

View File

@@ -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",

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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,

View File

@@ -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}` });
}
});

View File

@@ -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 && (
<div style={{
backgroundColor: "#c53030",
color: "white",
padding: "10px",
textAlign: "center",
borderTopLeftRadius: "10px",
borderTopRightRadius: "10px"
}}>
{errorMessage}
</div>
)}
<Tabs
value={activeTab}
onChange={(e, val) => setActiveTab(val)}

View File

@@ -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,
}}
>
<Tabs
value={activeTab}
{showError && (
<div style={{
backgroundColor: "#c53030",
color: "white",
padding: "10px",
textAlign: "center",
borderTopLeftRadius: "10px",
borderTopRightRadius: "10px"
}}>
{errorMessage}
</div>
)}
<Tabs
value={activeTab}
onChange={(e, val) => setActiveTab(val)}
sx={{
sx={{
width: '100%',
mb: 0,
backgroundColor: theme.palette.general.tertiary,
@@ -241,22 +270,21 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo
<FormLabel>Host Name</FormLabel>
<Input
value={form.name}
onChange={(e) => 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,
}}
/>
</FormControl>
<FormControl>
<FormLabel>Folder</FormLabel>
<Input
value={form.folder}
onChange={(e) => 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,
}}
/>
</FormControl>
@@ -269,35 +297,38 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo
<FormLabel>Host IP</FormLabel>
<Input
value={form.ip}
onChange={(e) => 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,
}}
/>
</FormControl>
<FormControl error={!form.user}>
<FormLabel>Host User</FormLabel>
<Input
value={form.user}
onChange={(e) => setForm({ ...form, user: e.target.value })}
required
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
/>
</FormControl>
<FormControl error={form.port < 1 || form.port > 65535}>
<FormLabel>Host Port</FormLabel>
<Input
type="number"
value={form.port}
onChange={(e) => 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
}}
/>
</FormControl>
<FormControl error={!form.user}>
<FormLabel>Host User</FormLabel>
<Input
value={form.user}
onChange={(e) => setForm((prev) => ({ ...prev, user: e.target.value }))}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary
color: theme.palette.text.primary,
}}
/>
</FormControl>
@@ -306,23 +337,9 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo
<TabPanel value={2}>
<Stack spacing={2}>
<FormControl>
<FormLabel>Store Password</FormLabel>
<Checkbox
checked={Boolean(form.storePassword)}
onChange={(e) => handleStorePasswordChange(e.target.checked)}
sx={{
color: theme.palette.text.primary,
'&.Mui-checked': {
color: theme.palette.text.primary,
},
}}
/>
</FormControl>
{form.storePassword && (
<>
<FormControl error={form.storePassword && (!form.authMethod || form.authMethod === 'Select Auth')}>
<FormControl error={!form.authMethod || form.authMethod === 'Select Auth'}>
<FormLabel>Authentication Method</FormLabel>
<Select
value={form.authMethod}
@@ -339,13 +356,13 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo
</FormControl>
{form.authMethod === 'password' && (
<FormControl error={form.storePassword && !form.password}>
<FormControl error={!form.password}>
<FormLabel>Password</FormLabel>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Input
type={showPassword ? 'text' : 'password'}
value={form.password}
onChange={(e) => 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' && (
<Stack spacing={2}>
<FormControl error={form.storePassword && !form.sshKey}>
<FormControl error={!form.sshKey}>
<FormLabel>SSH Key</FormLabel>
<Button
component="label"
@@ -409,6 +426,26 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo
)}
</>
)}
<FormControl>
<FormLabel>Store Password</FormLabel>
<Checkbox
checked={Boolean(form.storePassword)}
onChange={(e) => setForm({
...form,
storePassword: e.target.checked,
password: e.target.checked ? form.password : "",
sshKey: e.target.checked ? form.sshKey : "",
authMethod: e.target.checked ? form.authMethod : "Select Auth"
})}
sx={{
color: theme.palette.text.primary,
'&.Mui-checked': {
color: theme.palette.text.primary,
},
}}
/>
</FormControl>
</Stack>
</TabPanel>
</div>

View File

@@ -43,15 +43,29 @@ const NoAuthenticationModal = ({ isHidden, form, setForm, setIsNoAuthHidden, han
const handleSubmit = (e) => {
e.preventDefault();
if(isFormValid()) {
handleAuthSubmit(form);
setForm (prev => ({
...prev,
authMethod: 'Select Auth',
password: '',
sshKey: '',
keyType: '',
}))
e.stopPropagation();
try {
if(isFormValid()) {
const formData = {
authMethod: form.authMethod,
password: form.authMethod === 'password' ? form.password : '',
sshKey: form.authMethod === 'sshKey' ? form.sshKey : '',
keyType: form.authMethod === 'sshKey' ? form.keyType : '',
};
handleAuthSubmit(formData);
setForm(prev => ({
...prev,
authMethod: 'Select Auth',
password: '',
sshKey: '',
keyType: '',
}));
}
} catch (error) {
console.error("Authentication form error:", error);
}
};
@@ -76,8 +90,7 @@ const NoAuthenticationModal = ({ isHidden, form, setForm, setIsNoAuthHidden, han
reader.onload = (event) => {
const keyContent = event.target.result;
let keyType = 'UNKNOWN';
// Detect key type from content
if (keyContent.includes('BEGIN RSA PRIVATE KEY') || keyContent.includes('BEGIN RSA PUBLIC KEY')) {
keyType = 'RSA';
} else if (keyContent.includes('BEGIN OPENSSH PRIVATE KEY') && keyContent.includes('ssh-ed25519')) {