diff --git a/.gitignore b/.gitignore
index 56bf7b8a..e1d6051b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -157,4 +157,8 @@ typings/
.dotnet/
# .local
-.local/
\ No newline at end of file
+.local/
+/docker/docker-compose.yml
+/src/data/
+/docker/mongodb/
+/docker/docker-compose.yml
diff --git a/package-lock.json b/package-lock.json
index c1084feb..30417680 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index dbc72f7a..7674c153 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/App.jsx b/src/App.jsx
index 6c1a6474..eb34e7e6 100644
--- a/src/App.jsx
+++ b/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() {
>
+
+
+ {/* Profile Button */}
+
{/* 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,
}}
>
{
- if (ref && !terminal.terminalRef) {
- setTerminals((prev) =>
- prev.map((t) =>
- t.id === terminal.id ? { ...t, terminalRef: ref } : t
- )
- );
- }
+ terminal.terminalRef = ref;
}}
/>
@@ -237,12 +338,57 @@ function App() {
{/* Modals */}
+
+
+
+
{isLaunchpadOpen && setIsLaunchpadOpen(false)} />}
+
+ {/* User component */}
+ 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);
+ }}
+ />
);
diff --git a/src/CreateUserModal.jsx b/src/CreateUserModal.jsx
new file mode 100644
index 00000000..c9e6d797
--- /dev/null
+++ b/src/CreateUserModal.jsx
@@ -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 (
+
+ {}}>
+
+ Create
+
+
+
+
+
+
+ );
+};
+
+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;
\ No newline at end of file
diff --git a/src/ErrorModal.jsx b/src/ErrorModal.jsx
new file mode 100644
index 00000000..272b6675
--- /dev/null
+++ b/src/ErrorModal.jsx
@@ -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 (
+
+ setIsErrorHidden(true)}>
+
+ Error
+
+ {errorMessage}
+
+
+
+
+
+ );
+};
+
+ErrorModal.propTypes = {
+ isHidden: PropTypes.bool.isRequired,
+ errorMessage: PropTypes.string.isRequired,
+ setIsErrorHidden: PropTypes.func.isRequired,
+};
+
+export default ErrorModal;
\ No newline at end of file
diff --git a/src/LoginUserModal.jsx b/src/LoginUserModal.jsx
new file mode 100644
index 00000000..44ec49bc
--- /dev/null
+++ b/src/LoginUserModal.jsx
@@ -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 (
+
+ {}}>
+
+ Login
+
+
+
+
+
+
+ );
+};
+
+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;
\ No newline at end of file
diff --git a/src/ProfileModal.jsx b/src/ProfileModal.jsx
new file mode 100644
index 00000000..0910c35e
--- /dev/null
+++ b/src/ProfileModal.jsx
@@ -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 (
+
+ setIsProfileHidden(true)}>
+
+ Profile
+
+
+
+
+
+
+
+
+
+ );
+};
+
+ProfileModal.propTypes = {
+ isHidden: PropTypes.bool.isRequired,
+ handleDeleteUser: PropTypes.func.isRequired,
+ handleLogoutUser: PropTypes.func.isRequired,
+ setIsProfileHidden: PropTypes.func.isRequired,
+};
+
+export default ProfileModal;
\ No newline at end of file
diff --git a/src/Terminal.jsx b/src/Terminal.jsx
index 11a259e4..4e205814 100644
--- a/src/Terminal.jsx
+++ b/src/Terminal.jsx
@@ -91,7 +91,6 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible }, ref) => {
});
terminalInstance.current.attachCustomKeyEventHandler((event) => {
- console.log("Event caled");
if (isPasting) return;
isPasting = true;
diff --git a/src/User.jsx b/src/User.jsx
new file mode 100644
index 00000000..5863bcfc
--- /dev/null
+++ b/src/User.jsx
@@ -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 ;
+});
+
+User.displayName = "User";
+
+User.propTypes = {
+ onLoginSuccess: PropTypes.func.isRequired,
+ onCreateSuccess: PropTypes.func.isRequired,
+ onDeleteSuccess: PropTypes.func.isRequired,
+ onFailure: PropTypes.func.isRequired,
+};
\ No newline at end of file
diff --git a/src/backend/database.cjs b/src/backend/database.cjs
new file mode 100644
index 00000000..e72dec6e
--- /dev/null
+++ b/src/backend/database.cjs
@@ -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();
+});
\ No newline at end of file
diff --git a/src/images/profile_icon.png b/src/images/profile_icon.png
new file mode 100644
index 00000000..eb9822db
Binary files /dev/null and b/src/images/profile_icon.png differ
diff --git a/vite.config.js b/vite.config.js
index 5399993d..4f806a60 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -5,4 +5,10 @@ import tailwindcss from "@tailwindcss/vite";
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
+
+ server: {
+ watch: {
+ ignored: ["**/docker/**"],
+ },
+ },
})
\ No newline at end of file