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:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -157,4 +157,8 @@ typings/
|
||||
.dotnet/
|
||||
|
||||
# .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/xterm": "^5.5.0",
|
||||
"cors": "^2.8.5",
|
||||
"crypto": "^1.0.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"embla-carousel-react": "^7.1.0",
|
||||
"express": "^4.21.2",
|
||||
"is-stream": "^4.0.1",
|
||||
"make-dir": "^5.0.0",
|
||||
"mongoose": "^8.12.1",
|
||||
"node-ssh": "^13.2.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.3.1",
|
||||
@@ -1200,6 +1202,15 @@
|
||||
"@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": {
|
||||
"version": "5.0.0-beta.40-0",
|
||||
"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==",
|
||||
"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": {
|
||||
"version": "4.3.4",
|
||||
"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_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": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz",
|
||||
@@ -3223,6 +3258,13 @@
|
||||
"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": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
@@ -5275,6 +5317,15 @@
|
||||
"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": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@@ -5652,6 +5703,12 @@
|
||||
"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": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||
@@ -5716,6 +5773,105 @@
|
||||
"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": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -6362,7 +6518,6 @@
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -6986,6 +7141,12 @@
|
||||
"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": {
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
|
||||
@@ -7128,6 +7289,15 @@
|
||||
"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": {
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz",
|
||||
@@ -7335,6 +7505,18 @@
|
||||
"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": {
|
||||
"version": "0.14.5",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||
@@ -7652,6 +7834,28 @@
|
||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||
"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": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@@ -22,11 +22,13 @@
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"cors": "^2.8.5",
|
||||
"crypto": "^1.0.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"embla-carousel-react": "^7.1.0",
|
||||
"express": "^4.21.2",
|
||||
"is-stream": "^4.0.1",
|
||||
"make-dir": "^5.0.0",
|
||||
"mongoose": "^8.12.1",
|
||||
"node-ssh": "^13.2.0",
|
||||
"prop-types": "^15.8.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 { User } from "./User.jsx";
|
||||
import AddHostModal from "./AddHostModal.jsx";
|
||||
import LoginUserModal from "./LoginUserModal.jsx";
|
||||
import { Button } from "@mui/joy";
|
||||
import { CssVarsProvider } from "@mui/joy";
|
||||
import theme from "./theme";
|
||||
@@ -9,13 +11,23 @@ import Launchpad from "./Launchpad.jsx";
|
||||
import { Debounce } from './Utils';
|
||||
import TermixIcon from "./images/termix_icon.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() {
|
||||
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 userRef = useRef(null);
|
||||
const [activeTab, setActiveTab] = useState(null);
|
||||
const [nextId, setNextId] = useState(1);
|
||||
const [form, setForm] = useState({
|
||||
const [addHostForm, setAddHostForm] = useState({
|
||||
name: "",
|
||||
ip: "",
|
||||
user: "",
|
||||
@@ -23,6 +35,14 @@ function App() {
|
||||
port: 22,
|
||||
authMethod: "Select Auth",
|
||||
});
|
||||
const [loginUserForm, setLoginUserForm] = useState({
|
||||
username: "",
|
||||
password: "",
|
||||
});
|
||||
const [createUserForm, setCreateUserForm] = useState({
|
||||
username: "",
|
||||
password: "",
|
||||
});
|
||||
const [isLaunchpadOpen, setIsLaunchpadOpen] = useState(false);
|
||||
const [splitTabIds, setSplitTabIds] = useState([]);
|
||||
|
||||
@@ -81,17 +101,38 @@ function App() {
|
||||
});
|
||||
}, [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 = () => {
|
||||
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 = {
|
||||
id: nextId,
|
||||
title: form.name || form.ip,
|
||||
title: addHostForm.name || addHostForm.ip,
|
||||
hostConfig: {
|
||||
ip: form.ip,
|
||||
user: form.user,
|
||||
password: form.authMethod === 'password' ? form.password : undefined,
|
||||
rsaKey: form.authMethod === 'rsaKey' ? form.rsaKey : undefined,
|
||||
port: String(form.port),
|
||||
ip: addHostForm.ip,
|
||||
user: addHostForm.user,
|
||||
password: addHostForm.authMethod === 'password' ? addHostForm.password : undefined,
|
||||
rsaKey: addHostForm.authMethod === 'rsaKey' ? addHostForm.rsaKey : undefined,
|
||||
port: String(addHostForm.port),
|
||||
},
|
||||
terminalRef: null,
|
||||
};
|
||||
@@ -99,12 +140,58 @@ function App() {
|
||||
setActiveTab(nextId);
|
||||
setNextId(nextId + 1);
|
||||
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 {
|
||||
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 newTerminals = terminals.filter((t) => t.id !== id);
|
||||
setTerminals(newTerminals);
|
||||
@@ -197,6 +284,28 @@ function App() {
|
||||
>
|
||||
+
|
||||
</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>
|
||||
|
||||
{/* Terminal Views */}
|
||||
@@ -209,10 +318,8 @@ function App() {
|
||||
} flex-1`}
|
||||
style={{
|
||||
order: splitTabIds.includes(terminal.id)
|
||||
? splitTabIds.indexOf(terminal.id) + 1
|
||||
: activeTab === terminal.id
|
||||
? 0
|
||||
: undefined
|
||||
? splitTabIds.indexOf(terminal.id)
|
||||
: 0,
|
||||
}}
|
||||
>
|
||||
<NewTerminal
|
||||
@@ -220,13 +327,7 @@ function App() {
|
||||
hostConfig={terminal.hostConfig}
|
||||
isVisible={activeTab === terminal.id || splitTabIds.includes(terminal.id)}
|
||||
ref={(ref) => {
|
||||
if (ref && !terminal.terminalRef) {
|
||||
setTerminals((prev) =>
|
||||
prev.map((t) =>
|
||||
t.id === terminal.id ? { ...t, terminalRef: ref } : t
|
||||
)
|
||||
);
|
||||
}
|
||||
terminal.terminalRef = ref;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -237,12 +338,57 @@ function App() {
|
||||
{/* Modals */}
|
||||
<AddHostModal
|
||||
isHidden={isAddHostHidden}
|
||||
form={form}
|
||||
setForm={setForm}
|
||||
form={addHostForm}
|
||||
setForm={setAddHostForm}
|
||||
handleAddHost={handleAddHost}
|
||||
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)} />}
|
||||
|
||||
{/* 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>
|
||||
</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) => {
|
||||
console.log("Event caled");
|
||||
if (isPasting) return;
|
||||
|
||||
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/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
|
||||
server: {
|
||||
watch: {
|
||||
ignored: ["**/docker/**"],
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user