Added user system with database features. This is fairly experimental and does not include dockerfile to automatically generate a mongodb. This should be in future commits along with ability to save hosts branching off this database feature.
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -158,3 +158,7 @@ typings/
|
|||||||
|
|
||||||
# .local
|
# .local
|
||||||
.local/
|
.local/
|
||||||
|
/docker/docker-compose.yml
|
||||||
|
/src/data/
|
||||||
|
/docker/mongodb/
|
||||||
|
/docker/docker-compose.yml
|
||||||
|
|||||||
206
package-lock.json
generated
206
package-lock.json
generated
@@ -20,11 +20,13 @@
|
|||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"crypto": "^1.0.1",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"embla-carousel-react": "^7.1.0",
|
"embla-carousel-react": "^7.1.0",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"is-stream": "^4.0.1",
|
"is-stream": "^4.0.1",
|
||||||
"make-dir": "^5.0.0",
|
"make-dir": "^5.0.0",
|
||||||
|
"mongoose": "^8.12.1",
|
||||||
"node-ssh": "^13.2.0",
|
"node-ssh": "^13.2.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
@@ -1200,6 +1202,15 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@mongodb-js/saslprep": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-+ywrb0AqkfaYuhHs6LxKWgqbh3I72EpEgESCw37o+9qPx9WTCkgDm2B+eMrwehGtHBWHFU4GXvnSCNiFhhausg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"sparse-bitfield": "^3.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@mui/base": {
|
"node_modules/@mui/base": {
|
||||||
"version": "5.0.0-beta.40-0",
|
"version": "5.0.0-beta.40-0",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40-0.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40-0.tgz",
|
||||||
@@ -2528,6 +2539,21 @@
|
|||||||
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/webidl-conversions": {
|
||||||
|
"version": "7.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
|
||||||
|
"integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/whatwg-url": {
|
||||||
|
"version": "11.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz",
|
||||||
|
"integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/webidl-conversions": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vitejs/plugin-react": {
|
"node_modules/@vitejs/plugin-react": {
|
||||||
"version": "4.3.4",
|
"version": "4.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz",
|
||||||
@@ -2958,6 +2984,15 @@
|
|||||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bson": {
|
||||||
|
"version": "6.10.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/bson/-/bson-6.10.3.tgz",
|
||||||
|
"integrity": "sha512-MTxGsqgYTwfshYWTRdmZRC+M7FnG1b4y7RO7p2k3X24Wq0yv1m77Wsj0BzlPzd/IowgESfsruQCUToa7vbOpPQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.20.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/buildcheck": {
|
"node_modules/buildcheck": {
|
||||||
"version": "0.0.6",
|
"version": "0.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz",
|
||||||
@@ -3223,6 +3258,13 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/crypto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==",
|
||||||
|
"deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
@@ -5275,6 +5317,15 @@
|
|||||||
"node": ">=4.0"
|
"node": ">=4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/kareem": {
|
||||||
|
"version": "2.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz",
|
||||||
|
"integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/keyv": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
@@ -5652,6 +5703,12 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/memory-pager": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/merge-descriptors": {
|
"node_modules/merge-descriptors": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||||
@@ -5716,6 +5773,105 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mongodb": {
|
||||||
|
"version": "6.14.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.14.2.tgz",
|
||||||
|
"integrity": "sha512-kMEHNo0F3P6QKDq17zcDuPeaywK/YaJVCEQRzPF3TOM/Bl9MFg64YE5Tu7ifj37qZJMhwU1tl2Ioivws5gRG5Q==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@mongodb-js/saslprep": "^1.1.9",
|
||||||
|
"bson": "^6.10.3",
|
||||||
|
"mongodb-connection-string-url": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.20.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@aws-sdk/credential-providers": "^3.188.0",
|
||||||
|
"@mongodb-js/zstd": "^1.1.0 || ^2.0.0",
|
||||||
|
"gcp-metadata": "^5.2.0",
|
||||||
|
"kerberos": "^2.0.1",
|
||||||
|
"mongodb-client-encryption": ">=6.0.0 <7",
|
||||||
|
"snappy": "^7.2.2",
|
||||||
|
"socks": "^2.7.1"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@aws-sdk/credential-providers": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@mongodb-js/zstd": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"gcp-metadata": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"kerberos": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"mongodb-client-encryption": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"snappy": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"socks": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mongodb-connection-string-url": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/whatwg-url": "^11.0.2",
|
||||||
|
"whatwg-url": "^14.1.0 || ^13.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mongoose": {
|
||||||
|
"version": "8.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.12.1.tgz",
|
||||||
|
"integrity": "sha512-UW22y8QFVYmrb36hm8cGncfn4ARc/XsYWQwRTaj0gxtQk1rDuhzDO1eBantS+hTTatfAIS96LlRCJrcNHvW5+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bson": "^6.10.3",
|
||||||
|
"kareem": "2.6.3",
|
||||||
|
"mongodb": "~6.14.0",
|
||||||
|
"mpath": "0.9.0",
|
||||||
|
"mquery": "5.0.0",
|
||||||
|
"ms": "2.1.3",
|
||||||
|
"sift": "17.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.20.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/mongoose"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mpath": {
|
||||||
|
"version": "0.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
|
||||||
|
"integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mquery": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "4.x"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@@ -6362,7 +6518,6 @@
|
|||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
@@ -6986,6 +7141,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sift": {
|
||||||
|
"version": "17.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz",
|
||||||
|
"integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/socket.io": {
|
"node_modules/socket.io": {
|
||||||
"version": "4.8.1",
|
"version": "4.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
|
||||||
@@ -7128,6 +7289,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sparse-bitfield": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"memory-pager": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ssh2": {
|
"node_modules/ssh2": {
|
||||||
"version": "1.16.0",
|
"version": "1.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz",
|
||||||
@@ -7335,6 +7505,18 @@
|
|||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tr46": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"punycode": "^2.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tweetnacl": {
|
"node_modules/tweetnacl": {
|
||||||
"version": "0.14.5",
|
"version": "0.14.5",
|
||||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||||
@@ -7652,6 +7834,28 @@
|
|||||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/webidl-conversions": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-url": {
|
||||||
|
"version": "14.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.1.tgz",
|
||||||
|
"integrity": "sha512-mDGf9diDad/giZ/Sm9Xi2YcyzaFpbdLpJPr+E9fSkyQ7KpQD4SdFcugkRQYzhmfI4KeV4Qpnn2sKPdo+kmsgRQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tr46": "^5.0.0",
|
||||||
|
"webidl-conversions": "^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
|||||||
@@ -22,11 +22,13 @@
|
|||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"crypto": "^1.0.1",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"embla-carousel-react": "^7.1.0",
|
"embla-carousel-react": "^7.1.0",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"is-stream": "^4.0.1",
|
"is-stream": "^4.0.1",
|
||||||
"make-dir": "^5.0.0",
|
"make-dir": "^5.0.0",
|
||||||
|
"mongoose": "^8.12.1",
|
||||||
"node-ssh": "^13.2.0",
|
"node-ssh": "^13.2.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
|||||||
192
src/App.jsx
192
src/App.jsx
@@ -1,6 +1,8 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { NewTerminal } from "./Terminal.jsx";
|
import { NewTerminal } from "./Terminal.jsx";
|
||||||
|
import { User } from "./User.jsx";
|
||||||
import AddHostModal from "./AddHostModal.jsx";
|
import AddHostModal from "./AddHostModal.jsx";
|
||||||
|
import LoginUserModal from "./LoginUserModal.jsx";
|
||||||
import { Button } from "@mui/joy";
|
import { Button } from "@mui/joy";
|
||||||
import { CssVarsProvider } from "@mui/joy";
|
import { CssVarsProvider } from "@mui/joy";
|
||||||
import theme from "./theme";
|
import theme from "./theme";
|
||||||
@@ -9,13 +11,23 @@ import Launchpad from "./Launchpad.jsx";
|
|||||||
import { Debounce } from './Utils';
|
import { Debounce } from './Utils';
|
||||||
import TermixIcon from "./images/termix_icon.png";
|
import TermixIcon from "./images/termix_icon.png";
|
||||||
import RocketIcon from './images/launchpad_rocket.png';
|
import RocketIcon from './images/launchpad_rocket.png';
|
||||||
|
import ProfileIcon from './images/profile_icon.png';
|
||||||
|
import CreateUserModal from "./CreateUserModal.jsx";
|
||||||
|
import ProfileModal from "./ProfileModal.jsx";
|
||||||
|
import ErrorModal from "./ErrorModal.jsx";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [isAddHostHidden, setIsAddHostHidden] = useState(true);
|
const [isAddHostHidden, setIsAddHostHidden] = useState(true);
|
||||||
|
const [isLoginUserHidden, setIsLoginUserHidden] = useState(true);
|
||||||
|
const [isCreateUserHidden, setIsCreateUserHidden] = useState(true);
|
||||||
|
const [isProfileHidden, setIsProfileHidden] = useState(true);
|
||||||
|
const [isErrorHidden, setIsErrorHidden] = useState(true);
|
||||||
|
const [errorMessage, setErrorMessage] = useState('');
|
||||||
const [terminals, setTerminals] = useState([]);
|
const [terminals, setTerminals] = useState([]);
|
||||||
|
const userRef = useRef(null);
|
||||||
const [activeTab, setActiveTab] = useState(null);
|
const [activeTab, setActiveTab] = useState(null);
|
||||||
const [nextId, setNextId] = useState(1);
|
const [nextId, setNextId] = useState(1);
|
||||||
const [form, setForm] = useState({
|
const [addHostForm, setAddHostForm] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
ip: "",
|
ip: "",
|
||||||
user: "",
|
user: "",
|
||||||
@@ -23,6 +35,14 @@ function App() {
|
|||||||
port: 22,
|
port: 22,
|
||||||
authMethod: "Select Auth",
|
authMethod: "Select Auth",
|
||||||
});
|
});
|
||||||
|
const [loginUserForm, setLoginUserForm] = useState({
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
});
|
||||||
|
const [createUserForm, setCreateUserForm] = useState({
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
});
|
||||||
const [isLaunchpadOpen, setIsLaunchpadOpen] = useState(false);
|
const [isLaunchpadOpen, setIsLaunchpadOpen] = useState(false);
|
||||||
const [splitTabIds, setSplitTabIds] = useState([]);
|
const [splitTabIds, setSplitTabIds] = useState([]);
|
||||||
|
|
||||||
@@ -81,17 +101,38 @@ function App() {
|
|||||||
});
|
});
|
||||||
}, [splitTabIds]);
|
}, [splitTabIds]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const sessionToken = localStorage.getItem('sessionToken');
|
||||||
|
if (sessionToken) {
|
||||||
|
setTimeout(() => {
|
||||||
|
handleLoginUser({
|
||||||
|
sessionToken,
|
||||||
|
onSuccess: () => {
|
||||||
|
setIsLoginUserHidden(true);
|
||||||
|
},
|
||||||
|
onFailure: (error) => {
|
||||||
|
setErrorMessage(`Auto-login failed: ${error}`);
|
||||||
|
setIsErrorHidden(false);
|
||||||
|
setIsLoginUserHidden(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
} else {
|
||||||
|
setIsLoginUserHidden(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleAddHost = () => {
|
const handleAddHost = () => {
|
||||||
if (form.ip && form.user && ((form.authMethod === 'password' && form.password) || (form.authMethod === 'rsaKey' && form.rsaKey)) && form.port) {
|
if (addHostForm.ip && addHostForm.user && ((addHostForm.authMethod === 'password' && addHostForm.password) || (addHostForm.authMethod === 'rsaKey' && addHostForm.rsaKey)) && addHostForm.port) {
|
||||||
const newTerminal = {
|
const newTerminal = {
|
||||||
id: nextId,
|
id: nextId,
|
||||||
title: form.name || form.ip,
|
title: addHostForm.name || addHostForm.ip,
|
||||||
hostConfig: {
|
hostConfig: {
|
||||||
ip: form.ip,
|
ip: addHostForm.ip,
|
||||||
user: form.user,
|
user: addHostForm.user,
|
||||||
password: form.authMethod === 'password' ? form.password : undefined,
|
password: addHostForm.authMethod === 'password' ? addHostForm.password : undefined,
|
||||||
rsaKey: form.authMethod === 'rsaKey' ? form.rsaKey : undefined,
|
rsaKey: addHostForm.authMethod === 'rsaKey' ? addHostForm.rsaKey : undefined,
|
||||||
port: String(form.port),
|
port: String(addHostForm.port),
|
||||||
},
|
},
|
||||||
terminalRef: null,
|
terminalRef: null,
|
||||||
};
|
};
|
||||||
@@ -99,12 +140,58 @@ function App() {
|
|||||||
setActiveTab(nextId);
|
setActiveTab(nextId);
|
||||||
setNextId(nextId + 1);
|
setNextId(nextId + 1);
|
||||||
setIsAddHostHidden(true);
|
setIsAddHostHidden(true);
|
||||||
setForm({ name: "", ip: "", user: "", password: "", rsaKey: "", port: 22, authMethod: "Select Auth" });
|
setAddHostForm({ name: "", ip: "", user: "", password: "", rsaKey: "", port: 22, authMethod: "Select Auth" });
|
||||||
} else {
|
} else {
|
||||||
alert("Please fill out all fields.");
|
alert("Please fill out all fields.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLoginUser = ({ username, password, sessionToken, onSuccess, onFailure }) => {
|
||||||
|
if (userRef.current) {
|
||||||
|
if (sessionToken) {
|
||||||
|
userRef.current.loginUser({
|
||||||
|
sessionToken,
|
||||||
|
onSuccess,
|
||||||
|
onFailure,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
userRef.current.loginUser({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
onSuccess,
|
||||||
|
onFailure,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateUser = ({ username, password, onSuccess, onFailure }) => {
|
||||||
|
if (userRef.current) {
|
||||||
|
userRef.current.createUser({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
onSuccess,
|
||||||
|
onFailure,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteUser = ({ onSuccess, onFailure }) => {
|
||||||
|
if (userRef.current) {
|
||||||
|
userRef.current.deleteUser({
|
||||||
|
onSuccess,
|
||||||
|
onFailure,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogoutUser = () => {
|
||||||
|
if (userRef.current) {
|
||||||
|
userRef.current.logoutUser();
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const closeTab = (id) => {
|
const closeTab = (id) => {
|
||||||
const newTerminals = terminals.filter((t) => t.id !== id);
|
const newTerminals = terminals.filter((t) => t.id !== id);
|
||||||
setTerminals(newTerminals);
|
setTerminals(newTerminals);
|
||||||
@@ -197,6 +284,28 @@ function App() {
|
|||||||
>
|
>
|
||||||
+
|
+
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{/* Profile Button */}
|
||||||
|
<Button
|
||||||
|
onClick={() => setIsProfileHidden(false)}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.tertiary,
|
||||||
|
"&:hover": { backgroundColor: theme.palette.general.secondary },
|
||||||
|
flexShrink: 0,
|
||||||
|
height: "52px",
|
||||||
|
width: "52px",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={ProfileIcon}
|
||||||
|
alt="Profile"
|
||||||
|
style={{ width: "70%", height: "70%", objectFit: "contain" }}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Terminal Views */}
|
{/* Terminal Views */}
|
||||||
@@ -209,10 +318,8 @@ function App() {
|
|||||||
} flex-1`}
|
} flex-1`}
|
||||||
style={{
|
style={{
|
||||||
order: splitTabIds.includes(terminal.id)
|
order: splitTabIds.includes(terminal.id)
|
||||||
? splitTabIds.indexOf(terminal.id) + 1
|
? splitTabIds.indexOf(terminal.id)
|
||||||
: activeTab === terminal.id
|
: 0,
|
||||||
? 0
|
|
||||||
: undefined
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<NewTerminal
|
<NewTerminal
|
||||||
@@ -220,13 +327,7 @@ function App() {
|
|||||||
hostConfig={terminal.hostConfig}
|
hostConfig={terminal.hostConfig}
|
||||||
isVisible={activeTab === terminal.id || splitTabIds.includes(terminal.id)}
|
isVisible={activeTab === terminal.id || splitTabIds.includes(terminal.id)}
|
||||||
ref={(ref) => {
|
ref={(ref) => {
|
||||||
if (ref && !terminal.terminalRef) {
|
terminal.terminalRef = ref;
|
||||||
setTerminals((prev) =>
|
|
||||||
prev.map((t) =>
|
|
||||||
t.id === terminal.id ? { ...t, terminalRef: ref } : t
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -237,12 +338,57 @@ function App() {
|
|||||||
{/* Modals */}
|
{/* Modals */}
|
||||||
<AddHostModal
|
<AddHostModal
|
||||||
isHidden={isAddHostHidden}
|
isHidden={isAddHostHidden}
|
||||||
form={form}
|
form={addHostForm}
|
||||||
setForm={setForm}
|
setForm={setAddHostForm}
|
||||||
handleAddHost={handleAddHost}
|
handleAddHost={handleAddHost}
|
||||||
setIsAddHostHidden={setIsAddHostHidden}
|
setIsAddHostHidden={setIsAddHostHidden}
|
||||||
/>
|
/>
|
||||||
|
<LoginUserModal
|
||||||
|
isHidden={isLoginUserHidden}
|
||||||
|
form={loginUserForm}
|
||||||
|
setForm={setLoginUserForm}
|
||||||
|
handleLoginUser={handleLoginUser}
|
||||||
|
setIsLoginUserHidden={setIsLoginUserHidden}
|
||||||
|
setIsCreateUserHidden={setIsCreateUserHidden}
|
||||||
|
/>
|
||||||
|
<CreateUserModal
|
||||||
|
isHidden={isCreateUserHidden}
|
||||||
|
form={createUserForm}
|
||||||
|
setForm={setCreateUserForm}
|
||||||
|
handleCreateUser={handleCreateUser}
|
||||||
|
setIsCreateUserHidden={setIsCreateUserHidden}
|
||||||
|
setIsLoginUserHidden={setIsLoginUserHidden}
|
||||||
|
/>
|
||||||
|
<ProfileModal
|
||||||
|
isHidden={isProfileHidden}
|
||||||
|
handleDeleteUser={handleDeleteUser}
|
||||||
|
handleLogoutUser={handleLogoutUser}
|
||||||
|
setIsProfileHidden={setIsProfileHidden}
|
||||||
|
/>
|
||||||
|
<ErrorModal
|
||||||
|
isHidden={isErrorHidden}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
setIsErrorHidden={setIsErrorHidden}
|
||||||
|
/>
|
||||||
{isLaunchpadOpen && <Launchpad onClose={() => setIsLaunchpadOpen(false)} />}
|
{isLaunchpadOpen && <Launchpad onClose={() => setIsLaunchpadOpen(false)} />}
|
||||||
|
|
||||||
|
{/* User component */}
|
||||||
|
<User
|
||||||
|
ref={userRef}
|
||||||
|
onLoginSuccess={() => setIsLoginUserHidden(true)}
|
||||||
|
onCreateSuccess={() => {
|
||||||
|
setIsCreateUserHidden(true);
|
||||||
|
handleLoginUser({ username: createUserForm.username, password: createUserForm.password })}
|
||||||
|
}
|
||||||
|
onDeleteSuccess={() => {
|
||||||
|
setIsProfileHidden(true);
|
||||||
|
window.location.reload();
|
||||||
|
}}
|
||||||
|
onFailure={(error) => {
|
||||||
|
setErrorMessage(`Action failed: ${error}`);
|
||||||
|
setIsErrorHidden(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CssVarsProvider>
|
</CssVarsProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
122
src/CreateUserModal.jsx
Normal file
122
src/CreateUserModal.jsx
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { CssVarsProvider } from '@mui/joy/styles';
|
||||||
|
import { Modal, Button, FormControl, FormLabel, Input, Stack, DialogTitle, DialogContent, ModalDialog } from '@mui/joy';
|
||||||
|
import theme from './theme';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
const CreateUserModal = ({ isHidden, form, setForm, handleCreateUser, setIsCreateUserHidden, setIsLoginUserHidden }) => {
|
||||||
|
const isFormValid = () => {
|
||||||
|
if (!form.username || !form.password) return false;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreate = () => {
|
||||||
|
handleCreateUser({
|
||||||
|
...form
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isHidden) {
|
||||||
|
setForm({ username: '', password: '' });
|
||||||
|
}
|
||||||
|
}, [isHidden]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CssVarsProvider theme={theme}>
|
||||||
|
<Modal open={!isHidden} onClose={() => {}}>
|
||||||
|
<ModalDialog
|
||||||
|
layout="center"
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.tertiary,
|
||||||
|
borderColor: theme.palette.general.secondary,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
padding: 3,
|
||||||
|
borderRadius: 10,
|
||||||
|
width: "auto",
|
||||||
|
maxWidth: "90vw",
|
||||||
|
minWidth: "fit-content",
|
||||||
|
overflow: "hidden",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTitle>Create</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<form
|
||||||
|
onSubmit={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (isFormValid()) handleCreate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack spacing={2} sx={{ width: "100%", maxWidth: "100%", overflow: "hidden" }}>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Username</FormLabel>
|
||||||
|
<Input
|
||||||
|
value={form.username}
|
||||||
|
onChange={(event) => setForm({ ...form, username: event.target.value })}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.primary,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Password</FormLabel>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
value={form.password}
|
||||||
|
onChange={(event) => setForm({ ...form, password: event.target.value })}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.primary,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={!isFormValid()}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.primary,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: theme.palette.general.disabled,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setForm({ username: '', password: '' });
|
||||||
|
setIsCreateUserHidden(true);
|
||||||
|
setIsLoginUserHidden(false);
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.primary,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: theme.palette.general.disabled,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</ModalDialog>
|
||||||
|
</Modal>
|
||||||
|
</CssVarsProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
CreateUserModal.propTypes = {
|
||||||
|
isHidden: PropTypes.bool.isRequired,
|
||||||
|
form: PropTypes.object.isRequired,
|
||||||
|
setForm: PropTypes.func.isRequired,
|
||||||
|
handleCreateUser: PropTypes.func.isRequired,
|
||||||
|
setIsCreateUserHidden: PropTypes.func.isRequired,
|
||||||
|
setIsLoginUserHidden: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateUserModal;
|
||||||
56
src/ErrorModal.jsx
Normal file
56
src/ErrorModal.jsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { CssVarsProvider } from '@mui/joy/styles';
|
||||||
|
import { Modal, Button, DialogTitle, DialogContent, ModalDialog } from '@mui/joy';
|
||||||
|
import theme from './theme';
|
||||||
|
|
||||||
|
const ErrorModal = ({ isHidden, errorMessage, setIsErrorHidden }) => {
|
||||||
|
return (
|
||||||
|
<CssVarsProvider theme={theme}>
|
||||||
|
<Modal open={!isHidden} onClose={() => setIsErrorHidden(true)}>
|
||||||
|
<ModalDialog
|
||||||
|
layout="center"
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.tertiary,
|
||||||
|
borderColor: theme.palette.general.secondary,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
padding: 3,
|
||||||
|
borderRadius: 10,
|
||||||
|
width: "auto",
|
||||||
|
maxWidth: "90vw",
|
||||||
|
minWidth: "fit-content",
|
||||||
|
overflow: "hidden",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTitle sx={{ marginBottom: 1.5 }}>Error</DialogTitle>
|
||||||
|
<DialogContent sx={{ color: theme.palette.text.primary }}>
|
||||||
|
{errorMessage}
|
||||||
|
</DialogContent>
|
||||||
|
<Button
|
||||||
|
onClick={() => setIsErrorHidden(true)}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.primary,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: theme.palette.general.disabled,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</ModalDialog>
|
||||||
|
</Modal>
|
||||||
|
</CssVarsProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ErrorModal.propTypes = {
|
||||||
|
isHidden: PropTypes.bool.isRequired,
|
||||||
|
errorMessage: PropTypes.string.isRequired,
|
||||||
|
setIsErrorHidden: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ErrorModal;
|
||||||
122
src/LoginUserModal.jsx
Normal file
122
src/LoginUserModal.jsx
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { CssVarsProvider } from '@mui/joy/styles';
|
||||||
|
import { Modal, Button, FormControl, FormLabel, Input, Stack, DialogTitle, DialogContent, ModalDialog } from '@mui/joy';
|
||||||
|
import theme from './theme';
|
||||||
|
import {useEffect} from 'react';
|
||||||
|
|
||||||
|
const LoginUserModal = ({ isHidden, form, setForm, handleLoginUser, setIsLoginUserHidden, setIsCreateUserHidden }) => {
|
||||||
|
const isFormValid = () => {
|
||||||
|
if (!form.username || !form.password) return false;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogin = () => {
|
||||||
|
handleLoginUser({
|
||||||
|
...form,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isHidden) {
|
||||||
|
setForm({ username: '', password: '' });
|
||||||
|
}
|
||||||
|
}, [isHidden]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CssVarsProvider theme={theme}>
|
||||||
|
<Modal open={!isHidden} onClose={() => {}}>
|
||||||
|
<ModalDialog
|
||||||
|
layout="center"
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.tertiary,
|
||||||
|
borderColor: theme.palette.general.secondary,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
padding: 3,
|
||||||
|
borderRadius: 10,
|
||||||
|
width: "auto",
|
||||||
|
maxWidth: "90vw",
|
||||||
|
minWidth: "fit-content",
|
||||||
|
overflow: "hidden",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTitle>Login</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<form
|
||||||
|
onSubmit={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (isFormValid()) handleLogin();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack spacing={2} sx={{ width: "100%", maxWidth: "100%", overflow: "hidden" }}>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Username</FormLabel>
|
||||||
|
<Input
|
||||||
|
value={form.username}
|
||||||
|
onChange={(event) => setForm({ ...form, username: event.target.value })}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.primary,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Password</FormLabel>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
value={form.password}
|
||||||
|
onChange={(event) => setForm({ ...form, password: event.target.value })}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.primary,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={!isFormValid()}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.primary,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: theme.palette.general.disabled,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setForm({ username: '', password: '' });
|
||||||
|
setIsCreateUserHidden(false);
|
||||||
|
setIsLoginUserHidden(true);
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.primary,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: theme.palette.general.disabled,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Create User
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</ModalDialog>
|
||||||
|
</Modal>
|
||||||
|
</CssVarsProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
LoginUserModal.propTypes = {
|
||||||
|
isHidden: PropTypes.bool.isRequired,
|
||||||
|
form: PropTypes.object.isRequired,
|
||||||
|
setForm: PropTypes.func.isRequired,
|
||||||
|
handleLoginUser: PropTypes.func.isRequired,
|
||||||
|
setIsLoginUserHidden: PropTypes.func.isRequired,
|
||||||
|
setIsCreateUserHidden: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginUserModal;
|
||||||
85
src/ProfileModal.jsx
Normal file
85
src/ProfileModal.jsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { CssVarsProvider } from '@mui/joy/styles';
|
||||||
|
import {Modal, Button, DialogTitle, DialogContent, ModalDialog, Stack } from '@mui/joy';
|
||||||
|
import theme from './theme';
|
||||||
|
|
||||||
|
const ProfileModal = ({ isHidden, handleDeleteUser, handleLogoutUser, setIsProfileHidden }) => {
|
||||||
|
const handleDelete = () => {
|
||||||
|
handleDeleteUser({
|
||||||
|
onSuccess: () => {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
handleLogoutUser({
|
||||||
|
onSuccess: () => {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CssVarsProvider theme={theme}>
|
||||||
|
<Modal open={!isHidden} onClose={() => setIsProfileHidden(true)}>
|
||||||
|
<ModalDialog
|
||||||
|
layout="center"
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.tertiary,
|
||||||
|
borderColor: theme.palette.general.secondary,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
padding: 3,
|
||||||
|
borderRadius: 10,
|
||||||
|
width: "auto",
|
||||||
|
maxWidth: "90vw",
|
||||||
|
minWidth: "fit-content",
|
||||||
|
overflow: "hidden",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTitle sx={{ marginBottom: 1.5 }}>Profile</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Stack spacing={2} sx={{ width: "100%", maxWidth: "100%", overflow: "hidden", mt: 1.5 }}>
|
||||||
|
<Button
|
||||||
|
onClick={handleDelete}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.primary,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: theme.palette.general.disabled,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete User
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleLogout}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.primary,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: theme.palette.general.disabled,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</DialogContent>
|
||||||
|
</ModalDialog>
|
||||||
|
</Modal>
|
||||||
|
</CssVarsProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ProfileModal.propTypes = {
|
||||||
|
isHidden: PropTypes.bool.isRequired,
|
||||||
|
handleDeleteUser: PropTypes.func.isRequired,
|
||||||
|
handleLogoutUser: PropTypes.func.isRequired,
|
||||||
|
setIsProfileHidden: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProfileModal;
|
||||||
@@ -91,7 +91,6 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible }, ref) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
terminalInstance.current.attachCustomKeyEventHandler((event) => {
|
terminalInstance.current.attachCustomKeyEventHandler((event) => {
|
||||||
console.log("Event caled");
|
|
||||||
if (isPasting) return;
|
if (isPasting) return;
|
||||||
|
|
||||||
isPasting = true;
|
isPasting = true;
|
||||||
|
|||||||
129
src/User.jsx
Normal file
129
src/User.jsx
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import { useRef, forwardRef, useImperativeHandle } from "react";
|
||||||
|
import io from "socket.io-client";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
let socket;
|
||||||
|
|
||||||
|
if (!socket) {
|
||||||
|
socket = io(
|
||||||
|
window.location.hostname === "localhost"
|
||||||
|
? "http://localhost:8082"
|
||||||
|
: "/",
|
||||||
|
{
|
||||||
|
path: "/socket.io",
|
||||||
|
transports: ["websocket", "polling"],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const User = forwardRef(({ onLoginSuccess, onCreateSuccess, onDeleteSuccess, onFailure }, ref) => {
|
||||||
|
const socketRef = useRef(socket);
|
||||||
|
const currentUser = useRef(null);
|
||||||
|
|
||||||
|
const createUser = (userConfig) => {
|
||||||
|
if (socketRef.current) {
|
||||||
|
socketRef.current.emit("createUser", {
|
||||||
|
username: userConfig.username,
|
||||||
|
password: userConfig.password,
|
||||||
|
});
|
||||||
|
|
||||||
|
socketRef.current.once("userCreated", (data) => {
|
||||||
|
console.log("User created", data);
|
||||||
|
currentUser.current = {
|
||||||
|
id: data.user._id,
|
||||||
|
username: data.user.username,
|
||||||
|
sessionToken: data.user.sessionToken,
|
||||||
|
};
|
||||||
|
localStorage.setItem('sessionToken', data.user.sessionToken);
|
||||||
|
onCreateSuccess(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socketRef.current.once("error", (error) => {
|
||||||
|
console.error(error);
|
||||||
|
const errorMsg = (error && typeof error === 'object' && error !== null)
|
||||||
|
? error.error || error.message || 'An error occurred'
|
||||||
|
: String(error);
|
||||||
|
onFailure(errorMsg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loginUser = (userConfig) => {
|
||||||
|
if (socketRef.current) {
|
||||||
|
setTimeout(() => {
|
||||||
|
socketRef.current.emit("loginUser", {
|
||||||
|
username: userConfig.username,
|
||||||
|
password: userConfig.password,
|
||||||
|
sessionToken: userConfig.sessionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
socketRef.current.once("userFound", (data) => {
|
||||||
|
console.log("User found", data);
|
||||||
|
currentUser.current = {
|
||||||
|
id: data._id,
|
||||||
|
username: data.username,
|
||||||
|
sessionToken: data.sessionToken,
|
||||||
|
};
|
||||||
|
localStorage.setItem('sessionToken', data.sessionToken);
|
||||||
|
onLoginSuccess(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socketRef.current.once("error", (error) => {
|
||||||
|
console.error(error);
|
||||||
|
const errorMsg = (error && typeof error === 'object' && error !== null)
|
||||||
|
? error.error || error.message || 'An error occurred'
|
||||||
|
: String(error);
|
||||||
|
onFailure(errorMsg);
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const logoutUser = () => {
|
||||||
|
localStorage.removeItem('sessionToken');
|
||||||
|
currentUser.current = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteUser = () => {
|
||||||
|
if (currentUser.current?.id && socketRef.current) {
|
||||||
|
socketRef.current.emit("deleteUser", {
|
||||||
|
userId: currentUser.current.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
socketRef.current.once("userDeleted", (data) => {
|
||||||
|
console.log("User deleted", data);
|
||||||
|
onDeleteSuccess(data);
|
||||||
|
currentUser.current = null;
|
||||||
|
localStorage.removeItem('sessionToken');
|
||||||
|
});
|
||||||
|
|
||||||
|
socketRef.current.once("error", (error) => {
|
||||||
|
console.error(error);
|
||||||
|
const errorMsg = (error && typeof error === 'object' && error !== null)
|
||||||
|
? error.error || error.message || 'An error occurred'
|
||||||
|
: String(error);
|
||||||
|
onFailure(errorMsg);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
onFailure("No user is currently logged in.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
createUser,
|
||||||
|
loginUser,
|
||||||
|
logoutUser,
|
||||||
|
deleteUser,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return <div></div>;
|
||||||
|
});
|
||||||
|
|
||||||
|
User.displayName = "User";
|
||||||
|
|
||||||
|
User.propTypes = {
|
||||||
|
onLoginSuccess: PropTypes.func.isRequired,
|
||||||
|
onCreateSuccess: PropTypes.func.isRequired,
|
||||||
|
onDeleteSuccess: PropTypes.func.isRequired,
|
||||||
|
onFailure: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
156
src/backend/database.cjs
Normal file
156
src/backend/database.cjs
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
const http = require("http");
|
||||||
|
const socketIo = require("socket.io");
|
||||||
|
const mongoose = require("mongoose");
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
const server = http.createServer();
|
||||||
|
const io = socketIo(server, {
|
||||||
|
cors: {
|
||||||
|
origin: "*",
|
||||||
|
methods: ["GET", "POST"],
|
||||||
|
credentials: true
|
||||||
|
},
|
||||||
|
allowEIO3: true
|
||||||
|
});
|
||||||
|
|
||||||
|
async function connectToMongoDB() {
|
||||||
|
try {
|
||||||
|
const mongoUrl = process.env.MONGO_URL || 'mongodb://localhost:27017/termix';
|
||||||
|
await mongoose.connect(mongoUrl, {});
|
||||||
|
console.log('Connected to MongoDB');
|
||||||
|
|
||||||
|
const db = mongoose.connection.db;
|
||||||
|
|
||||||
|
// Create the 'users' collection if it doesn't exist
|
||||||
|
const collections = await db.listCollections().toArray();
|
||||||
|
if (!collections.find(col => col.name === 'users')) {
|
||||||
|
await db.createCollection('users');
|
||||||
|
console.log('Successfully created collection: users');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error connecting to MongoDB:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const userSchema = new mongoose.Schema({
|
||||||
|
username: { type: String, required: true, unique: true },
|
||||||
|
password: { type: String, required: true },
|
||||||
|
sessionToken: { type: String, required: true },
|
||||||
|
sshConnections: { type: [Object], default: [] },
|
||||||
|
});
|
||||||
|
|
||||||
|
const User = mongoose.model('User', userSchema);
|
||||||
|
|
||||||
|
async function createUser(username, password) {
|
||||||
|
try {
|
||||||
|
const userExists = await User.findOne({ username });
|
||||||
|
if (userExists) {
|
||||||
|
return { error: "User already exists for username" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionToken = crypto.randomBytes(64).toString('hex');
|
||||||
|
const newUser = new User({ username, password, sessionToken });
|
||||||
|
await newUser.save();
|
||||||
|
return { success: true, user: { _id: newUser._id, username: newUser.username, sessionToken: newUser.sessionToken } };
|
||||||
|
} catch (err) {
|
||||||
|
return { error: 'Error creating user: ' + err.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loginUser(username, password) {
|
||||||
|
try {
|
||||||
|
const user = await User.findOne({ username, password });
|
||||||
|
if (user) {
|
||||||
|
if (!user.sessionToken) {
|
||||||
|
user.sessionToken = crypto.randomBytes(64).toString('hex');
|
||||||
|
await user.save();
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
_id: user._id,
|
||||||
|
username: user.username,
|
||||||
|
sessionToken: user.sessionToken,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return { error: 'User not found or incorrect credentials for username' };
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return { error: 'Error checking user: ' + err.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loginWithToken(sessionToken) {
|
||||||
|
try {
|
||||||
|
const user = await User.findOne({ sessionToken });
|
||||||
|
if (user) {
|
||||||
|
return {
|
||||||
|
_id: user._id,
|
||||||
|
username: user.username,
|
||||||
|
sessionToken: user.sessionToken,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return { error: 'Invalid session token' };
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return { error: 'Error checking session token: ' + err.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteUser(userId) {
|
||||||
|
try {
|
||||||
|
const user = await User.findById(userId);
|
||||||
|
if (user) {
|
||||||
|
await User.deleteOne({ _id: userId });
|
||||||
|
return { success: true };
|
||||||
|
} else {
|
||||||
|
return { error: 'User not found'};
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return { error: 'Error removing user: ' + err.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
io.on("connection", (socket) => {
|
||||||
|
console.log("New socket connection established");
|
||||||
|
|
||||||
|
socket.on("createUser", async (data) => {
|
||||||
|
const { username, password } = data;
|
||||||
|
if (!username || !password) {
|
||||||
|
socket.emit("error", "Please provide both username and password");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = await createUser(username, password);
|
||||||
|
socket.emit(result.error ? "error" : "userCreated", result);
|
||||||
|
console.log(result.error || `User created`);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("loginUser", async (data) => {
|
||||||
|
const { username, password, sessionToken } = data;
|
||||||
|
let result;
|
||||||
|
if (sessionToken) {
|
||||||
|
result = await loginWithToken(sessionToken);
|
||||||
|
} else if (username && password) {
|
||||||
|
result = await loginUser(username, password);
|
||||||
|
} else {
|
||||||
|
socket.emit("error", "Please provide both username and password or a session token");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
socket.emit(result.error ? "error" : "userFound", result);
|
||||||
|
console.log(result.error || `User logged in`);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("deleteUser", async (data) => {
|
||||||
|
const { userId } = data;
|
||||||
|
if (!userId) {
|
||||||
|
socket.emit("error", "User ID is required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = await deleteUser(userId);
|
||||||
|
socket.emit(result.error ? "error" : "userDeleted", result);
|
||||||
|
console.log(result.error || `User deleted`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(8082, '0.0.0.0', async () => {
|
||||||
|
console.log("Server is running on port 8082");
|
||||||
|
await connectToMongoDB();
|
||||||
|
});
|
||||||
BIN
src/images/profile_icon.png
Normal file
BIN
src/images/profile_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
@@ -5,4 +5,10 @@ import tailwindcss from "@tailwindcss/vite";
|
|||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), tailwindcss()],
|
plugins: [react(), tailwindcss()],
|
||||||
|
|
||||||
|
server: {
|
||||||
|
watch: {
|
||||||
|
ignored: ["**/docker/**"],
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
Reference in New Issue
Block a user