Fixed control v pasting formating. Reorganized location of scripts. Visbile password and confirm password. Guest login. OpenSSH key authentication. Optional to remember password. Serach for host viewer.

This commit is contained in:
Karmaa
2025-03-16 00:46:20 -05:00
parent 346d833f3d
commit fd966b9954
17 changed files with 976 additions and 249 deletions

241
package-lock.json generated
View File

@@ -11,6 +11,7 @@
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0", "@emotion/styled": "^11.14.0",
"@fontsource/inter": "^5.1.1", "@fontsource/inter": "^5.1.1",
"@mui/icons-material": "^6.4.7",
"@mui/joy": "^5.0.0-beta.51", "@mui/joy": "^5.0.0-beta.51",
"@tailwindcss/vite": "^4.0.8", "@tailwindcss/vite": "^4.0.8",
"@tiptap/extension-link": "^2.11.5", "@tiptap/extension-link": "^2.11.5",
@@ -1321,6 +1322,32 @@
"url": "https://opencollective.com/mui-org" "url": "https://opencollective.com/mui-org"
} }
}, },
"node_modules/@mui/icons-material": {
"version": "6.4.7",
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.4.7.tgz",
"integrity": "sha512-Rk8cs9ufQoLBw582Rdqq7fnSXXZTqhYRbpe1Y5SAz9lJKZP3CIdrj0PfG8HJLGw1hrsHFN/rkkm70IDzhJsG1g==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@mui/material": "^6.4.7",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/joy": { "node_modules/@mui/joy": {
"version": "5.0.0-beta.51", "version": "5.0.0-beta.51",
"resolved": "https://registry.npmjs.org/@mui/joy/-/joy-5.0.0-beta.51.tgz", "resolved": "https://registry.npmjs.org/@mui/joy/-/joy-5.0.0-beta.51.tgz",
@@ -1362,6 +1389,209 @@
} }
} }
}, },
"node_modules/@mui/material": {
"version": "6.4.7",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.7.tgz",
"integrity": "sha512-K65StXUeGAtFJ4ikvHKtmDCO5Ab7g0FZUu2J5VpoKD+O6Y3CjLYzRi+TMlI3kaL4CL158+FccMoOd/eaddmeRQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.26.0",
"@mui/core-downloads-tracker": "^6.4.7",
"@mui/system": "^6.4.7",
"@mui/types": "^7.2.21",
"@mui/utils": "^6.4.6",
"@popperjs/core": "^2.11.8",
"@types/react-transition-group": "^4.4.12",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"prop-types": "^15.8.1",
"react-is": "^19.0.0",
"react-transition-group": "^4.4.5"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@mui/material-pigment-css": "^6.4.7",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"@mui/material-pigment-css": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/material/node_modules/@mui/core-downloads-tracker": {
"version": "6.4.7",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.7.tgz",
"integrity": "sha512-XjJrKFNt9zAKvcnoIIBquXyFyhfrHYuttqMsoDS7lM7VwufYG4fAPw4kINjBFg++fqXM2BNAuWR9J7XVIuKIKg==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
}
},
"node_modules/@mui/material/node_modules/@mui/private-theming": {
"version": "6.4.6",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.4.6.tgz",
"integrity": "sha512-T5FxdPzCELuOrhpA2g4Pi6241HAxRwZudzAuL9vBvniuB5YU82HCmrARw32AuCiyTfWzbrYGGpZ4zyeqqp9RvQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.26.0",
"@mui/utils": "^6.4.6",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/material/node_modules/@mui/styled-engine": {
"version": "6.4.6",
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.4.6.tgz",
"integrity": "sha512-vSWYc9ZLX46be5gP+FCzWVn5rvDr4cXC5JBZwSIkYk9xbC7GeV+0kCvB8Q6XLFQJy+a62bbqtmdwS4Ghi9NBlQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.26.0",
"@emotion/cache": "^11.13.5",
"@emotion/serialize": "^1.3.3",
"@emotion/sheet": "^1.4.0",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
}
}
},
"node_modules/@mui/material/node_modules/@mui/system": {
"version": "6.4.7",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-6.4.7.tgz",
"integrity": "sha512-7wwc4++Ak6tGIooEVA9AY7FhH2p9fvBMORT4vNLMAysH3Yus/9B9RYMbrn3ANgsOyvT3Z7nE+SP8/+3FimQmcg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.26.0",
"@mui/private-theming": "^6.4.6",
"@mui/styled-engine": "^6.4.6",
"@mui/types": "^7.2.21",
"@mui/utils": "^6.4.6",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/material/node_modules/@mui/utils": {
"version": "6.4.6",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.4.6.tgz",
"integrity": "sha512-43nZeE1pJF2anGafNydUcYFPtHwAqiBiauRtaMvurdrZI3YrUjHkAu43RBsxef7OFtJMXGiHFvq43kb7lig0sA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.26.0",
"@mui/types": "^7.2.21",
"@types/prop-types": "^15.7.14",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
"react-is": "^19.0.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/material/node_modules/react-is": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz",
"integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==",
"license": "MIT",
"peer": true
},
"node_modules/@mui/private-theming": { "node_modules/@mui/private-theming": {
"version": "5.16.14", "version": "5.16.14",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.14.tgz", "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.14.tgz",
@@ -2583,7 +2813,6 @@
"version": "18.3.18", "version": "18.3.18",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
"integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
@@ -2600,6 +2829,16 @@
"@types/react": "^18.0.0" "@types/react": "^18.0.0"
} }
}, },
"node_modules/@types/react-transition-group": {
"version": "4.4.12",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
"integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "*"
}
},
"node_modules/@types/use-sync-external-store": { "node_modules/@types/use-sync-external-store": {
"version": "0.0.6", "version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",

View File

@@ -13,6 +13,7 @@
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0", "@emotion/styled": "^11.14.0",
"@fontsource/inter": "^5.1.1", "@fontsource/inter": "^5.1.1",
"@mui/icons-material": "^6.4.7",
"@mui/joy": "^5.0.0-beta.51", "@mui/joy": "^5.0.0-beta.51",
"@tailwindcss/vite": "^4.0.8", "@tailwindcss/vite": "^4.0.8",
"@tiptap/extension-link": "^2.11.5", "@tiptap/extension-link": "^2.11.5",

View File

@@ -1,14 +1,14 @@
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import { NewTerminal } from "./Terminal.jsx"; import { NewTerminal } from "./apps/ssh/Terminal.jsx";
import { User } from "./User.jsx"; import { User } from "./apps/user/User.jsx";
import AddHostModal from "./modals/AddHostModal.jsx"; import AddHostModal from "./modals/AddHostModal.jsx";
import LoginUserModal from "./modals/LoginUserModal.jsx"; import LoginUserModal from "./modals/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";
import TabList from "./TabList.jsx"; import TabList from "./ui/TabList.jsx";
import Launchpad from "./Launchpad.jsx"; import Launchpad from "./apps/Launchpad.jsx";
import { Debounce } from './Utils'; import { Debounce } from './other/Utils.jsx';
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 ProfileIcon from './images/profile_icon.png';
@@ -16,6 +16,7 @@ import CreateUserModal from "./modals/CreateUserModal.jsx";
import ProfileModal from "./modals/ProfileModal.jsx"; import ProfileModal from "./modals/ProfileModal.jsx";
import ErrorModal from "./modals/ErrorModal.jsx"; import ErrorModal from "./modals/ErrorModal.jsx";
import EditHostModal from "./modals/EditHostModal.jsx"; import EditHostModal from "./modals/EditHostModal.jsx";
import NoAuthenticationModal from "./modals/NoAuthenticationModal.jsx";
function App() { function App() {
const [isAddHostHidden, setIsAddHostHidden] = useState(true); const [isAddHostHidden, setIsAddHostHidden] = useState(true);
@@ -36,6 +37,7 @@ function App() {
port: 22, port: 22,
authMethod: "Select Auth", authMethod: "Select Auth",
rememberHost: false, rememberHost: false,
storePassword: true,
}); });
const [editHostForm, setEditHostForm] = useState({ const [editHostForm, setEditHostForm] = useState({
name: "", name: "",
@@ -45,6 +47,12 @@ function App() {
port: 22, port: 22,
authMethod: "Select Auth", authMethod: "Select Auth",
rememberHost: true, rememberHost: true,
storePassword: true,
});
const [isNoAuthHidden, setIsNoAuthHidden] = useState(true);
const [authForm, setAuthForm] = useState({
password: "",
rsaKey: "",
}); });
const [loginUserForm, setLoginUserForm] = useState({ const [loginUserForm, setLoginUserForm] = useState({
username: "", username: "",
@@ -136,11 +144,20 @@ function App() {
}, []); }, []);
const handleAddHost = () => { const handleAddHost = () => {
if (addHostForm.ip && addHostForm.user && ((addHostForm.authMethod === 'password' && addHostForm.password) || (addHostForm.authMethod === 'rsaKey' && addHostForm.rsaKey)) && addHostForm.port && addHostForm.authMethod !== 'Select Auth') { if (addHostForm.ip && addHostForm.user && addHostForm.port && addHostForm.authMethod !== 'Select Auth') {
if (addHostForm.authMethod === 'password' && !addHostForm.password) {
setIsNoAuthHidden(false);
} else if (addHostForm.authMethod === 'rsaKey' && !addHostForm.rsaKey) {
setIsNoAuthHidden(false);
} else {
connectToHost(); connectToHost();
if (addHostForm.rememberHost) { if (addHostForm.rememberHost) {
if (!addHostForm.storePassword) {
addHostForm.password = '';
}
handleSaveHost(); handleSaveHost();
} }
}
} else { } else {
alert("Please fill out all fields."); alert("Please fill out all fields.");
} }
@@ -166,6 +183,24 @@ function App() {
setAddHostForm({ name: "", ip: "", user: "", password: "", rsaKey: "", port: 22, authMethod: "Select Auth" }); setAddHostForm({ name: "", ip: "", user: "", password: "", rsaKey: "", port: 22, authMethod: "Select Auth" });
} }
const handleAuthSubmit = (form) => {
const updatedTerminals = terminals.map((terminal) => {
if (terminal.id === activeTab) {
return {
...terminal,
hostConfig: {
...terminal.hostConfig,
password: form.password,
rsaKey: form.rsaKey
}
};
}
return terminal;
});
setTerminals(updatedTerminals);
setIsNoAuthHidden(true);
};
const connectToHostWithConfig = (hostConfig) => { const connectToHostWithConfig = (hostConfig) => {
const newTerminal = { const newTerminal = {
id: nextId, id: nextId,
@@ -195,23 +230,6 @@ function App() {
} }
} }
const createFolder = (folderName) => {
if (userRef.current) {
userRef.current.createFolder({
folderName,
});
}
}
const moveHostToFolder = (folderName, hostConfig) => {
if (userRef.current) {
userRef.current.moveHostToFolder({
folderName,
hostConfig,
});
}
}
const handleLoginUser = ({ username, password, sessionToken, onSuccess, onFailure }) => { const handleLoginUser = ({ username, password, sessionToken, onSuccess, onFailure }) => {
if (userRef.current) { if (userRef.current) {
if (sessionToken) { if (sessionToken) {
@@ -231,6 +249,12 @@ function App() {
} }
}; };
const handleGuestLogin = () => {
if (userRef.current) {
userRef.current.loginAsGuest();
}
}
const handleCreateUser = ({ username, password, onSuccess, onFailure }) => { const handleCreateUser = ({ username, password, onSuccess, onFailure }) => {
if (userRef.current) { if (userRef.current) {
userRef.current.createUser({ userRef.current.createUser({
@@ -287,21 +311,31 @@ function App() {
} }
}; };
const handleEditHost = () => { const handleEditHost = async () => {
if (editHostForm.ip && editHostForm.user && ((editHostForm.authMethod === 'password' && editHostForm.password) || (editHostForm.authMethod === 'rsaKey' && editHostForm.rsaKey)) && editHostForm.port && editHostForm.authMethod !== 'Select Auth') { try {
editHostForm.rememberHost = true; // Only clear the password if switching to RSA or storePassword is false
if (editHostForm.authMethod === 'rsaKey') {
editHostForm.password = '';
} else if (!editHostForm.storePassword) {
editHostForm.password = '';
}
if (currentHostConfig) { await userRef.current.editHost({
userRef.current.editHost({
oldHostConfig: currentHostConfig, oldHostConfig: currentHostConfig,
newHostConfig: editHostForm, newHostConfig: editHostForm,
}); });
setIsEditHostHidden(true);
} else { // Refresh the updated config
alert("Host not found"); const refreshedHosts = await userRef.current.getAllHosts();
const updated = refreshedHosts.find(
(h) => h.config.ip === editHostForm.ip && h.config.user === editHostForm.user
);
if (updated) {
setCurrentHostConfig(updated.config);
} }
} else { setIsEditHostHidden(true);
alert("Please fill out all fields."); } catch (error) {
alert('Edit failed: ' + error);
} }
}; };
@@ -439,12 +473,20 @@ function App() {
key={terminal.id} key={terminal.id}
hostConfig={terminal.hostConfig} hostConfig={terminal.hostConfig}
isVisible={activeTab === terminal.id || splitTabIds.includes(terminal.id)} isVisible={activeTab === terminal.id || splitTabIds.includes(terminal.id)}
setIsNoAuthHidden={setIsNoAuthHidden}
ref={(ref) => { ref={(ref) => {
terminal.terminalRef = ref; terminal.terminalRef = ref;
}} }}
/> />
</div> </div>
))} ))}
<NoAuthenticationModal
isHidden={isNoAuthHidden}
form={authForm}
setForm={setAuthForm}
setIsNoAuthHidden={setIsNoAuthHidden}
handleAuthSubmit={handleAuthSubmit}
/>
</div> </div>
</div> </div>
@@ -495,8 +537,6 @@ function App() {
isErrorHidden={isErrorHidden} isErrorHidden={isErrorHidden}
deleteHost={deleteHost} deleteHost={deleteHost}
editHost={updateEditHostForm} editHost={updateEditHostForm}
createFolder={createFolder}
moveHostToFolder={moveHostToFolder}
/> />
)} )}
@@ -505,6 +545,7 @@ function App() {
form={loginUserForm} form={loginUserForm}
setForm={setLoginUserForm} setForm={setLoginUserForm}
handleLoginUser={handleLoginUser} handleLoginUser={handleLoginUser}
handleGuestLogin={handleGuestLogin}
setIsLoginUserHidden={setIsLoginUserHidden} setIsLoginUserHidden={setIsLoginUserHidden}
setIsCreateUserHidden={setIsCreateUserHidden} setIsCreateUserHidden={setIsCreateUserHidden}
/> />

View File

@@ -2,14 +2,13 @@ import PropTypes from 'prop-types';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { CssVarsProvider } from '@mui/joy/styles'; import { CssVarsProvider } from '@mui/joy/styles';
import { Button } from '@mui/joy'; import { Button } from '@mui/joy';
import HostViewerIcon from './images/host_viewer_icon.png'; import HostViewerIcon from '../images/host_viewer_icon.png';
import theme from './theme'; import theme from '../theme.js';
// Apps // Apps
import HostViewer from './apps/HostViewer'; import HostViewer from './ssh/HostViewer.jsx';
function Launchpad({ function Launchpad({onClose,
onClose,
getHosts, getHosts,
connectToHost, connectToHost,
isAddHostHidden, isAddHostHidden,
@@ -18,8 +17,6 @@ function Launchpad({
isErrorHidden, isErrorHidden,
deleteHost, deleteHost,
editHost, editHost,
createFolder,
moveHostToFolder,
}) { }) {
const launchpadRef = useRef(null); const launchpadRef = useRef(null);
const [sidebarOpen, setSidebarOpen] = useState(false); const [sidebarOpen, setSidebarOpen] = useState(false);
@@ -174,8 +171,6 @@ function Launchpad({
setIsAddHostHidden={setIsAddHostHidden} setIsAddHostHidden={setIsAddHostHidden}
deleteHost={deleteHost} deleteHost={deleteHost}
editHost={editHost} editHost={editHost}
createFolder={createFolder}
moveHostToFolder={moveHostToFolder}
onEditHostClick={handleEditHostClick} onEditHostClick={handleEditHostClick}
/> />
)} )}
@@ -196,8 +191,6 @@ Launchpad.propTypes = {
isErrorHidden: PropTypes.bool.isRequired, isErrorHidden: PropTypes.bool.isRequired,
deleteHost: PropTypes.func.isRequired, deleteHost: PropTypes.func.isRequired,
editHost: PropTypes.func.isRequired, editHost: PropTypes.func.isRequired,
createFolder: PropTypes.func.isRequired,
moveHostToFolder: PropTypes.func.isRequired,
}; };
export default Launchpad; export default Launchpad;

View File

@@ -1,10 +1,12 @@
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import { Button } from "@mui/joy"; import { Button, Input } from "@mui/joy";
function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, editHost }) { function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, editHost }) {
const [hosts, setHosts] = useState([]); const [hosts, setHosts] = useState([]);
const [filteredHosts, setFilteredHosts] = useState([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState("");
const isMounted = useRef(true); const isMounted = useRef(true);
const fetchHosts = async () => { const fetchHosts = async () => {
@@ -12,12 +14,14 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
const savedHosts = await getHosts(); const savedHosts = await getHosts();
if (isMounted.current) { if (isMounted.current) {
setHosts(savedHosts || []); setHosts(savedHosts || []);
setFilteredHosts(savedHosts || []);
setIsLoading(false); setIsLoading(false);
} }
} catch (error) { } catch (error) {
console.error("Host fetch failed:", error); console.error("Host fetch failed:", error);
if (isMounted.current) { if (isMounted.current) {
setHosts([]); setHosts([]);
setFilteredHosts([]);
setIsLoading(false); setIsLoading(false);
} }
} }
@@ -37,10 +41,28 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
}; };
}, []); }, []);
useEffect(() => {
const filtered = hosts.filter((hostWrapper) => {
const hostConfig = hostWrapper.config || {};
return hostConfig.name?.toLowerCase().includes(searchTerm.toLowerCase()) || hostConfig.ip?.toLowerCase().includes(searchTerm.toLowerCase());
});
setFilteredHosts(filtered);
}, [searchTerm, hosts]);
return ( return (
<div className="h-full w-full p-4 text-white flex flex-col"> <div className="h-full w-full p-4 text-white flex flex-col">
<div className="flex items-center justify-between mb-2 w-full"> <div className="flex items-center justify-between mb-2 w-full gap-2">
<h2 className="text-lg font-bold">Hosts</h2> <Input
placeholder="Search hosts..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
sx={{
flex: 1,
backgroundColor: "#6e6e6e",
color: "#fff",
"&::placeholder": { color: "#ccc" },
}}
/>
<Button <Button
className="text-black" className="text-black"
onClick={() => setIsAddHostHidden(false)} onClick={() => setIsAddHostHidden(false)}
@@ -55,9 +77,9 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
<div className="flex-grow overflow-auto"> <div className="flex-grow overflow-auto">
{isLoading ? ( {isLoading ? (
<p className="text-gray-300">Loading hosts...</p> <p className="text-gray-300">Loading hosts...</p>
) : hosts.length > 0 ? ( ) : filteredHosts.length > 0 ? (
<div className="flex flex-col gap-2 w-full"> <div className="flex flex-col gap-2 w-full">
{hosts.map((hostWrapper, index) => { {filteredHosts.map((hostWrapper, index) => {
const hostConfig = hostWrapper.config || {}; const hostConfig = hostWrapper.config || {};
if (!hostConfig) { if (!hostConfig) {

View File

@@ -4,9 +4,9 @@ import { FitAddon } from "@xterm/addon-fit";
import "@xterm/xterm/css/xterm.css"; import "@xterm/xterm/css/xterm.css";
import io from "socket.io-client"; import io from "socket.io-client";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import theme from "./theme"; import theme from "../../theme.js";
export const NewTerminal = forwardRef(({ hostConfig, isVisible }, ref) => { export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidden }, ref) => {
const terminalRef = useRef(null); const terminalRef = useRef(null);
const socketRef = useRef(null); const socketRef = useRef(null);
const fitAddon = useRef(new FitAddon()); const fitAddon = useRef(new FitAddon());
@@ -76,7 +76,11 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible }, ref) => {
fitAddon.current.fit(); fitAddon.current.fit();
resizeTerminal(); resizeTerminal();
const { cols, rows } = terminalInstance.current; const { cols, rows } = terminalInstance.current;
if (!hostConfig.password && !hostConfig.rsaKey) {
setIsNoAuthHidden(false);
} else {
socket.emit("connectToHost", cols, rows, hostConfig); socket.emit("connectToHost", cols, rows, hostConfig);
}
}); });
socket.on("data", (data) => { socket.on("data", (data) => {
@@ -91,23 +95,41 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible }, ref) => {
}); });
terminalInstance.current.attachCustomKeyEventHandler((event) => { terminalInstance.current.attachCustomKeyEventHandler((event) => {
if (isPasting) return;
isPasting = true;
setTimeout(() => {
isPasting = false;
}, 200);
if ((event.ctrlKey || event.metaKey) && event.key === "v") { if ((event.ctrlKey || event.metaKey) && event.key === "v") {
if (isPasting) return false;
isPasting = true;
event.preventDefault(); event.preventDefault();
navigator.clipboard.readText().then((text) => { navigator.clipboard.readText().then((text) => {
socketRef.current.emit("data", text); text = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
const lines = text.split("\n");
if (socketRef.current) {
let index = 0;
const sendLine = () => {
if (index < lines.length) {
socketRef.current.emit("data", lines[index] + "\r");
index++;
setTimeout(sendLine, 10);
} else {
isPasting = false;
}
};
sendLine();
} else {
isPasting = false;
}
}).catch((err) => { }).catch((err) => {
console.error("Failed to read clipboard contents:", err); console.error("Failed to read clipboard contents:", err);
isPasting = false;
}); });
return false; return false;
} }
return true; return true;
}); });
@@ -120,6 +142,10 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible }, ref) => {
} }
}); });
socket.on("noAuthRequired", () => {
setIsNoAuthHidden(false);
});
socket.on("error", (err) => { socket.on("error", (err) => {
terminalInstance.current.write(`\r\n*** Error: ${err} ***\r\n`); terminalInstance.current.write(`\r\n*** Error: ${err} ***\r\n`);
}); });
@@ -173,8 +199,10 @@ NewTerminal.propTypes = {
hostConfig: PropTypes.shape({ hostConfig: PropTypes.shape({
ip: PropTypes.string.isRequired, ip: PropTypes.string.isRequired,
user: PropTypes.string.isRequired, user: PropTypes.string.isRequired,
password: PropTypes.string.isRequired, password: PropTypes.string,
port: PropTypes.string.isRequired, rsaKey: PropTypes.string,
port: PropTypes.number.isRequired,
}).isRequired, }).isRequired,
isVisible: PropTypes.bool.isRequired, isVisible: PropTypes.bool.isRequired,
setIsNoAuthHidden: PropTypes.func.isRequired,
}; };

View File

@@ -12,12 +12,7 @@ const socket = io(SOCKET_URL, {
autoConnect: false, autoConnect: false,
}); });
export const User = forwardRef(({ export const User = forwardRef(({ onLoginSuccess, onCreateSuccess, onDeleteSuccess, onFailure }, ref) => {
onLoginSuccess,
onCreateSuccess,
onDeleteSuccess,
onFailure
}, ref) => {
const socketRef = useRef(socket); const socketRef = useRef(socket);
const currentUser = useRef(null); const currentUser = useRef(null);
@@ -100,6 +95,28 @@ export const User = forwardRef(({
} }
}; };
const loginAsGuest = async () => {
try {
const response = await new Promise((resolve) => {
socketRef.current.emit("loginAsGuest", resolve);
});
if (response?.success) {
currentUser.current = {
id: response.user.id,
username: response.user.username,
sessionToken: response.user.sessionToken,
};
localStorage.setItem("sessionToken", response.user.sessionToken);
onLoginSuccess(response.user);
} else {
throw new Error(response?.error || "Guest login failed");
}
} catch (error) {
onFailure(error.message);
}
}
const logoutUser = () => { const logoutUser = () => {
localStorage.removeItem("sessionToken"); localStorage.removeItem("sessionToken");
currentUser.current = null; currentUser.current = null;
@@ -236,6 +253,7 @@ export const User = forwardRef(({
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
createUser, createUser,
loginUser, loginUser,
loginAsGuest,
logoutUser, logoutUser,
deleteUser, deleteUser,
saveHost, saveHost,

View File

@@ -35,7 +35,8 @@ const User = mongoose.model('User', userSchema);
const Host = mongoose.model('Host', hostSchema); const Host = mongoose.model('Host', hostSchema);
const getEncryptionKey = (userId, sessionToken) => { const getEncryptionKey = (userId, sessionToken) => {
return crypto.scryptSync(`${userId}-${sessionToken}`, 'salt', 32); const salt = process.env.SALT || 'default_salt';
return crypto.scryptSync(`${userId}-${sessionToken}`, salt, 32);
}; };
const encryptData = (data, userId, sessionToken) => { const encryptData = (data, userId, sessionToken) => {
@@ -130,6 +131,29 @@ io.of('/database.io').on('connection', (socket) => {
} }
}); });
socket.on('loginAsGuest', async (callback) => {
try {
const username = `guest-${crypto.randomBytes(4).toString('hex')}`;
const sessionToken = crypto.randomBytes(64).toString('hex');
const user = await User.create({
username,
password: await bcrypt.hash(username, 10),
sessionToken
});
logger.info(`Guest user created: ${username}`);
callback({ success: true, user: {
id: user._id,
username: user.username,
sessionToken
}});
} catch (error) {
logger.error('Guest login error:', error);
callback({error: 'Guest login failed'});
}
});
socket.on('saveHostConfig', async ({ userId, sessionToken, hostConfig }, callback) => { socket.on('saveHostConfig', async ({ userId, sessionToken, hostConfig }, callback) => {
try { try {
if (!userId || !sessionToken) { if (!userId || !sessionToken) {

View File

@@ -28,6 +28,7 @@ io.on("connection", (socket) => {
socket.on("connectToHost", (cols, rows, hostConfig) => { socket.on("connectToHost", (cols, rows, hostConfig) => {
if (!hostConfig || !hostConfig.ip || !hostConfig.user || (!hostConfig.password && !hostConfig.rsaKey) || !hostConfig.port) { if (!hostConfig || !hostConfig.ip || !hostConfig.user || (!hostConfig.password && !hostConfig.rsaKey) || !hostConfig.port) {
logger.error("Invalid hostConfig received:", hostConfig); logger.error("Invalid hostConfig received:", hostConfig);
socket.emit("noAuthRequired");
return; return;
} }

View File

@@ -12,11 +12,17 @@ import {
ModalDialog, ModalDialog,
Select, Select,
Option, Option,
Checkbox Checkbox,
IconButton
} from '@mui/joy'; } from '@mui/joy';
import theme from '/src/theme'; import theme from '/src/theme';
import { useState } from 'react';
import Visibility from '@mui/icons-material/Visibility';
import VisibilityOff from '@mui/icons-material/VisibilityOff';
const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidden }) => { const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidden }) => {
const [showPassword, setShowPassword] = useState(false);
const handleFileChange = (e) => { const handleFileChange = (e) => {
const file = e.target.files[0]; const file = e.target.files[0];
if (file) { if (file) {
@@ -40,9 +46,34 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
return true; return true;
}; };
const handleSubmit = (event) => {
event.preventDefault();
if (isFormValid()) {
handleAddHost();
setForm({
name: '',
ip: '',
user: '',
password: '',
rsaKey: '',
port: 22,
authMethod: 'Select Auth',
rememberHost: false,
storePassword: true,
});
}
};
return ( return (
<CssVarsProvider theme={theme}> <CssVarsProvider theme={theme}>
<Modal open={!isHidden} onClose={() => setIsAddHostHidden(true)}> <Modal open={!isHidden} onClose={() => setIsAddHostHidden(true)}
sx={{
overflow: 'hidden',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
<ModalDialog <ModalDialog
layout="center" layout="center"
sx={{ sx={{
@@ -51,26 +82,17 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
color: theme.palette.text.primary, color: theme.palette.text.primary,
padding: 3, padding: 3,
borderRadius: 10, borderRadius: 10,
width: "auto", maxWidth: '400px',
maxWidth: "90vw", width: '100%',
minWidth: "fit-content", overflow: 'hidden',
overflow: "hidden", boxSizing: 'border-box',
display: "flex", mx: 2,
flexDirection: "column",
alignItems: "center",
}} }}
> >
<DialogTitle>Add Host</DialogTitle> <DialogTitle>Add Host</DialogTitle>
<DialogContent> <DialogContent>
<form <form onSubmit={handleSubmit}>
onSubmit={(event) => { <Stack spacing={2} sx={{ width: '100%' }}>
event.preventDefault();
if (isFormValid()) {
handleAddHost();
}
}}
>
<Stack spacing={2} sx={{ width: "100%", maxWidth: "100%", overflow: "hidden" }}>
<FormControl> <FormControl>
<FormLabel>Host Name</FormLabel> <FormLabel>Host Name</FormLabel>
<Input <Input
@@ -130,16 +152,28 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
{form.authMethod === 'password' && ( {form.authMethod === 'password' && (
<FormControl error={!form.password}> <FormControl error={!form.password}>
<FormLabel>Host Password</FormLabel> <FormLabel>Host Password</FormLabel>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Input <Input
type="password" type={showPassword ? 'text' : 'password'}
value={form.password} value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })} onChange={(e) => setForm({ ...form, password: e.target.value })}
required required
sx={{ sx={{
backgroundColor: theme.palette.general.primary, backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary, color: theme.palette.text.primary,
flex: 1,
}} }}
/> />
<IconButton
onClick={() => setShowPassword(!showPassword)}
sx={{
color: theme.palette.text.primary,
marginLeft: 1,
}}
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</div>
</FormControl> </FormControl>
)} )}
{form.authMethod === 'rsaKey' && ( {form.authMethod === 'rsaKey' && (
@@ -155,8 +189,6 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
padding: 1, padding: 1,
textAlign: 'center', textAlign: 'center',
width: '100%', width: '100%',
minWidth: 'auto',
minHeight: 'auto',
}} }}
/> />
</FormControl> </FormControl>
@@ -188,6 +220,21 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
}} }}
/> />
</FormControl> </FormControl>
{form.rememberHost && (
<FormControl>
<FormLabel>Store Password</FormLabel>
<Checkbox
checked={form.storePassword}
onChange={(e) => setForm({ ...form, storePassword: e.target.checked })}
sx={{
color: theme.palette.text.primary,
'&.Mui-checked': {
color: theme.palette.text.primary,
},
}}
/>
</FormControl>
)}
<Button <Button
type="submit" type="submit"
disabled={!isFormValid()} disabled={!isFormValid()}
@@ -220,6 +267,7 @@ AddHostModal.propTypes = {
port: PropTypes.number.isRequired, port: PropTypes.number.isRequired,
authMethod: PropTypes.string.isRequired, authMethod: PropTypes.string.isRequired,
rememberHost: PropTypes.bool, rememberHost: PropTypes.bool,
storePassword: PropTypes.bool,
}).isRequired, }).isRequired,
setForm: PropTypes.func.isRequired, setForm: PropTypes.func.isRequired,
handleAddHost: PropTypes.func.isRequired, handleAddHost: PropTypes.func.isRequired,

View File

@@ -1,12 +1,18 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { CssVarsProvider } from '@mui/joy/styles'; import { CssVarsProvider } from '@mui/joy/styles';
import { Modal, Button, FormControl, FormLabel, Input, Stack, DialogTitle, DialogContent, ModalDialog } from '@mui/joy'; import { Modal, Button, FormControl, FormLabel, Input, Stack, DialogTitle, DialogContent, ModalDialog, IconButton } from '@mui/joy';
import theme from '/src/theme'; import theme from '/src/theme';
import { useEffect } from 'react'; import { useEffect, useState } from 'react';
import Visibility from '@mui/icons-material/Visibility';
import VisibilityOff from '@mui/icons-material/VisibilityOff';
const CreateUserModal = ({ isHidden, form, setForm, handleCreateUser, setIsCreateUserHidden, setIsLoginUserHidden }) => { const CreateUserModal = ({ isHidden, form, setForm, handleCreateUser, setIsCreateUserHidden, setIsLoginUserHidden }) => {
const [confirmPassword, setConfirmPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const isFormValid = () => { const isFormValid = () => {
if (!form.username || !form.password) return false; if (!form.username || !form.password || form.password !== confirmPassword) return false;
return true; return true;
}; };
@@ -19,6 +25,7 @@ const CreateUserModal = ({ isHidden, form, setForm, handleCreateUser, setIsCreat
useEffect(() => { useEffect(() => {
if (isHidden) { if (isHidden) {
setForm({ username: '', password: '' }); setForm({ username: '', password: '' });
setConfirmPassword('');
} }
}, [isHidden]); }, [isHidden]);
@@ -64,15 +71,51 @@ const CreateUserModal = ({ isHidden, form, setForm, handleCreateUser, setIsCreat
</FormControl> </FormControl>
<FormControl> <FormControl>
<FormLabel>Password</FormLabel> <FormLabel>Password</FormLabel>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Input <Input
type="password" type={showPassword ? 'text' : 'password'}
value={form.password} value={form.password}
onChange={(event) => setForm({ ...form, password: event.target.value })} onChange={(event) => setForm({ ...form, password: event.target.value })}
sx={{ sx={{
backgroundColor: theme.palette.general.primary, backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary, color: theme.palette.text.primary,
flex: 1,
}} }}
/> />
<IconButton
onClick={() => setShowPassword(!showPassword)}
sx={{
color: theme.palette.text.primary,
marginLeft: 1,
}}
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</div>
</FormControl>
<FormControl>
<FormLabel>Confirm Password</FormLabel>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Input
type={showConfirmPassword ? 'text' : 'password'}
value={confirmPassword}
onChange={(event) => setConfirmPassword(event.target.value)}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
flex: 1,
}}
/>
<IconButton
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
sx={{
color: theme.palette.text.primary,
marginLeft: 1,
}}
>
{showConfirmPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</div>
</FormControl> </FormControl>
<Button <Button
type="submit" type="submit"
@@ -89,6 +132,7 @@ const CreateUserModal = ({ isHidden, form, setForm, handleCreateUser, setIsCreat
<Button <Button
onClick={() => { onClick={() => {
setForm({ username: '', password: '' }); setForm({ username: '', password: '' });
setConfirmPassword('');
setIsCreateUserHidden(true); setIsCreateUserHidden(true);
setIsLoginUserHidden(false); setIsLoginUserHidden(false);
}} }}

View File

@@ -1,5 +1,5 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useEffect } from 'react'; import { useEffect, useState } from 'react';
import { CssVarsProvider } from '@mui/joy/styles'; import { CssVarsProvider } from '@mui/joy/styles';
import { import {
Modal, Modal,
@@ -12,57 +12,100 @@ import {
DialogContent, DialogContent,
ModalDialog, ModalDialog,
Select, Select,
Option Option,
IconButton,
Checkbox
} from '@mui/joy'; } from '@mui/joy';
import theme from '/src/theme'; import theme from '/src/theme';
import Visibility from '@mui/icons-material/Visibility';
import VisibilityOff from '@mui/icons-material/VisibilityOff';
const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostHidden, hostConfig }) => { const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostHidden, hostConfig }) => {
const handleFileChange = (e) => { const [showPassword, setShowPassword] = useState(false);
const file = e.target.files[0];
if (file) {
if (file.name.endsWith('.rsa') || file.name.endsWith('.key') || file.name.endsWith('.pem') || file.name.endsWith('.der') || file.name.endsWith('.p8') || file.name.endsWith('.ssh')) {
const reader = new FileReader();
reader.onload = (event) => {
setForm({ ...form, rsaKey: event.target.result });
};
reader.readAsText(file);
} else {
alert("Please upload a valid RSA private key file.");
}
}
};
const isFormValid = () => {
if (form.authMethod === 'Select Auth') return false;
if (!form.ip || !form.user || !form.port) return false;
if (form.authMethod === 'rsaKey' && !form.rsaKey) return false;
if (form.authMethod === 'password' && !form.password) return false;
return true;
};
const handleEditHostInternal = (form) => {
const updatedForm = { ...form, name: form.name || form.ip };
handleEditHost(updatedForm);
};
useEffect(() => { useEffect(() => {
if (hostConfig) { if (hostConfig) {
const storePassword = hostConfig.password || hostConfig.rsaKey;
setForm({ setForm({
...form,
name: hostConfig.name || '', name: hostConfig.name || '',
ip: hostConfig.ip || '', ip: hostConfig.ip || '',
user: hostConfig.user || '', user: hostConfig.user || '',
password: hostConfig.password || '', password: storePassword && hostConfig.password ? hostConfig.password : '',
rsaKey: hostConfig.rsaKey || '', rsaKey: '',
port: Number(hostConfig.port) || 22, port: Number(hostConfig.port) || 22,
authMethod: hostConfig.password ? 'password' : 'rsaKey', authMethod: hostConfig.rsaKey ? 'rsaKey' : (storePassword ? 'password' : 'Select Auth'),
rememberHost: hostConfig.rememberHost || false, rememberHost: hostConfig.rememberHost || true,
storePassword: storePassword ?? false
}); });
} }
}, [hostConfig, setForm]); }, [hostConfig, setForm]);
const handleFileChange = (e) => {
const file = e.target.files[0];
if (file.name.endsWith('.rsa') || file.name.endsWith('.key') || file.name.endsWith('.pem') || file.name.endsWith('.der') || file.name.endsWith('.p8') || file.name.endsWith('.ssh') || file.name.endsWith('.pub')) {
const reader = new FileReader();
reader.onload = (evt) => {
setForm((prev) => ({ ...prev, rsaKey: evt.target.result }));
};
reader.readAsText(file);
} else {
alert('Please upload a valid RSA private key file.');
}
};
const handleAuthChange = (newMethod) => {
setForm((prev) => ({
...prev,
authMethod: newMethod
}));
};
const handleStorePasswordChange = (checked) => {
setForm((prev) => ({
...prev,
storePassword: checked,
authMethod: checked ? 'password' : 'Select Auth'
}));
};
const isFormValid = () => {
const { ip, user, port, authMethod, password, rsaKey, storePassword } = form;
if (!ip?.trim() || !user?.trim() || !port) return false;
const portNum = Number(port);
if (isNaN(portNum) || portNum < 1 || portNum > 65535) return false;
if (storePassword && authMethod === 'password' && !password.trim()) return false;
if (storePassword && authMethod === 'rsaKey' && !rsaKey && !hostConfig?.rsaKey) return false;
if (storePassword && authMethod === 'Select Auth') return false;
return true;
};
const handleSubmit = (e) => {
e.preventDefault();
if (isFormValid()) {
const { authMethod, password, rsaKey, storePassword, ...rest } = form;
handleEditHost({
...rest,
authMethod,
password: authMethod === 'password' && storePassword ? password : '',
rsaKey: authMethod === 'rsaKey' ? rsaKey : ''
});
}
};
return ( return (
<CssVarsProvider theme={theme}> <CssVarsProvider theme={theme}>
<Modal open={!isHidden} onClose={() => setIsEditHostHidden(true)}> <Modal open={!isHidden} onClose={() => setIsEditHostHidden(true)}
sx={{
overflowX: 'hidden',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
<ModalDialog <ModalDialog
layout="center" layout="center"
sx={{ sx={{
@@ -71,139 +114,167 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
color: theme.palette.text.primary, color: theme.palette.text.primary,
padding: 3, padding: 3,
borderRadius: 10, borderRadius: 10,
width: "auto", maxWidth: '400px',
maxWidth: "90vw", width: '100%',
minWidth: "fit-content", overflow: 'hidden',
overflow: "hidden", boxSizing: 'border-box',
display: "flex", mx: 2,
flexDirection: "column",
alignItems: "center",
}} }}
> >
<DialogTitle>Edit Host</DialogTitle> <DialogTitle>Edit Host</DialogTitle>
<DialogContent> <DialogContent>
<form <form onSubmit={handleSubmit}>
onSubmit={(event) => { <Stack spacing={2} sx={{ width: '100%', overflow: 'hidden' }}>
event.preventDefault();
if (isFormValid()) handleEditHostInternal(form);
}}
>
<Stack spacing={2} sx={{ width: "100%", maxWidth: "100%", overflow: "hidden" }}>
<FormControl> <FormControl>
<FormLabel>Host Name</FormLabel> <FormLabel>Host Name</FormLabel>
<Input <Input
value={form.name} value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })} onChange={(e) => setForm((prev) => ({ ...prev, name: e.target.value }))}
sx={{ sx={{
backgroundColor: theme.palette.general.primary, backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary, color: theme.palette.text.primary
}} }}
/> />
</FormControl> </FormControl>
<FormControl error={!form.ip}> <FormControl error={!form.ip}>
<FormLabel>Host IP</FormLabel> <FormLabel>Host IP</FormLabel>
<Input <Input
value={form.ip} value={form.ip}
onChange={(e) => setForm({ ...form, ip: e.target.value })} onChange={(e) => setForm((prev) => ({ ...prev, ip: e.target.value }))}
required
sx={{ sx={{
backgroundColor: theme.palette.general.primary, backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary, color: theme.palette.text.primary
}} }}
/> />
</FormControl> </FormControl>
<FormControl error={!form.user}> <FormControl error={!form.user}>
<FormLabel>Host User</FormLabel> <FormLabel>Host User</FormLabel>
<Input <Input
value={form.user} value={form.user}
onChange={(e) => setForm({ ...form, user: e.target.value })} onChange={(e) => setForm((prev) => ({ ...prev, user: e.target.value }))}
required
sx={{ sx={{
backgroundColor: theme.palette.general.primary, backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary, color: theme.palette.text.primary
}} }}
/> />
</FormControl> </FormControl>
<FormControl error={!form.authMethod || form.authMethod === 'Select Auth'}>
{form.storePassword && form.authMethod !== 'Select Auth' && (
<FormControl error={form.authMethod === 'Select Auth'}>
<FormLabel>Authentication Method</FormLabel> <FormLabel>Authentication Method</FormLabel>
<Select <Select
value={form.authMethod || 'Select Auth'} value={form.authMethod}
onChange={(e, newValue) => setForm({ ...form, authMethod: newValue })} onChange={(e, val) => handleAuthChange(val)}
required
sx={{ sx={{
backgroundColor: !form.authMethod || form.authMethod === 'Select Auth' ? theme.palette.general.tertiary : theme.palette.general.primary, backgroundColor:
form.authMethod === 'Select Auth'
? theme.palette.general.tertiary
: theme.palette.general.primary,
color: theme.palette.text.primary, color: theme.palette.text.primary,
'&:hover': { '&:hover': {
backgroundColor: theme.palette.general.disabled, backgroundColor: theme.palette.general.disabled
}, }
}} }}
> >
<Option value="Select Auth" disabled> <Option value="Select Auth" disabled>Select Auth</Option>
Select Auth
</Option>
<Option value="password">Password</Option> <Option value="password">Password</Option>
<Option value="rsaKey">RSA Key</Option> <Option value="rsaKey">RSA Key</Option>
</Select> </Select>
</FormControl> </FormControl>
{form.authMethod === 'password' && ( )}
{form.authMethod === 'password' && form.storePassword && (
<FormControl error={!form.password}> <FormControl error={!form.password}>
<FormLabel>Host Password</FormLabel> <FormLabel>Password</FormLabel>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Input <Input
type="password" type={showPassword ? 'text' : 'password'}
value={form.password} value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })} onChange={(e) =>
required setForm((prev) => ({ ...prev, password: e.target.value }))
}
sx={{ sx={{
backgroundColor: theme.palette.general.primary, backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary, color: theme.palette.text.primary,
flex: 1
}} }}
/> />
<IconButton
onClick={() => setShowPassword(!showPassword)}
sx={{
color: theme.palette.text.primary,
marginLeft: 1
}}
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</div>
</FormControl> </FormControl>
)} )}
{form.authMethod === 'rsaKey' && (
<FormControl error={!form.rsaKey}> {form.authMethod === 'rsaKey' && form.storePassword && (
<FormControl
error={!form.rsaKey && !hostConfig?.rsaKey}
sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center' }}
>
<FormLabel>RSA Key</FormLabel> <FormLabel>RSA Key</FormLabel>
<Input <Input
type="file" type="file"
onChange={handleFileChange} onChange={handleFileChange}
required
sx={{ sx={{
backgroundColor: theme.palette.general.primary, backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary, color: theme.palette.text.primary,
padding: 1, alignItems: 'center'
textAlign: 'center',
width: '100%',
minWidth: 'auto',
minHeight: 'auto',
}} }}
/> />
{hostConfig?.rsaKey && !form.rsaKey && (
<FormLabel sx={{ color: theme.palette.text.secondary }}>
Existing key detected. Upload to replace.
</FormLabel>
)}
</FormControl> </FormControl>
)} )}
<FormControl error={form.port < 1 || form.port > 65535}> <FormControl error={form.port < 1 || form.port > 65535}>
<FormLabel>Host Port</FormLabel> <FormLabel>Host Port</FormLabel>
<Input <Input
value={form.port} value={form.port}
onChange={(e) => setForm({ ...form, port: e.target.value })} onChange={(e) => setForm((prev) => ({ ...prev, port: e.target.value }))}
min={1}
max={65535}
required
sx={{ sx={{
backgroundColor: theme.palette.general.primary, backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary, color: theme.palette.text.primary
}} }}
/> />
</FormControl> </FormControl>
<FormControl>
<FormLabel>Store Password</FormLabel>
<Checkbox
checked={form.storePassword}
onChange={(e) => handleStorePasswordChange(e.target.checked)}
sx={{
color: theme.palette.text.primary,
'&.Mui-checked': {
color: theme.palette.text.primary
}
}}
/>
</FormControl>
<Button <Button
type="submit" type="submit"
disabled={!isFormValid()} disabled={!isFormValid()}
sx={{ sx={{
backgroundColor: theme.palette.general.primary, backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
'&:hover': { '&:hover': {
backgroundColor: theme.palette.general.disabled, backgroundColor: theme.palette.general.disabled
}, }
}} }}
> >
Edit Host Save Changes
</Button> </Button>
</Stack> </Stack>
</form> </form>
@@ -216,20 +287,11 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
EditHostModal.propTypes = { EditHostModal.propTypes = {
isHidden: PropTypes.bool.isRequired, isHidden: PropTypes.bool.isRequired,
form: PropTypes.shape({ form: PropTypes.object.isRequired,
name: PropTypes.string,
ip: PropTypes.string.isRequired,
user: PropTypes.string.isRequired,
password: PropTypes.string,
rsaKey: PropTypes.string,
port: PropTypes.number.isRequired,
authMethod: PropTypes.string.isRequired,
rememberHost: PropTypes.bool,
}).isRequired,
setForm: PropTypes.func.isRequired, setForm: PropTypes.func.isRequired,
handleEditHost: PropTypes.func.isRequired, handleEditHost: PropTypes.func.isRequired,
setIsEditHostHidden: PropTypes.func.isRequired, setIsEditHostHidden: PropTypes.func.isRequired,
hostConfig: PropTypes.object, hostConfig: PropTypes.object
}; };
export default EditHostModal; export default EditHostModal;

View File

@@ -1,10 +1,14 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { CssVarsProvider } from '@mui/joy/styles'; import { CssVarsProvider } from '@mui/joy/styles';
import { Modal, Button, FormControl, FormLabel, Input, Stack, DialogTitle, DialogContent, ModalDialog } from '@mui/joy'; import { Modal, Button, FormControl, FormLabel, Input, Stack, DialogTitle, DialogContent, ModalDialog, IconButton } from '@mui/joy';
import theme from '/src/theme'; import theme from '/src/theme';
import {useEffect} from 'react'; import { useEffect, useState } from 'react';
import Visibility from '@mui/icons-material/Visibility';
import VisibilityOff from '@mui/icons-material/VisibilityOff';
const LoginUserModal = ({ isHidden, form, setForm, handleLoginUser, handleGuestLogin, setIsLoginUserHidden, setIsCreateUserHidden }) => {
const [showPassword, setShowPassword] = useState(false);
const LoginUserModal = ({ isHidden, form, setForm, handleLoginUser, setIsLoginUserHidden, setIsCreateUserHidden }) => {
const isFormValid = () => { const isFormValid = () => {
if (!form.username || !form.password) return false; if (!form.username || !form.password) return false;
return true; return true;
@@ -64,15 +68,27 @@ const LoginUserModal = ({ isHidden, form, setForm, handleLoginUser, setIsLoginUs
</FormControl> </FormControl>
<FormControl> <FormControl>
<FormLabel>Password</FormLabel> <FormLabel>Password</FormLabel>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Input <Input
type="password" type={showPassword ? 'text' : 'password'}
value={form.password} value={form.password}
onChange={(event) => setForm({ ...form, password: event.target.value })} onChange={(event) => setForm({ ...form, password: event.target.value })}
sx={{ sx={{
backgroundColor: theme.palette.general.primary, backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary, color: theme.palette.text.primary,
flex: 1,
}} }}
/> />
<IconButton
onClick={() => setShowPassword(!showPassword)}
sx={{
color: theme.palette.text.primary,
marginLeft: 1,
}}
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</div>
</FormControl> </FormControl>
<Button <Button
type="submit" type="submit"
@@ -101,6 +117,17 @@ const LoginUserModal = ({ isHidden, form, setForm, handleLoginUser, setIsLoginUs
> >
Create User Create User
</Button> </Button>
<Button
onClick={handleGuestLogin}
sx={{
backgroundColor: theme.palette.general.primary,
'&:hover': {
backgroundColor: theme.palette.general.disabled,
},
}}
>
Login as Guest
</Button>
</Stack> </Stack>
</form> </form>
</DialogContent> </DialogContent>
@@ -115,6 +142,7 @@ LoginUserModal.propTypes = {
form: PropTypes.object.isRequired, form: PropTypes.object.isRequired,
setForm: PropTypes.func.isRequired, setForm: PropTypes.func.isRequired,
handleLoginUser: PropTypes.func.isRequired, handleLoginUser: PropTypes.func.isRequired,
handleGuestLogin: PropTypes.func.isRequired,
setIsLoginUserHidden: PropTypes.func.isRequired, setIsLoginUserHidden: PropTypes.func.isRequired,
setIsCreateUserHidden: PropTypes.func.isRequired, setIsCreateUserHidden: PropTypes.func.isRequired,
}; };

View File

@@ -0,0 +1,176 @@
import PropTypes from 'prop-types';
import { CssVarsProvider } from '@mui/joy/styles';
import {
Modal,
Button,
FormControl,
FormLabel,
Input,
Stack,
DialogTitle,
DialogContent,
ModalDialog,
IconButton,
Select,
Option,
} from '@mui/joy';
import theme from '/src/theme';
import { useState } from 'react';
import Visibility from '@mui/icons-material/Visibility';
import VisibilityOff from '@mui/icons-material/VisibilityOff';
const NoAuthenticationModal = ({ isHidden, form, setForm, setIsNoAuthHidden, handleAuthSubmit }) => {
const [showPassword, setShowPassword] = useState(false);
const isFormValid = () => {
if (form.authMethod === 'Select Auth') return false;
if (form.authMethod === 'rsaKey' && !form.rsaKey) return false;
if (form.authMethod === 'password' && !form.password) return false;
return true;
};
const handleSubmit = (event) => {
event.preventDefault();
if (isFormValid()) {
handleAuthSubmit(form);
setForm({ authMethod: 'Select Auth', password: '', rsaKey: '' });
}
};
return (
<CssVarsProvider theme={theme}>
<Modal
open={!isHidden}
onClose={(e, reason) => {
if (reason !== 'backdropClick') {
setIsNoAuthHidden(true);
}
}}
disableBackdropClic
>
<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>Authentication Required</DialogTitle>
<DialogContent>
<form onSubmit={handleSubmit}>
<Stack spacing={2} sx={{ width: "100%", maxWidth: "100%", overflow: "hidden" }}>
<FormControl error={!form.authMethod || form.authMethod === 'Select Auth'}>
<FormLabel sx={{ color: theme.palette.text.primary }}>Authentication Method</FormLabel>
<Select
value={form.authMethod || 'Select Auth'}
onChange={(e, newValue) => setForm({ ...form, authMethod: newValue })}
required
sx={{
backgroundColor: !form.authMethod || form.authMethod === 'Select Auth' ? theme.palette.general.tertiary : theme.palette.general.primary,
color: theme.palette.text.primary,
'&:hover': {
backgroundColor: theme.palette.general.disabled,
},
}}
>
<Option value="Select Auth" disabled>Select Auth</Option>
<Option value="password">Password</Option>
<Option value="rsaKey">RSA Key</Option>
</Select>
</FormControl>
{form.authMethod === 'password' && (
<FormControl error={!form.password}>
<FormLabel sx={{ color: theme.palette.text.primary }}>Password</FormLabel>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Input
type={showPassword ? 'text' : 'password'}
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
required
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
flex: 1,
}}
/>
<IconButton
onClick={() => setShowPassword(!showPassword)}
sx={{
color: theme.palette.text.primary,
marginLeft: 1,
}}
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</div>
</FormControl>
)}
{form.authMethod === 'rsaKey' && (
<FormControl error={!form.rsaKey}>
<FormLabel sx={{ color: theme.palette.text.primary }}>RSA Key</FormLabel>
<Input
type="file"
onChange={(e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
setForm({ ...form, rsaKey: event.target.result });
};
reader.readAsText(file);
}
}}
required
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
padding: 1,
textAlign: 'center',
width: '100%',
minWidth: 'auto',
minHeight: 'auto',
}}
/>
</FormControl>
)}
<Button
type="submit"
disabled={!isFormValid()}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
'&:hover': {
backgroundColor: theme.palette.general.disabled,
},
}}
>
Connect
</Button>
</Stack>
</form>
</DialogContent>
</ModalDialog>
</Modal>
</CssVarsProvider>
);
};
NoAuthenticationModal.propTypes = {
isHidden: PropTypes.bool.isRequired,
form: PropTypes.object.isRequired,
setForm: PropTypes.func.isRequired,
setIsNoAuthHidden: PropTypes.func.isRequired,
handleAuthSubmit: PropTypes.func.isRequired,
};
export default NoAuthenticationModal;

View File

@@ -56,10 +56,12 @@ const ProfileModal = ({ isHidden, getUser, handleDeleteUser, handleLogoutUser, s
borderRadius: 10, borderRadius: 10,
width: "100%", width: "100%",
textAlign: "center", textAlign: "center",
display: "flex",
justifyContent: "center",
alignItems: "center",
}} }}
> >
Username: <br /> User: {getUserName()}
{getUserName()}
</DialogTitle> </DialogTitle>
<DialogContent sx={{ width: "100%" }}> <DialogContent sx={{ width: "100%" }}>
<Stack spacing={2} sx={{ width: "100%", maxWidth: "100%", overflow: "hidden", mt: 1.5 }}> <Stack spacing={2} sx={{ width: "100%", maxWidth: "100%", overflow: "hidden", mt: 1.5 }}>