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:
Karmaa
2025-03-10 23:59:06 -05:00
parent 54f03d73ce
commit 4e277bdd07
13 changed files with 1057 additions and 26 deletions

4
.gitignore vendored
View File

@@ -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
View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -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/**"],
},
},
}) })