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 + +
{ + event.preventDefault(); + if (isFormValid()) handleCreate(); + }} + > + + + Username + setForm({ ...form, username: event.target.value })} + sx={{ + backgroundColor: theme.palette.general.primary, + color: theme.palette.text.primary, + }} + /> + + + Password + setForm({ ...form, password: event.target.value })} + sx={{ + backgroundColor: theme.palette.general.primary, + color: theme.palette.text.primary, + }} + /> + + + + +
+
+
+
+
+ ); +}; + +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 + +
{ + event.preventDefault(); + if (isFormValid()) handleLogin(); + }} + > + + + Username + setForm({ ...form, username: event.target.value })} + sx={{ + backgroundColor: theme.palette.general.primary, + color: theme.palette.text.primary, + }} + /> + + + Password + setForm({ ...form, password: event.target.value })} + sx={{ + backgroundColor: theme.palette.general.primary, + color: theme.palette.text.primary, + }} + /> + + + + +
+
+
+
+
+ ); +}; + +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