Better encryption for everything, new session login, rewrote a lot of database code changing its storage methods. Prepared for release of 2.0.
This commit is contained in:
14
src/App.jsx
14
src/App.jsx
@@ -181,7 +181,7 @@ function App() {
|
||||
|
||||
const handleSaveHost = () => {
|
||||
let hostConfig = {
|
||||
name: addHostForm.name,
|
||||
name: addHostForm.name || addHostForm.ip,
|
||||
ip: addHostForm.ip,
|
||||
user: addHostForm.user,
|
||||
password: addHostForm.authMethod === 'password' ? addHostForm.password : undefined,
|
||||
@@ -273,10 +273,10 @@ function App() {
|
||||
const deleteHost = (hostConfig) => {
|
||||
if (userRef.current) {
|
||||
userRef.current.deleteHost({
|
||||
hostConfig,
|
||||
hostId: hostConfig._id,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const updateEditHostForm = (hostConfig) => {
|
||||
if (hostConfig) {
|
||||
@@ -289,18 +289,16 @@ function App() {
|
||||
|
||||
const handleEditHost = () => {
|
||||
if (editHostForm.ip && editHostForm.user && ((editHostForm.authMethod === 'password' && editHostForm.password) || (editHostForm.authMethod === 'rsaKey' && editHostForm.rsaKey)) && editHostForm.port && editHostForm.authMethod !== 'Select Auth') {
|
||||
const user = getUser();
|
||||
editHostForm.rememberHost = true;
|
||||
|
||||
if (user && currentHostConfig) {
|
||||
userRef.current.editExistingHost({
|
||||
userId: user.id,
|
||||
if (currentHostConfig) {
|
||||
userRef.current.editHost({
|
||||
oldHostConfig: currentHostConfig,
|
||||
newHostConfig: editHostForm,
|
||||
});
|
||||
setIsEditHostHidden(true);
|
||||
} else {
|
||||
console.error("User or currentHostConfig is null");
|
||||
alert("Host not found");
|
||||
}
|
||||
} else {
|
||||
alert("Please fill out all fields.");
|
||||
|
||||
@@ -27,13 +27,12 @@ function Launchpad({
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
// Close the launchpad when neither form is visible and no error is showing
|
||||
if (
|
||||
launchpadRef.current &&
|
||||
!launchpadRef.current.contains(event.target) &&
|
||||
isAddHostHidden && // Only close if addHost form is hidden
|
||||
isEditHostHidden && // Only close if editHost form is hidden
|
||||
isErrorHidden // Only close if error is hidden
|
||||
isAddHostHidden &&
|
||||
isEditHostHidden &&
|
||||
isErrorHidden
|
||||
) {
|
||||
onClose();
|
||||
}
|
||||
@@ -47,8 +46,8 @@ function Launchpad({
|
||||
}, [onClose, isAddHostHidden, isEditHostHidden, isErrorHidden]);
|
||||
|
||||
const handleEditHostClick = () => {
|
||||
setIsAddHostHidden(false); // Open the form for editing
|
||||
setActiveApp('hostViewer'); // Set active app to HostViewer
|
||||
setIsAddHostHidden(false);
|
||||
setActiveApp('hostViewer');
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -174,10 +173,10 @@ function Launchpad({
|
||||
connectToHost={connectToHost}
|
||||
setIsAddHostHidden={setIsAddHostHidden}
|
||||
deleteHost={deleteHost}
|
||||
editHost={editHost} // Pass editHost here
|
||||
editHost={editHost}
|
||||
createFolder={createFolder}
|
||||
moveHostToFolder={moveHostToFolder}
|
||||
onEditHostClick={handleEditHostClick} // Pass the handler to the form
|
||||
onEditHostClick={handleEditHostClick}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -63,10 +63,10 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible }, ref) => {
|
||||
|
||||
const socket = io(
|
||||
window.location.hostname === "localhost"
|
||||
? "http://localhost:8081" // Modified path here
|
||||
? "http://localhost:8081"
|
||||
: "/",
|
||||
{
|
||||
path: "/ssh.io/socket.io", // Same path, no need to modify
|
||||
path: "/ssh.io/socket.io",
|
||||
transports: ["websocket", "polling"],
|
||||
}
|
||||
);
|
||||
|
||||
363
src/User.jsx
363
src/User.jsx
@@ -1,219 +1,237 @@
|
||||
import { useRef, forwardRef, useImperativeHandle } from "react";
|
||||
import { useRef, forwardRef, useImperativeHandle, useEffect } from "react";
|
||||
import io from "socket.io-client";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
let socket;
|
||||
const SOCKET_URL = window.location.hostname === "localhost"
|
||||
? "http://localhost:8082/database.io"
|
||||
: "/database.io";
|
||||
|
||||
if (!socket) {
|
||||
socket = io(
|
||||
window.location.hostname === "localhost"
|
||||
? "http://localhost:8082/database.io"
|
||||
: "/database.io",
|
||||
{
|
||||
path: "/database.io/socket.io",
|
||||
transports: ["websocket", "polling"],
|
||||
}
|
||||
);
|
||||
}
|
||||
const socket = io(SOCKET_URL, {
|
||||
path: "/database.io/socket.io",
|
||||
transports: ["websocket", "polling"],
|
||||
autoConnect: false,
|
||||
});
|
||||
|
||||
export const User = forwardRef(({ onLoginSuccess, onCreateSuccess, onDeleteSuccess, onFailure }, ref) => {
|
||||
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,
|
||||
useEffect(() => {
|
||||
socketRef.current.connect();
|
||||
return () => socketRef.current.disconnect();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const verifySession = async () => {
|
||||
const storedSession = localStorage.getItem("sessionToken");
|
||||
if (!storedSession || storedSession === "undefined") return;
|
||||
|
||||
try {
|
||||
const response = await new Promise((resolve) => {
|
||||
socketRef.current.emit("verifySession", { sessionToken: storedSession }, resolve);
|
||||
});
|
||||
|
||||
if (response?.success) {
|
||||
currentUser.current = {
|
||||
id: response.user.id,
|
||||
username: response.user.username,
|
||||
sessionToken: storedSession,
|
||||
};
|
||||
onLoginSuccess(response.user);
|
||||
} else {
|
||||
localStorage.removeItem("sessionToken");
|
||||
onFailure("Session expired");
|
||||
}
|
||||
} catch (error) {
|
||||
onFailure(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
verifySession();
|
||||
}, []);
|
||||
|
||||
const createUser = async (userConfig) => {
|
||||
try {
|
||||
const response = await new Promise((resolve) => {
|
||||
socketRef.current.emit("createUser", userConfig, resolve);
|
||||
});
|
||||
|
||||
socketRef.current.once("userCreated", (data) => {
|
||||
if (response?.user?.sessionToken) {
|
||||
currentUser.current = {
|
||||
id: data.user._id,
|
||||
username: data.user.username,
|
||||
sessionToken: data.user.sessionToken,
|
||||
id: response.user.id,
|
||||
username: response.user.username,
|
||||
sessionToken: response.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);
|
||||
});
|
||||
localStorage.setItem("sessionToken", response.user.sessionToken);
|
||||
onCreateSuccess(response.user);
|
||||
} else {
|
||||
throw new Error(response?.error || "User creation failed");
|
||||
}
|
||||
} catch (error) {
|
||||
onFailure(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const loginUser = (userConfig) => {
|
||||
if (socketRef.current) {
|
||||
setTimeout(() => {
|
||||
socketRef.current.emit("loginUser", {
|
||||
username: userConfig.username,
|
||||
password: userConfig.password,
|
||||
sessionToken: userConfig.sessionToken,
|
||||
});
|
||||
const loginUser = async ({ username, password, sessionToken }) => {
|
||||
try {
|
||||
const response = await new Promise((resolve) => {
|
||||
const credentials = sessionToken ? { sessionToken } : { username, password };
|
||||
socketRef.current.emit("loginUser", credentials, resolve);
|
||||
});
|
||||
|
||||
socketRef.current.once("userFound", (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);
|
||||
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 || "Login failed");
|
||||
}
|
||||
} catch (error) {
|
||||
onFailure(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const logoutUser = () => {
|
||||
localStorage.removeItem('sessionToken');
|
||||
localStorage.removeItem("sessionToken");
|
||||
currentUser.current = null;
|
||||
onLoginSuccess(null);
|
||||
};
|
||||
|
||||
const deleteUser = () => {
|
||||
if (currentUser.current?.id && socketRef.current) {
|
||||
socketRef.current.emit("deleteUser", {
|
||||
userId: currentUser.current.id,
|
||||
const deleteUser = async () => {
|
||||
if (!currentUser.current) return onFailure("No user logged in");
|
||||
|
||||
try {
|
||||
const response = await new Promise((resolve) => {
|
||||
socketRef.current.emit("deleteUser", {
|
||||
userId: currentUser.current.id,
|
||||
sessionToken: currentUser.current.sessionToken,
|
||||
}, resolve);
|
||||
});
|
||||
|
||||
socketRef.current.once("userDeleted", (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.");
|
||||
if (response?.success) {
|
||||
logoutUser();
|
||||
onDeleteSuccess(response);
|
||||
} else {
|
||||
throw new Error(response?.error || "User deletion failed");
|
||||
}
|
||||
} catch (error) {
|
||||
onFailure(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const saveHost = (hostConfig) => {
|
||||
if (currentUser.current?.id && socketRef.current) {
|
||||
socketRef.current.emit("saveHostConfig", {
|
||||
userId: currentUser.current.id,
|
||||
hostConfig: hostConfig,
|
||||
const saveHost = async (hostConfig) => {
|
||||
if (!currentUser.current) return onFailure("Not authenticated");
|
||||
|
||||
try {
|
||||
const response = await new Promise((resolve) => {
|
||||
socketRef.current.emit("saveHostConfig", {
|
||||
userId: currentUser.current.id,
|
||||
sessionToken: currentUser.current.sessionToken,
|
||||
...hostConfig
|
||||
}, resolve);
|
||||
});
|
||||
|
||||
socketRef.current.once("error", (error) => {
|
||||
onFailure(error);
|
||||
});
|
||||
} else {
|
||||
onFailure("No user is currently logged in.");
|
||||
if (!response?.success) {
|
||||
throw new Error(response?.error || "Failed to save host");
|
||||
}
|
||||
} catch (error) {
|
||||
onFailure(error.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getUser = () => {
|
||||
return currentUser.current;
|
||||
}
|
||||
const getAllHosts = async () => {
|
||||
if (!currentUser.current) return [];
|
||||
|
||||
const getAllHosts = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (currentUser.current?.id && socketRef.current) {
|
||||
try {
|
||||
const response = await new Promise((resolve) => {
|
||||
socketRef.current.emit("getHosts", {
|
||||
userId: currentUser.current.id,
|
||||
});
|
||||
sessionToken: currentUser.current.sessionToken,
|
||||
}, resolve);
|
||||
});
|
||||
|
||||
socketRef.current.once("hostsFound", (data) => {
|
||||
if (data && Array.isArray(data)) {
|
||||
resolve(data);
|
||||
} else {
|
||||
reject("Invalid data received.");
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
reject(errorMsg);
|
||||
});
|
||||
if (response?.success) {
|
||||
return response.hosts;
|
||||
} else {
|
||||
reject("No user is currently logged in.");
|
||||
throw new Error(response?.error || "Failed to fetch hosts");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const deleteHost = (hostConfig) => {
|
||||
if (currentUser.current?.id && socketRef.current) {
|
||||
socketRef.current.emit("deleteHost", {
|
||||
userId: currentUser.current.id,
|
||||
hostConfig: hostConfig,
|
||||
});
|
||||
|
||||
socketRef.current.once("error", (error) => {
|
||||
onFailure(error);
|
||||
});
|
||||
} else {
|
||||
onFailure("No user is currently logged in.");
|
||||
}
|
||||
}
|
||||
|
||||
const editExistingHost = ({ userId, oldHostConfig, newHostConfig }) => {
|
||||
if (currentUser.current?.id && socketRef.current) {
|
||||
socketRef.current.emit("editHost", {
|
||||
userId: userId,
|
||||
oldHostConfig: oldHostConfig,
|
||||
newHostConfig: newHostConfig,
|
||||
});
|
||||
|
||||
socketRef.current.once("error", (error) => {
|
||||
onFailure(error);
|
||||
});
|
||||
} else {
|
||||
onFailure("No user is currently logged in.");
|
||||
} catch (error) {
|
||||
onFailure(error.message);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const createFolder = (folderName) => {
|
||||
if (currentUser.current?.id && socketRef.current) {
|
||||
socketRef.current.emit("createFolder", {
|
||||
userId: currentUser.current.id,
|
||||
folderName: folderName,
|
||||
const deleteHost = async ({ hostId }) => {
|
||||
if (!currentUser.current) return onFailure("Not authenticated");
|
||||
|
||||
try {
|
||||
const response = await new Promise((resolve) => {
|
||||
socketRef.current.emit("deleteHost", {
|
||||
userId: currentUser.current.id,
|
||||
sessionToken: currentUser.current.sessionToken,
|
||||
hostId: hostId,
|
||||
}, resolve);
|
||||
});
|
||||
|
||||
socketRef.current.once("error", (error) => {
|
||||
onFailure(error);
|
||||
});
|
||||
} else {
|
||||
onFailure("No user is currently logged in.");
|
||||
if (!response?.success) {
|
||||
throw new Error(response?.error || "Failed to delete host");
|
||||
}
|
||||
} catch (error) {
|
||||
onFailure(error.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const moveHostToFolder = (folderName, hostConfig) => {
|
||||
if (currentUser.current?.id && socketRef.current) {
|
||||
socketRef.current.emit("moveHostToFolder", {
|
||||
userId: currentUser.current.id,
|
||||
folderName: folderName,
|
||||
hostConfig: hostConfig,
|
||||
const editHost = async ({ oldHostConfig, newHostConfig }) => {
|
||||
if (!currentUser.current) return onFailure("Not authenticated");
|
||||
|
||||
try {
|
||||
console.log('Editing host with configs:', { oldHostConfig, newHostConfig });
|
||||
const response = await new Promise((resolve) => {
|
||||
socketRef.current.emit("editHost", {
|
||||
userId: currentUser.current.id,
|
||||
sessionToken: currentUser.current.sessionToken,
|
||||
oldHostConfig,
|
||||
newHostConfig,
|
||||
}, resolve);
|
||||
});
|
||||
|
||||
socketRef.current.once("error", (error) => {
|
||||
onFailure(error);
|
||||
});
|
||||
} else {
|
||||
onFailure("No user is currently logged in.");
|
||||
if (!response?.success) {
|
||||
throw new Error(response?.error || "Failed to edit host");
|
||||
}
|
||||
} catch (error) {
|
||||
onFailure(error.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const shareHost = async (hostId, targetUsername) => {
|
||||
if (!currentUser.current) return onFailure("Not authenticated");
|
||||
|
||||
try {
|
||||
const response = await new Promise((resolve) => {
|
||||
socketRef.current.emit("shareHost", {
|
||||
userId: currentUser.current.id,
|
||||
sessionToken: currentUser.current.sessionToken,
|
||||
hostId,
|
||||
targetUsername,
|
||||
}, resolve);
|
||||
});
|
||||
|
||||
if (!response?.success) {
|
||||
throw new Error(response?.error || "Failed to share host");
|
||||
}
|
||||
} catch (error) {
|
||||
onFailure(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
createUser,
|
||||
@@ -221,15 +239,14 @@ export const User = forwardRef(({ onLoginSuccess, onCreateSuccess, onDeleteSucce
|
||||
logoutUser,
|
||||
deleteUser,
|
||||
saveHost,
|
||||
getUser,
|
||||
getAllHosts,
|
||||
deleteHost,
|
||||
editExistingHost,
|
||||
createFolder,
|
||||
moveHostToFolder,
|
||||
shareHost,
|
||||
editHost,
|
||||
getUser: () => currentUser.current,
|
||||
}));
|
||||
|
||||
return <div></div>;
|
||||
return null;
|
||||
});
|
||||
|
||||
User.displayName = "User";
|
||||
|
||||
@@ -4,46 +4,38 @@ import { Button } from "@mui/joy";
|
||||
|
||||
function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, editHost }) {
|
||||
const [hosts, setHosts] = useState([]);
|
||||
const [initialLoadComplete, setInitialLoadComplete] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const isMounted = useRef(true);
|
||||
|
||||
const fetchHosts = async () => {
|
||||
try {
|
||||
const savedHosts = await getHosts();
|
||||
if (isMounted.current) {
|
||||
setHosts(savedHosts || []);
|
||||
setIsLoading(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Host fetch failed:", error);
|
||||
if (isMounted.current) {
|
||||
setHosts([]);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
isMounted.current = true;
|
||||
fetchHosts();
|
||||
|
||||
async function fetchInitialHosts() {
|
||||
try {
|
||||
const savedHosts = await getHosts();
|
||||
if (isMounted.current) {
|
||||
setHosts(savedHosts || []);
|
||||
setInitialLoadComplete(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Initial host fetch failed:", error);
|
||||
if (isMounted.current) {
|
||||
setHosts([]);
|
||||
setInitialLoadComplete(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fetchInitialHosts();
|
||||
|
||||
const intervalId = setInterval(async () => {
|
||||
try {
|
||||
const savedHosts = await getHosts();
|
||||
if (isMounted.current) {
|
||||
setHosts(savedHosts || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Periodic host update failed:", error);
|
||||
}
|
||||
const intervalId = setInterval(() => {
|
||||
fetchHosts();
|
||||
}, 2000);
|
||||
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, [getHosts]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full p-4 text-white flex flex-col">
|
||||
@@ -61,25 +53,29 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-grow overflow-auto">
|
||||
{hosts.length > 0 ? (
|
||||
{isLoading ? (
|
||||
<p className="text-gray-300">Loading hosts...</p>
|
||||
) : hosts.length > 0 ? (
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
{hosts.map((hostWrapper, index) => {
|
||||
const hostConfig = hostWrapper.hostConfig || {};
|
||||
const hostConfig = hostWrapper.config || {};
|
||||
|
||||
if (!hostConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={index} className="flex justify-between items-center bg-neutral-800 p-3 rounded-lg shadow-md border border-neutral-700 w-full">
|
||||
<div>
|
||||
<p className="font-semibold">{hostConfig.name || hostConfig.ip}</p>
|
||||
<p className="text-sm text-gray-400">
|
||||
{hostConfig.user ? `${hostConfig.user}@${hostConfig.ip}` : hostConfig.ip}:{hostConfig.port}
|
||||
{hostConfig.user ? `${hostConfig.user}@${hostConfig.ip}` : `${hostConfig.ip}:${hostConfig.port}`}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
className="text-black"
|
||||
onClick={() => {
|
||||
connectToHost(hostConfig);
|
||||
}}
|
||||
onClick={() => connectToHost(hostConfig)}
|
||||
sx={{
|
||||
backgroundColor: "#6e6e6e",
|
||||
"&:hover": { backgroundColor: "#0f0f0f" }
|
||||
@@ -90,7 +86,7 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
|
||||
<Button
|
||||
className="text-black"
|
||||
onClick={() => {
|
||||
deleteHost(hostConfig);
|
||||
deleteHost({ ...hostConfig, _id: hostWrapper._id });
|
||||
}}
|
||||
sx={{
|
||||
backgroundColor: "#6e6e6e",
|
||||
@@ -117,7 +113,7 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-300">Hosts are either loading or do not exist...</p>
|
||||
<p className="text-gray-300">No hosts available...</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,341 +1,385 @@
|
||||
const http = require("http");
|
||||
const socketIo = require("socket.io");
|
||||
const mongoose = require("mongoose");
|
||||
const http = require('http');
|
||||
const socketIo = require('socket.io');
|
||||
const mongoose = require('mongoose');
|
||||
const bcrypt = require('bcrypt');
|
||||
const crypto = require('crypto');
|
||||
require('dotenv').config();
|
||||
|
||||
const logger = {
|
||||
info: (...args) => console.log(`🔧 [${new Date().toISOString()}] INFO:`, ...args),
|
||||
error: (...args) => console.error(`❌ [${new Date().toISOString()}] ERROR:`, ...args),
|
||||
warn: (...args) => console.warn(`⚠️ [${new Date().toISOString()}] WARN:`, ...args),
|
||||
debug: (...args) => console.debug(`🔍 [${new Date().toISOString()}] DEBUG:`, ...args)
|
||||
};
|
||||
|
||||
const server = http.createServer();
|
||||
const io = socketIo(server, {
|
||||
path: "/database.io/socket.io",
|
||||
cors: {
|
||||
origin: "*",
|
||||
methods: ["GET", "POST"],
|
||||
credentials: true
|
||||
},
|
||||
allowEIO3: true
|
||||
path: '/database.io/socket.io',
|
||||
cors: { origin: '*', methods: ['GET', 'POST'] }
|
||||
});
|
||||
|
||||
const dbNamespace = io.of("/database.io");
|
||||
|
||||
async function connectToMongoDB() {
|
||||
try {
|
||||
const mongoUrl = process.env.MONGO_URL || 'mongodb://mongodb: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: [] },
|
||||
sessionToken: { type: String, required: true }
|
||||
});
|
||||
|
||||
const hostSchema = new mongoose.Schema({
|
||||
name: { type: String, required: true },
|
||||
config: { type: String, required: true },
|
||||
users: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
|
||||
createdBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }
|
||||
});
|
||||
|
||||
const User = mongoose.model('User', userSchema);
|
||||
const Host = mongoose.model('Host', hostSchema);
|
||||
|
||||
async function createUser(username, password) {
|
||||
const getEncryptionKey = (userId, sessionToken) => {
|
||||
return crypto.scryptSync(`${userId}-${sessionToken}`, 'salt', 32);
|
||||
};
|
||||
|
||||
const encryptData = (data, userId, sessionToken) => {
|
||||
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 };
|
||||
const iv = crypto.randomBytes(16);
|
||||
const cipher = crypto.createCipheriv('aes-256-gcm', getEncryptionKey(userId, sessionToken), iv);
|
||||
const encrypted = Buffer.concat([cipher.update(JSON.stringify(data)), cipher.final()]);
|
||||
return `${iv.toString('hex')}:${encrypted.toString('hex')}:${cipher.getAuthTag().toString('hex')}`;
|
||||
} catch (error) {
|
||||
logger.error('Encryption failed:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async function loginUser(username, password) {
|
||||
const decryptData = (encryptedData, userId, sessionToken) => {
|
||||
try {
|
||||
const user = await User.findOne({ username, password });
|
||||
if (user) {
|
||||
if (!user.sessionToken) {
|
||||
user.sessionToken = crypto.randomBytes(64).toString('hex');
|
||||
await user.save();
|
||||
const [ivHex, contentHex, authTagHex] = encryptedData.split(':');
|
||||
const iv = Buffer.from(ivHex, 'hex');
|
||||
const content = Buffer.from(contentHex, 'hex');
|
||||
const authTag = Buffer.from(authTagHex, 'hex');
|
||||
|
||||
const decipher = crypto.createDecipheriv('aes-256-gcm', getEncryptionKey(userId, sessionToken), iv);
|
||||
decipher.setAuthTag(authTag);
|
||||
|
||||
return JSON.parse(Buffer.concat([decipher.update(content), decipher.final()]).toString());
|
||||
} catch (error) {
|
||||
logger.error('Decryption failed:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
mongoose.connect(process.env.MONGO_URL || 'mongodb://localhost:27017/termix')
|
||||
.then(() => logger.info('Connected to MongoDB'))
|
||||
.catch(err => logger.error('MongoDB connection error:', err));
|
||||
|
||||
io.of('/database.io').on('connection', (socket) => {
|
||||
socket.on('createUser', async ({ username, password }, callback) => {
|
||||
try {
|
||||
logger.debug(`Creating user: ${username}`);
|
||||
|
||||
if (await User.exists({ username })) {
|
||||
logger.warn(`Username already exists: ${username}`);
|
||||
return callback({ error: 'Username already exists' });
|
||||
}
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
||||
async function saveHostConfig(userId, hostConfig) {
|
||||
try {
|
||||
const user = await User.findById(userId);
|
||||
if (user) {
|
||||
user.sshConnections.push(hostConfig);
|
||||
await user.save();
|
||||
return { success: true };
|
||||
} else {
|
||||
return { error: 'User not found' };
|
||||
}
|
||||
} catch (err) {
|
||||
return { error: 'Error saving host config: ' + err.message };
|
||||
}
|
||||
}
|
||||
|
||||
async function getHosts(userId) {
|
||||
try {
|
||||
const user = await User.findById(userId);
|
||||
if (user) {
|
||||
return user.sshConnections;
|
||||
} else {
|
||||
return { error: 'User not found' };
|
||||
}
|
||||
} catch (err) {
|
||||
return { error: 'Error getting hosts: ' + err.message };
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteHost(userId, hostConfig) {
|
||||
try {
|
||||
const user = await User.findById(userId);
|
||||
if (user) {
|
||||
user.sshConnections = user.sshConnections.filter(connection => {
|
||||
const matches =
|
||||
connection.name === hostConfig.name &&
|
||||
connection.ip === hostConfig.ip &&
|
||||
connection.port === hostConfig.port &&
|
||||
connection.user === hostConfig.user;
|
||||
|
||||
return !matches;
|
||||
const sessionToken = crypto.randomBytes(64).toString('hex');
|
||||
const user = await User.create({
|
||||
username,
|
||||
password: await bcrypt.hash(password, 10),
|
||||
sessionToken
|
||||
});
|
||||
|
||||
await user.save();
|
||||
return { success: true };
|
||||
} else {
|
||||
return { error: 'User not found' };
|
||||
logger.info(`User created: ${username}`);
|
||||
callback({ success: true, user: {
|
||||
id: user._id,
|
||||
username: user.username,
|
||||
sessionToken
|
||||
}});
|
||||
} catch (error) {
|
||||
logger.error('User creation error:', error);
|
||||
callback({ error: 'User creation failed' });
|
||||
}
|
||||
} catch (err) {
|
||||
return { error: 'Error deleting host: ' + err.message };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function editHost(userId, oldHostConfig, newHostConfig) {
|
||||
try {
|
||||
const user = await User.findById(userId);
|
||||
if (user) {
|
||||
user.sshConnections = user.sshConnections.map(connection => {
|
||||
const matches =
|
||||
connection.hostConfig.name === oldHostConfig.name &&
|
||||
connection.hostConfig.ip === oldHostConfig.ip &&
|
||||
connection.hostConfig.port === oldHostConfig.port &&
|
||||
connection.hostConfig.user === oldHostConfig.user;
|
||||
|
||||
if (matches) {
|
||||
return { hostConfig: newHostConfig };
|
||||
} else {
|
||||
return connection;
|
||||
}
|
||||
});
|
||||
|
||||
await user.save();
|
||||
return { success: true };
|
||||
} else {
|
||||
return { error: 'User not found' };
|
||||
}
|
||||
} catch (err) {
|
||||
return { error: 'Error editing host: ' + err.message };
|
||||
}
|
||||
}
|
||||
|
||||
async function createFolder(userId, folderName) {
|
||||
try {
|
||||
const user = await User.findById(userId);
|
||||
if (user) {
|
||||
user.sshConnections.push({ folderName, connections: [] });
|
||||
await user.save();
|
||||
return { success: true };
|
||||
} else {
|
||||
return { error: 'User not found' };
|
||||
}
|
||||
} catch (err) {
|
||||
return { error: 'Error creating folder: ' + err.message };
|
||||
}
|
||||
}
|
||||
|
||||
async function moveHostToFolder(userId, hostConfig, folderName) {
|
||||
try {
|
||||
const user = await User.findById(userId);
|
||||
if (user) {
|
||||
const folder = user.sshConnections.find(folder => folder.folderName === folderName);
|
||||
if (folder) {
|
||||
folder.connections.push(hostConfig);
|
||||
await user.save();
|
||||
return { success: true };
|
||||
socket.on('loginUser', async ({ username, password, sessionToken }, callback) => {
|
||||
try {
|
||||
let user;
|
||||
if (sessionToken) {
|
||||
user = await User.findOne({ sessionToken });
|
||||
} else {
|
||||
return { error: 'Folder not found' };
|
||||
user = await User.findOne({ username });
|
||||
if (!user || !(await bcrypt.compare(password, user.password))) {
|
||||
logger.warn(`Invalid credentials for: ${username}`);
|
||||
return callback({ error: 'Invalid credentials' });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return { error: 'User not found' };
|
||||
}
|
||||
} catch (err) {
|
||||
return { error: 'Error moving host to folder: ' + err.message };
|
||||
}
|
||||
}
|
||||
|
||||
dbNamespace.on("connection", (socket) => {
|
||||
console.log("New socket connection established on");
|
||||
if (!user) {
|
||||
logger.warn('Login failed - user not found');
|
||||
return callback({ error: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
socket.on("createUser", async (data) => {
|
||||
const { username, password } = data;
|
||||
if (!username || !password) {
|
||||
socket.emit("error", "Please provide both username and password");
|
||||
return;
|
||||
logger.info(`User logged in: ${user.username}`);
|
||||
callback({ success: true, user: {
|
||||
id: user._id,
|
||||
username: user.username,
|
||||
sessionToken: user.sessionToken
|
||||
}});
|
||||
} catch (error) {
|
||||
logger.error('Login error:', error);
|
||||
callback({ error: 'Login failed' });
|
||||
}
|
||||
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.on('saveHostConfig', async ({ userId, sessionToken, hostConfig }, callback) => {
|
||||
try {
|
||||
if (!userId || !sessionToken) {
|
||||
logger.warn('Missing authentication parameters');
|
||||
return callback({ error: 'Authentication required' });
|
||||
}
|
||||
|
||||
if (!hostConfig || typeof hostConfig !== 'object') {
|
||||
logger.warn('Invalid host config format');
|
||||
return callback({ error: 'Invalid host configuration' });
|
||||
}
|
||||
|
||||
if (!hostConfig.ip || !hostConfig.user) {
|
||||
logger.warn('Missing required fields:', hostConfig);
|
||||
return callback({ error: 'IP and User are required' });
|
||||
}
|
||||
|
||||
const user = await User.findOne({ _id: userId, sessionToken });
|
||||
if (!user) {
|
||||
logger.warn(`Invalid session for user: ${userId}`);
|
||||
return callback({ error: 'Invalid session' });
|
||||
}
|
||||
|
||||
const cleanConfig = {
|
||||
name: hostConfig.name.trim(),
|
||||
ip: hostConfig.ip.trim(),
|
||||
user: hostConfig.user.trim(),
|
||||
port: hostConfig.port || 22,
|
||||
password: hostConfig.password?.trim() || undefined,
|
||||
rsaKey: hostConfig.rsaKey?.trim() || undefined
|
||||
};
|
||||
|
||||
const finalName = cleanConfig.name || cleanConfig.ip;
|
||||
|
||||
const existingHost = await Host.findOne({
|
||||
name: finalName,
|
||||
createdBy: userId
|
||||
});
|
||||
|
||||
if (existingHost) {
|
||||
logger.warn(`Host with name ${finalName} already exists for user: ${userId}`);
|
||||
return callback({ error: 'Host with this name already exists' });
|
||||
}
|
||||
|
||||
const encryptedConfig = encryptData(cleanConfig, userId, sessionToken);
|
||||
if (!encryptedConfig) {
|
||||
logger.error('Encryption failed for host config');
|
||||
return callback({ error: 'Configuration encryption failed' });
|
||||
}
|
||||
|
||||
await Host.create({
|
||||
name: finalName,
|
||||
config: encryptedConfig,
|
||||
users: [userId],
|
||||
createdBy: userId
|
||||
});
|
||||
|
||||
logger.info(`Host created successfully: ${finalName}`);
|
||||
callback({ success: true });
|
||||
} catch (error) {
|
||||
logger.error('Host save error:', error);
|
||||
callback({ error: `Host save failed: ${error.message}` });
|
||||
}
|
||||
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;
|
||||
socket.on('getHosts', async ({ userId, sessionToken }, callback) => {
|
||||
try {
|
||||
const user = await User.findOne({ _id: userId, sessionToken });
|
||||
if (!user) {
|
||||
logger.warn(`Invalid session for user: ${userId}`);
|
||||
return callback({ error: 'Invalid session' });
|
||||
}
|
||||
|
||||
const hosts = await Host.find({ users: userId });
|
||||
const decryptedHosts = hosts.map(host => ({
|
||||
...host.toObject(),
|
||||
config: decryptData(host.config, userId, sessionToken)
|
||||
})).filter(host => host.config);
|
||||
|
||||
callback({ success: true, hosts: decryptedHosts });
|
||||
} catch (error) {
|
||||
logger.error('Get hosts error:', error);
|
||||
callback({ error: 'Failed to fetch hosts' });
|
||||
}
|
||||
const result = await deleteUser(userId);
|
||||
socket.emit(result.error ? "error" : "userDeleted", result);
|
||||
console.log(result.error || `User deleted`);
|
||||
});
|
||||
|
||||
socket.on("saveHostConfig", async (data) => {
|
||||
const { userId, hostConfig } = data;
|
||||
if (!userId || !hostConfig) {
|
||||
socket.emit("error", "User ID and host config are required");
|
||||
return;
|
||||
socket.on('deleteHost', async ({ userId, sessionToken, hostId }, callback) => {
|
||||
try {
|
||||
logger.debug(`Deleting host: ${hostId} for user: ${userId}`);
|
||||
|
||||
if (!userId || !sessionToken) {
|
||||
logger.warn('Missing authentication parameters');
|
||||
return callback({ error: 'Authentication required' });
|
||||
}
|
||||
|
||||
if (!hostId || typeof hostId !== 'string') {
|
||||
logger.warn('Invalid host ID format');
|
||||
return callback({ error: 'Invalid host ID' });
|
||||
}
|
||||
|
||||
const user = await User.findOne({ _id: userId, sessionToken });
|
||||
if (!user) {
|
||||
logger.warn(`Invalid session for user: ${userId}`);
|
||||
return callback({ error: 'Invalid session' });
|
||||
}
|
||||
|
||||
const result = await Host.deleteOne({ _id: hostId, createdBy: userId });
|
||||
if (result.deletedCount === 0) {
|
||||
logger.warn(`Host not found or not authorized: ${hostId}`);
|
||||
return callback({ error: 'Host not found or not authorized' });
|
||||
}
|
||||
|
||||
logger.info(`Host deleted: ${hostId}`);
|
||||
callback({ success: true });
|
||||
} catch (error) {
|
||||
logger.error('Host deletion error:', error);
|
||||
callback({ error: `Host deletion failed: ${error.message}` });
|
||||
}
|
||||
const result = await saveHostConfig(userId, hostConfig);
|
||||
socket.emit(result.error ? "error" : "hostConfigSaved", result);
|
||||
console.log(result.error || `Host config saved`);
|
||||
});
|
||||
|
||||
socket.on("getHosts", async (data) => {
|
||||
const { userId } = data;
|
||||
if (!userId) {
|
||||
socket.emit("error", "User ID is required");
|
||||
return;
|
||||
socket.on('shareHost', async ({ userId, sessionToken, hostId, targetUsername }, callback) => {
|
||||
try {
|
||||
logger.debug(`Sharing host ${hostId} with ${targetUsername}`);
|
||||
|
||||
const user = await User.findOne({ _id: userId, sessionToken });
|
||||
if (!user) {
|
||||
logger.warn(`Invalid session for user: ${userId}`);
|
||||
return callback({ error: 'Invalid session' });
|
||||
}
|
||||
|
||||
const targetUser = await User.findOne({ username: targetUsername });
|
||||
if (!targetUser) {
|
||||
logger.warn(`Target user not found: ${targetUsername}`);
|
||||
return callback({ error: 'User not found' });
|
||||
}
|
||||
|
||||
const host = await Host.findOne({ _id: hostId, createdBy: userId });
|
||||
if (!host) {
|
||||
logger.warn(`Host not found or unauthorized: ${hostId}`);
|
||||
return callback({ error: 'Host not found' });
|
||||
}
|
||||
|
||||
if (host.users.includes(targetUser._id)) {
|
||||
logger.warn(`Host already shared with user: ${targetUsername}`);
|
||||
return callback({ error: 'Already shared' });
|
||||
}
|
||||
|
||||
host.users.push(targetUser._id);
|
||||
await host.save();
|
||||
|
||||
logger.info(`Host shared successfully: ${hostId} -> ${targetUsername}`);
|
||||
callback({ success: true });
|
||||
} catch (error) {
|
||||
logger.error('Host sharing error:', error);
|
||||
callback({ error: 'Failed to share host' });
|
||||
}
|
||||
const result = await getHosts(userId);
|
||||
socket.emit(result.error ? "error" : "hostsFound", result);
|
||||
console.log(result.error || `Hosts found`);
|
||||
});
|
||||
|
||||
socket.on("deleteHost", async (data) => {
|
||||
const { userId, hostConfig } = data;
|
||||
if (!userId || !hostConfig) {
|
||||
socket.emit("error", "User ID and host config are required");
|
||||
return;
|
||||
socket.on('deleteUser', async ({ userId, sessionToken }, callback) => {
|
||||
try {
|
||||
logger.debug(`Deleting user: ${userId}`);
|
||||
|
||||
const user = await User.findOne({ _id: userId, sessionToken });
|
||||
if (!user) {
|
||||
logger.warn(`Invalid session for user: ${userId}`);
|
||||
return callback({ error: 'Invalid session' });
|
||||
}
|
||||
|
||||
await Host.deleteMany({ createdBy: userId });
|
||||
await User.deleteOne({ _id: userId });
|
||||
|
||||
logger.info(`User deleted: ${userId}`);
|
||||
callback({ success: true });
|
||||
} catch (error) {
|
||||
logger.error('User deletion error:', error);
|
||||
callback({ error: 'Failed to delete user' });
|
||||
}
|
||||
const result = await deleteHost(userId, hostConfig);
|
||||
socket.emit(result.error ? "error" : "hostDeleted", result);
|
||||
console.log(result.error || `Host deleted`);
|
||||
});
|
||||
|
||||
socket.on("editHost", async (data) => {
|
||||
const { userId, oldHostConfig, newHostConfig } = data;
|
||||
if (!userId || !oldHostConfig || !newHostConfig) {
|
||||
socket.emit("error", "User ID, old host config, and new host config are required");
|
||||
return;
|
||||
socket.on("editHost", async ({ userId, sessionToken, oldHostConfig, newHostConfig }, callback) => {
|
||||
try {
|
||||
logger.debug(`Editing host for user: ${userId}`);
|
||||
|
||||
if (!oldHostConfig || !newHostConfig) {
|
||||
logger.warn('Missing host configurations');
|
||||
return callback({ error: 'Missing host configurations' });
|
||||
}
|
||||
|
||||
const user = await User.findOne({ _id: userId, sessionToken });
|
||||
if (!user) {
|
||||
logger.warn(`Invalid session for user: ${userId}`);
|
||||
return callback({ error: 'Invalid session' });
|
||||
}
|
||||
|
||||
const hosts = await Host.find({ createdBy: userId });
|
||||
const host = hosts.find(h => {
|
||||
const decryptedConfig = decryptData(h.config, userId, sessionToken);
|
||||
return decryptedConfig && decryptedConfig.ip === oldHostConfig.ip;
|
||||
});
|
||||
|
||||
if (!host) {
|
||||
logger.warn(`Host not found or unauthorized`);
|
||||
return callback({ error: 'Host not found' });
|
||||
}
|
||||
|
||||
const cleanConfig = {
|
||||
ip: newHostConfig.ip.trim(),
|
||||
user: newHostConfig.user.trim(),
|
||||
port: newHostConfig.port || 22,
|
||||
name: newHostConfig.name.trim(),
|
||||
password: newHostConfig.password?.trim() || undefined,
|
||||
rsaKey: newHostConfig.rsaKey?.trim() || undefined
|
||||
};
|
||||
|
||||
const encryptedConfig = encryptData(cleanConfig, userId, sessionToken);
|
||||
if (!encryptedConfig) {
|
||||
logger.error('Encryption failed for host config');
|
||||
return callback({ error: 'Configuration encryption failed' });
|
||||
}
|
||||
|
||||
host.config = encryptedConfig;
|
||||
await host.save();
|
||||
|
||||
logger.info(`Host edited successfully`);
|
||||
callback({ success: true });
|
||||
} catch (error) {
|
||||
logger.error('Host edit error:', error);
|
||||
callback({ error: 'Failed to edit host' });
|
||||
}
|
||||
const result = await editHost(userId, oldHostConfig, newHostConfig);
|
||||
socket.emit(result.error ? "error" : "hostEdited", result);
|
||||
console.log(result.error || `Host edited`);
|
||||
});
|
||||
|
||||
socket.on("createFolder", async (data) => {
|
||||
const { userId, folderName } = data;
|
||||
if (!userId || !folderName) {
|
||||
socket.emit("error", "User ID and folder name are required");
|
||||
return;
|
||||
}
|
||||
const result = await createFolder(userId, folderName);
|
||||
socket.emit(result.error ? "error" : "folderCreated", result);
|
||||
console.log(result.error || `Folder created`);
|
||||
});
|
||||
socket.on('verifySession', async ({ sessionToken }, callback) => {
|
||||
try {
|
||||
const user = await User.findOne({ sessionToken });
|
||||
if (!user) {
|
||||
logger.warn(`Invalid session token: ${sessionToken}`);
|
||||
return callback({ error: 'Invalid session' });
|
||||
}
|
||||
|
||||
socket.on("moveHostToFolder", async (data) => {
|
||||
const { userId, hostConfig, folderName } = data;
|
||||
if (!userId || !hostConfig || !folderName) {
|
||||
socket.emit("error", "User ID, host config, and folder name are required");
|
||||
return;
|
||||
callback({ success: true, user: {
|
||||
id: user._id,
|
||||
username: user.username
|
||||
}});
|
||||
} catch (error) {
|
||||
logger.error('Session verification error:', error);
|
||||
callback({ error: 'Session verification failed' });
|
||||
}
|
||||
const result = await moveHostToFolder(userId, hostConfig, folderName);
|
||||
socket.emit(result.error ? "error" : "hostMoved", result);
|
||||
console.log(result.error || `Host moved to folder`);
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(8082, '0.0.0.0', async () => {
|
||||
console.log("Server is running on port 8082");
|
||||
await connectToMongoDB();
|
||||
server.listen(8082, () => {
|
||||
logger.info('Server running on port 8082');
|
||||
});
|
||||
@@ -4,27 +4,33 @@ const SSHClient = require("ssh2").Client;
|
||||
|
||||
const server = http.createServer();
|
||||
const io = socketIo(server, {
|
||||
path: "/ssh.io/socket.io", // Corrected path for socket.io
|
||||
path: "/ssh.io/socket.io",
|
||||
cors: {
|
||||
origin: "*", // Temporarily set to '*' to allow all origins. Change to specific URLs if needed.
|
||||
origin: "*",
|
||||
methods: ["GET", "POST"],
|
||||
credentials: true
|
||||
},
|
||||
allowEIO3: true
|
||||
});
|
||||
|
||||
const logger = {
|
||||
info: (...args) => console.log(`🔧 [${new Date().toISOString()}] INFO:`, ...args),
|
||||
error: (...args) => console.error(`❌ [${new Date().toISOString()}] ERROR:`, ...args),
|
||||
warn: (...args) => console.warn(`⚠️ [${new Date().toISOString()}] WARN:`, ...args),
|
||||
debug: (...args) => console.debug(`🔍 [${new Date().toISOString()}] DEBUG:`, ...args)
|
||||
};
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
console.log("New socket connection established");
|
||||
logger.info("New socket connection established");
|
||||
|
||||
let stream = null;
|
||||
|
||||
socket.on("connectToHost", (cols, rows, hostConfig) => {
|
||||
if (!hostConfig || !hostConfig.ip || !hostConfig.user || (!hostConfig.password && !hostConfig.rsaKey) || !hostConfig.port) {
|
||||
console.error("Invalid hostConfig received:", hostConfig);
|
||||
logger.error("Invalid hostConfig received:", hostConfig);
|
||||
return;
|
||||
}
|
||||
|
||||
// Redact only sensitive info for logging
|
||||
const safeHostConfig = {
|
||||
ip: hostConfig.ip,
|
||||
port: hostConfig.port,
|
||||
@@ -33,57 +39,52 @@ io.on("connection", (socket) => {
|
||||
rsaKey: hostConfig.rsaKey ? '***REDACTED***' : undefined,
|
||||
};
|
||||
|
||||
console.log("Received hostConfig:", safeHostConfig);
|
||||
logger.info("Received hostConfig:", safeHostConfig);
|
||||
const { ip, port, user, password, rsaKey } = hostConfig;
|
||||
|
||||
const conn = new SSHClient();
|
||||
conn
|
||||
.on("ready", function () {
|
||||
console.log("SSH connection established");
|
||||
logger.info("SSH connection established");
|
||||
|
||||
conn.shell({ term: "xterm-256color" }, function (err, newStream) {
|
||||
if (err) {
|
||||
console.error("Error:", err.message);
|
||||
logger.error("Error:", err.message);
|
||||
socket.emit("error", err.message);
|
||||
return;
|
||||
}
|
||||
stream = newStream;
|
||||
|
||||
// Set initial terminal size
|
||||
stream.setWindow(rows, cols, rows * 100, cols * 100);
|
||||
|
||||
// Pipe SSH output to client
|
||||
stream.on("data", function (data) {
|
||||
socket.emit("data", data);
|
||||
});
|
||||
|
||||
stream.on("close", function () {
|
||||
console.log("SSH stream closed");
|
||||
logger.info("SSH stream closed");
|
||||
conn.end();
|
||||
});
|
||||
|
||||
// Send keystrokes from terminal to SSH
|
||||
socket.on("data", function (data) {
|
||||
stream.write(data);
|
||||
});
|
||||
|
||||
// Resize SSH terminal when client resizes
|
||||
socket.on("resize", ({ cols, rows }) => {
|
||||
if (stream && stream.setWindow) {
|
||||
stream.setWindow(rows, cols, rows * 100, cols * 100);
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-send initial terminal size to backend
|
||||
socket.emit("resize", { cols, rows });
|
||||
});
|
||||
})
|
||||
.on("close", function () {
|
||||
console.log("SSH connection closed");
|
||||
logger.info("SSH connection closed");
|
||||
socket.emit("error", "SSH connection closed");
|
||||
})
|
||||
.on("error", function (err) {
|
||||
console.error("Error:", err.message);
|
||||
logger.error("Error:", err.message);
|
||||
socket.emit("error", err.message);
|
||||
})
|
||||
.connect({
|
||||
@@ -96,10 +97,10 @@ io.on("connection", (socket) => {
|
||||
});
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
console.log("Client disconnected");
|
||||
logger.info("Client disconnected");
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(8081, '0.0.0.0', () => {
|
||||
console.log("Server is running on port 8081");
|
||||
logger.info("Server is running on port 8081");
|
||||
});
|
||||
@@ -20,7 +20,7 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
|
||||
const handleFileChange = (e) => {
|
||||
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')) {
|
||||
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 = (event) => {
|
||||
setForm({ ...form, rsaKey: event.target.result });
|
||||
@@ -65,7 +65,9 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
if (isFormValid()) handleAddHost();
|
||||
if (isFormValid()) {
|
||||
handleAddHost();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Stack spacing={2} sx={{ width: "100%", maxWidth: "100%", overflow: "hidden" }}>
|
||||
@@ -176,10 +178,13 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
|
||||
<FormControl>
|
||||
<FormLabel>Remember Host</FormLabel>
|
||||
<Checkbox
|
||||
checked={form.rememberHost ?? false}
|
||||
checked={form.rememberHost}
|
||||
onChange={(e) => setForm({ ...form, rememberHost: e.target.checked })}
|
||||
sx={{
|
||||
color: theme.palette.text.primary,
|
||||
'&.Mui-checked': {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -12,8 +12,7 @@ import {
|
||||
DialogContent,
|
||||
ModalDialog,
|
||||
Select,
|
||||
Option,
|
||||
Checkbox
|
||||
Option
|
||||
} from '@mui/joy';
|
||||
import theme from '/src/theme';
|
||||
|
||||
@@ -41,6 +40,11 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleEditHostInternal = (form) => {
|
||||
const updatedForm = { ...form, name: form.name || form.ip };
|
||||
handleEditHost(updatedForm);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (hostConfig) {
|
||||
setForm({
|
||||
@@ -81,7 +85,7 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
if (isFormValid()) handleEditHost();
|
||||
if (isFormValid()) handleEditHostInternal(form);
|
||||
}}
|
||||
>
|
||||
<Stack spacing={2} sx={{ width: "100%", maxWidth: "100%", overflow: "hidden" }}>
|
||||
|
||||
Reference in New Issue
Block a user