Added ability to share hosts. Fixed up overall UI errors in console and cleaned up code for release.
This commit is contained in:
@@ -33,10 +33,9 @@ Termix is an open-source forever free self-hosted SSH (other protocols planned,
|
|||||||
- SSH
|
- SSH
|
||||||
- Split Screen (Up to 4) & Tab System
|
- Split Screen (Up to 4) & Tab System
|
||||||
- User Authentication
|
- User Authentication
|
||||||
- Data Persistence
|
- Save Hosts (and easily view, connect, and manage them)
|
||||||
|
|
||||||
# Planned Features
|
# Planned Features
|
||||||
- Organize Hosts (folders, tags, etc.)
|
|
||||||
- VNC
|
- VNC
|
||||||
- RDP
|
- RDP
|
||||||
- SFTP (build in file transfer)
|
- SFTP (build in file transfer)
|
||||||
@@ -46,8 +45,7 @@ Termix is an open-source forever free self-hosted SSH (other protocols planned,
|
|||||||
- User Management (roles, permissions, etc.)
|
- User Management (roles, permissions, etc.)
|
||||||
- SSH Tunneling
|
- SSH Tunneling
|
||||||
- More Authentication Methods
|
- More Authentication Methods
|
||||||
- Share Hosts
|
- More Security Features (like 2FA, etc.)
|
||||||
- More Security Features (like 2FA, optionally store host passwords, etc.)
|
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
Visit the Termix [Wiki](https://github.com/LukeGus/Termix/wiki) for information on how to install Termix. You can also use these links to go directly to guide. [Docker](https://github.com/LukeGus/Termix/wiki/Docker) or [Manual](https://github.com/LukeGus/Termix/wiki/Manual).
|
Visit the Termix [Wiki](https://github.com/LukeGus/Termix/wiki) for information on how to install Termix. You can also use these links to go directly to guide. [Docker](https://github.com/LukeGus/Termix/wiki/Docker) or [Manual](https://github.com/LukeGus/Termix/wiki/Manual).
|
||||||
|
|||||||
10
src/App.jsx
10
src/App.jsx
@@ -223,14 +223,12 @@ function App() {
|
|||||||
|
|
||||||
const handleAddHost = () => {
|
const handleAddHost = () => {
|
||||||
if (addHostForm.ip && addHostForm.user && addHostForm.port) {
|
if (addHostForm.ip && addHostForm.user && addHostForm.port) {
|
||||||
// If not remembering the host, just connect without auth validation
|
|
||||||
if (!addHostForm.rememberHost) {
|
if (!addHostForm.rememberHost) {
|
||||||
connectToHost();
|
connectToHost();
|
||||||
setIsAddHostHidden(true);
|
setIsAddHostHidden(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If remembering the host, validate auth method
|
|
||||||
if (addHostForm.authMethod === 'Select Auth') {
|
if (addHostForm.authMethod === 'Select Auth') {
|
||||||
alert("Please select an authentication method.");
|
alert("Please select an authentication method.");
|
||||||
return;
|
return;
|
||||||
@@ -244,7 +242,6 @@ function App() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect and save if all validation passes
|
|
||||||
connectToHost();
|
connectToHost();
|
||||||
if (!addHostForm.storePassword) {
|
if (!addHostForm.storePassword) {
|
||||||
addHostForm.password = '';
|
addHostForm.password = '';
|
||||||
@@ -257,7 +254,6 @@ function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const connectToHost = () => {
|
const connectToHost = () => {
|
||||||
// Create a clean host config object
|
|
||||||
const hostConfig = {
|
const hostConfig = {
|
||||||
name: addHostForm.name || '',
|
name: addHostForm.name || '',
|
||||||
folder: addHostForm.folder || '',
|
folder: addHostForm.folder || '',
|
||||||
@@ -300,17 +296,14 @@ function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const connectToHostWithConfig = (hostConfig) => {
|
const connectToHostWithConfig = (hostConfig) => {
|
||||||
// Ensure we have a valid host config
|
|
||||||
if (!hostConfig || typeof hostConfig !== 'object') {
|
if (!hostConfig || typeof hostConfig !== 'object') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure all required fields are present
|
|
||||||
if (!hostConfig.ip || !hostConfig.user) {
|
if (!hostConfig.ip || !hostConfig.user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a clean host config object with all required fields
|
|
||||||
const cleanHostConfig = {
|
const cleanHostConfig = {
|
||||||
name: hostConfig.name || '',
|
name: hostConfig.name || '',
|
||||||
folder: hostConfig.folder || '',
|
folder: hostConfig.folder || '',
|
||||||
@@ -460,7 +453,6 @@ function App() {
|
|||||||
newHostConfig: newConfig,
|
newHostConfig: newConfig,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Keep the modal visible during the delay
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||||
} finally {
|
} finally {
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
@@ -700,6 +692,8 @@ function App() {
|
|||||||
isErrorHidden={isErrorHidden}
|
isErrorHidden={isErrorHidden}
|
||||||
deleteHost={deleteHost}
|
deleteHost={deleteHost}
|
||||||
editHost={handleEditHost}
|
editHost={handleEditHost}
|
||||||
|
shareHost={(hostId, username) => userRef.current?.shareHost(hostId, username)}
|
||||||
|
userRef={userRef}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -16,10 +16,13 @@ function Launchpad({
|
|||||||
isErrorHidden,
|
isErrorHidden,
|
||||||
deleteHost,
|
deleteHost,
|
||||||
editHost,
|
editHost,
|
||||||
|
shareHost,
|
||||||
|
userRef,
|
||||||
}) {
|
}) {
|
||||||
const launchpadRef = useRef(null);
|
const launchpadRef = useRef(null);
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
const [activeApp, setActiveApp] = useState('hostViewer');
|
const [activeApp, setActiveApp] = useState('hostViewer');
|
||||||
|
const [isAnyModalOpen, setIsAnyModalOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event) => {
|
const handleClickOutside = (event) => {
|
||||||
@@ -28,7 +31,8 @@ function Launchpad({
|
|||||||
!launchpadRef.current.contains(event.target) &&
|
!launchpadRef.current.contains(event.target) &&
|
||||||
isAddHostHidden &&
|
isAddHostHidden &&
|
||||||
isEditHostHidden &&
|
isEditHostHidden &&
|
||||||
isErrorHidden
|
isErrorHidden &&
|
||||||
|
!isAnyModalOpen
|
||||||
) {
|
) {
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
@@ -39,7 +43,15 @@ function Launchpad({
|
|||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("mousedown", handleClickOutside);
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
};
|
};
|
||||||
}, [onClose, isAddHostHidden, isEditHostHidden, isErrorHidden]);
|
}, [onClose, isAddHostHidden, isEditHostHidden, isErrorHidden, isAnyModalOpen]);
|
||||||
|
|
||||||
|
const handleModalOpen = () => {
|
||||||
|
setIsAnyModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleModalClose = () => {
|
||||||
|
setIsAnyModalOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CssVarsProvider theme={theme}>
|
<CssVarsProvider theme={theme}>
|
||||||
@@ -174,6 +186,10 @@ function Launchpad({
|
|||||||
deleteHost={deleteHost}
|
deleteHost={deleteHost}
|
||||||
editHost={editHost}
|
editHost={editHost}
|
||||||
openEditPanel={editHost}
|
openEditPanel={editHost}
|
||||||
|
shareHost={shareHost}
|
||||||
|
onModalOpen={handleModalOpen}
|
||||||
|
onModalClose={handleModalClose}
|
||||||
|
userRef={userRef}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -193,6 +209,8 @@ 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,
|
||||||
|
shareHost: PropTypes.func.isRequired,
|
||||||
|
userRef: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Launchpad;
|
export default Launchpad;
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { Button, Input } from "@mui/joy";
|
import { Button, Input } from "@mui/joy";
|
||||||
|
import ShareHostModal from "../../modals/ShareHostModal";
|
||||||
|
|
||||||
function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, editHost, openEditPanel }) {
|
function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, editHost, openEditPanel, shareHost, onModalOpen, onModalClose, userRef }) {
|
||||||
const [hosts, setHosts] = useState([]);
|
const [hosts, setHosts] = useState([]);
|
||||||
const [filteredHosts, setFilteredHosts] = useState([]);
|
const [filteredHosts, setFilteredHosts] = useState([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
@@ -12,6 +13,8 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
|
|||||||
const [isDraggingOver, setIsDraggingOver] = useState(null);
|
const [isDraggingOver, setIsDraggingOver] = useState(null);
|
||||||
const isMounted = useRef(true);
|
const isMounted = useRef(true);
|
||||||
const [isDeleting, setIsDeleting] = useState(false);
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
const [isShareModalHidden, setIsShareModalHidden] = useState(true);
|
||||||
|
const [selectedHostForShare, setSelectedHostForShare] = useState(null);
|
||||||
|
|
||||||
const fetchHosts = async () => {
|
const fetchHosts = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -55,6 +58,14 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
|
|||||||
setFilteredHosts(filtered);
|
setFilteredHosts(filtered);
|
||||||
}, [searchTerm, hosts]);
|
}, [searchTerm, hosts]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isShareModalHidden) {
|
||||||
|
onModalOpen();
|
||||||
|
} else {
|
||||||
|
onModalClose();
|
||||||
|
}
|
||||||
|
}, [isShareModalHidden, onModalOpen, onModalClose]);
|
||||||
|
|
||||||
const toggleFolder = (folderName) => {
|
const toggleFolder = (folderName) => {
|
||||||
setCollapsedFolders(prev => {
|
setCollapsedFolders(prev => {
|
||||||
const newSet = new Set(prev);
|
const newSet = new Set(prev);
|
||||||
@@ -160,18 +171,33 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
|
|||||||
|
|
||||||
setIsDeleting(true);
|
setIsDeleting(true);
|
||||||
try {
|
try {
|
||||||
|
const isOwner = hostWrapper.createdBy?._id === userRef.current?.getUser()?.id;
|
||||||
|
if (isOwner) {
|
||||||
await deleteHost({ _id: hostWrapper._id });
|
await deleteHost({ _id: hostWrapper._id });
|
||||||
await new Promise(resolve => setTimeout(resolve, 500)); // Add a small delay for UX
|
} else {
|
||||||
|
await userRef.current.removeShare(hostWrapper._id);
|
||||||
|
}
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
await fetchHosts();
|
await fetchHosts();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete host:', error);
|
console.error('Failed to delete/remove host:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setIsDeleting(false);
|
setIsDeleting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleShare = async (hostId, username) => {
|
||||||
|
try {
|
||||||
|
await shareHost(hostId, username);
|
||||||
|
await fetchHosts();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to share host:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const renderHostItem = (hostWrapper) => {
|
const renderHostItem = (hostWrapper) => {
|
||||||
const hostConfig = hostWrapper.config || {};
|
const hostConfig = hostWrapper.config || {};
|
||||||
|
const isOwner = hostWrapper.createdBy?._id === userRef.current?.getUser()?.id;
|
||||||
|
|
||||||
if (!hostConfig) {
|
if (!hostConfig) {
|
||||||
return null;
|
return null;
|
||||||
@@ -181,14 +207,21 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
|
|||||||
<div
|
<div
|
||||||
key={hostWrapper._id}
|
key={hostWrapper._id}
|
||||||
className={`flex justify-between items-center bg-neutral-800 p-3 rounded-lg shadow-md border border-neutral-700 w-full cursor-grab active:cursor-grabbing hover:border-neutral-500 transition-colors ${draggedHost === hostWrapper ? 'opacity-50' : ''}`}
|
className={`flex justify-between items-center bg-neutral-800 p-3 rounded-lg shadow-md border border-neutral-700 w-full cursor-grab active:cursor-grabbing hover:border-neutral-500 transition-colors ${draggedHost === hostWrapper ? 'opacity-50' : ''}`}
|
||||||
draggable
|
draggable={isOwner}
|
||||||
onDragStart={(e) => handleDragStart(e, hostWrapper)}
|
onDragStart={(e) => isOwner && handleDragStart(e, hostWrapper)}
|
||||||
onDragEnd={() => setDraggedHost(null)}
|
onDragEnd={() => setDraggedHost(null)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 flex-1">
|
<div className="flex items-center gap-2 flex-1">
|
||||||
<div className="text-neutral-500 cursor-grab active:cursor-grabbing">⋮⋮</div>
|
<div className="text-neutral-500 cursor-grab active:cursor-grabbing">⋮⋮</div>
|
||||||
<div>
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<p className="font-semibold">{hostConfig.name || hostConfig.ip}</p>
|
<p className="font-semibold">{hostConfig.name || hostConfig.ip}</p>
|
||||||
|
{!isOwner && (
|
||||||
|
<span className="text-xs bg-neutral-700 text-neutral-300 px-2 py-1 rounded">
|
||||||
|
Shared by {hostWrapper.createdBy?.username}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<p className="text-sm text-gray-400">
|
<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>
|
</p>
|
||||||
@@ -214,6 +247,25 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
|
|||||||
>
|
>
|
||||||
Connect
|
Connect
|
||||||
</Button>
|
</Button>
|
||||||
|
{isOwner && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
className="text-black"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setSelectedHostForShare(hostWrapper);
|
||||||
|
setIsShareModalHidden(false);
|
||||||
|
}}
|
||||||
|
disabled={isDeleting}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: "#6e6e6e",
|
||||||
|
"&:hover": { backgroundColor: "#0f0f0f" },
|
||||||
|
opacity: isDeleting ? 0.5 : 1,
|
||||||
|
cursor: isDeleting ? "not-allowed" : "pointer"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Share
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="text-black"
|
className="text-black"
|
||||||
onClick={(e) => handleDelete(e, hostWrapper)}
|
onClick={(e) => handleDelete(e, hostWrapper)}
|
||||||
@@ -243,6 +295,23 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
|
|||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!isOwner && (
|
||||||
|
<Button
|
||||||
|
className="text-black"
|
||||||
|
onClick={(e) => handleDelete(e, hostWrapper)}
|
||||||
|
disabled={isDeleting}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: "#6e6e6e",
|
||||||
|
"&:hover": { backgroundColor: "#0f0f0f" },
|
||||||
|
opacity: isDeleting ? 0.5 : 1,
|
||||||
|
cursor: isDeleting ? "not-allowed" : "pointer"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isDeleting ? "Removing..." : "Remove Share"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -328,6 +397,12 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
|
|||||||
<p className="text-gray-300">No hosts available...</p>
|
<p className="text-gray-300">No hosts available...</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<ShareHostModal
|
||||||
|
isHidden={isShareModalHidden}
|
||||||
|
setIsHidden={setIsShareModalHidden}
|
||||||
|
handleShare={handleShare}
|
||||||
|
hostConfig={selectedHostForShare}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -339,6 +414,10 @@ HostViewer.propTypes = {
|
|||||||
deleteHost: PropTypes.func.isRequired,
|
deleteHost: PropTypes.func.isRequired,
|
||||||
editHost: PropTypes.func.isRequired,
|
editHost: PropTypes.func.isRequired,
|
||||||
openEditPanel: PropTypes.func.isRequired,
|
openEditPanel: PropTypes.func.isRequired,
|
||||||
|
shareHost: PropTypes.func.isRequired,
|
||||||
|
onModalOpen: PropTypes.func.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired,
|
||||||
|
userRef: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default HostViewer;
|
export default HostViewer;
|
||||||
@@ -88,13 +88,11 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde
|
|||||||
resizeTerminal();
|
resizeTerminal();
|
||||||
const { cols, rows } = terminalInstance.current;
|
const { cols, rows } = terminalInstance.current;
|
||||||
|
|
||||||
// Check if we have authentication
|
|
||||||
if (!hostConfig.password?.trim() && !hostConfig.rsaKey?.trim()) {
|
if (!hostConfig.password?.trim() && !hostConfig.rsaKey?.trim()) {
|
||||||
setIsNoAuthHidden(false);
|
setIsNoAuthHidden(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only connect if we have authentication
|
|
||||||
const sshConfig = {
|
const sshConfig = {
|
||||||
ip: hostConfig.ip,
|
ip: hostConfig.ip,
|
||||||
user: hostConfig.user,
|
user: hostConfig.user,
|
||||||
@@ -106,7 +104,6 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde
|
|||||||
socket.emit("connectToHost", cols, rows, sshConfig);
|
socket.emit("connectToHost", cols, rows, sshConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fit and focus the terminal after it's opened
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
fitAddon.current.fit();
|
fitAddon.current.fit();
|
||||||
resizeTerminal();
|
resizeTerminal();
|
||||||
@@ -175,7 +172,6 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde
|
|||||||
let authModalShown = false;
|
let authModalShown = false;
|
||||||
|
|
||||||
socket.on("noAuthRequired", () => {
|
socket.on("noAuthRequired", () => {
|
||||||
// Only show auth modal if we don't have valid credentials
|
|
||||||
if (!hostConfig.password?.trim() && !hostConfig.rsaKey?.trim() && !authModalShown) {
|
if (!hostConfig.password?.trim() && !hostConfig.rsaKey?.trim() && !authModalShown) {
|
||||||
authModalShown = true;
|
authModalShown = true;
|
||||||
setIsNoAuthHidden(false);
|
setIsNoAuthHidden(false);
|
||||||
|
|||||||
@@ -261,6 +261,26 @@ export const User = forwardRef(({ onLoginSuccess, onCreateSuccess, onDeleteSucce
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const removeShare = async (hostId) => {
|
||||||
|
if (!currentUser.current) return onFailure("Not authenticated");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await new Promise((resolve) => {
|
||||||
|
socketRef.current.emit("removeShare", {
|
||||||
|
userId: currentUser.current.id,
|
||||||
|
sessionToken: currentUser.current.sessionToken,
|
||||||
|
hostId,
|
||||||
|
}, resolve);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response?.success) {
|
||||||
|
throw new Error(response?.error || "Failed to remove share");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
onFailure(error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
createUser,
|
createUser,
|
||||||
loginUser,
|
loginUser,
|
||||||
@@ -272,6 +292,7 @@ export const User = forwardRef(({ onLoginSuccess, onCreateSuccess, onDeleteSucce
|
|||||||
deleteHost,
|
deleteHost,
|
||||||
shareHost,
|
shareHost,
|
||||||
editHost,
|
editHost,
|
||||||
|
removeShare,
|
||||||
getUser: () => currentUser.current,
|
getUser: () => currentUser.current,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -230,13 +230,32 @@ io.of('/database.io').on('connection', (socket) => {
|
|||||||
return callback({ error: 'Invalid session' });
|
return callback({ error: 'Invalid session' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const hosts = await Host.find({ users: userId });
|
const hosts = await Host.find({ users: userId }).populate('createdBy');
|
||||||
const decryptedHosts = hosts.map(host => ({
|
const decryptedHosts = await Promise.all(hosts.map(async host => {
|
||||||
...host.toObject(),
|
try {
|
||||||
config: decryptData(host.config, userId, sessionToken)
|
const ownerUser = host.createdBy;
|
||||||
})).filter(host => host.config);
|
if (!ownerUser) {
|
||||||
|
logger.warn(`Owner not found for host: ${host._id}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
callback({ success: true, hosts: decryptedHosts });
|
const decryptedConfig = decryptData(host.config, ownerUser._id.toString(), ownerUser.sessionToken);
|
||||||
|
if (!decryptedConfig) {
|
||||||
|
logger.warn(`Failed to decrypt host config for host: ${host._id}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...host.toObject(),
|
||||||
|
config: decryptedConfig
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to process host ${host._id}:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
callback({ success: true, hosts: decryptedHosts.filter(host => host && host.config) });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Get hosts error:', error);
|
logger.error('Get hosts error:', error);
|
||||||
callback({ error: 'Failed to fetch hosts' });
|
callback({ error: 'Failed to fetch hosts' });
|
||||||
@@ -315,6 +334,33 @@ io.of('/database.io').on('connection', (socket) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on('removeShare', async ({ userId, sessionToken, hostId }, callback) => {
|
||||||
|
try {
|
||||||
|
logger.debug(`Removing share for host ${hostId} from user ${userId}`);
|
||||||
|
|
||||||
|
const user = await User.findOne({ _id: userId, sessionToken });
|
||||||
|
if (!user) {
|
||||||
|
logger.warn(`Invalid session for user: ${userId}`);
|
||||||
|
return callback({ error: 'Invalid session' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const host = await Host.findById(hostId);
|
||||||
|
if (!host) {
|
||||||
|
logger.warn(`Host not found: ${hostId}`);
|
||||||
|
return callback({ error: 'Host not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
host.users = host.users.filter(id => id.toString() !== userId);
|
||||||
|
await host.save();
|
||||||
|
|
||||||
|
logger.info(`Share removed successfully: ${hostId} -> ${userId}`);
|
||||||
|
callback({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Share removal error:', error);
|
||||||
|
callback({ error: 'Failed to remove share' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
socket.on('deleteUser', async ({ userId, sessionToken }, callback) => {
|
socket.on('deleteUser', async ({ userId, sessionToken }, callback) => {
|
||||||
try {
|
try {
|
||||||
logger.debug(`Deleting user: ${userId}`);
|
logger.debug(`Deleting user: ${userId}`);
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ io.on("connection", (socket) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Require authentication
|
|
||||||
if (!hostConfig.password && !hostConfig.rsaKey) {
|
if (!hostConfig.password && !hostConfig.rsaKey) {
|
||||||
logger.error("No authentication provided");
|
logger.error("No authentication provided");
|
||||||
socket.emit("error", "Authentication required");
|
socket.emit("error", "Authentication required");
|
||||||
|
|||||||
@@ -53,12 +53,10 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isFormValid = () => {
|
const isFormValid = () => {
|
||||||
// Basic validation for required fields
|
|
||||||
if (!form.ip || !form.user || !form.port) return false;
|
if (!form.ip || !form.user || !form.port) return false;
|
||||||
const portNum = Number(form.port);
|
const portNum = Number(form.port);
|
||||||
if (isNaN(portNum) || portNum < 1 || portNum > 65535) return false;
|
if (isNaN(portNum) || portNum < 1 || portNum > 65535) return false;
|
||||||
|
|
||||||
// Only validate auth method if rememberHost is true
|
|
||||||
if (form.rememberHost) {
|
if (form.rememberHost) {
|
||||||
if (form.authMethod === 'Select Auth') return false;
|
if (form.authMethod === 'Select Auth') return false;
|
||||||
if (form.authMethod === 'rsaKey' && !form.rsaKey) return false;
|
if (form.authMethod === 'rsaKey' && !form.rsaKey) return false;
|
||||||
@@ -71,15 +69,12 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
|
|||||||
const handleSubmit = (event) => {
|
const handleSubmit = (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (isFormValid()) {
|
if (isFormValid()) {
|
||||||
// If not remembering the host, only send basic connection info
|
|
||||||
if (!form.rememberHost) {
|
if (!form.rememberHost) {
|
||||||
handleAddHost();
|
handleAddHost();
|
||||||
} else {
|
} else {
|
||||||
// Only include auth details if remembering the host
|
|
||||||
handleAddHost();
|
handleAddHost();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset form after successful submission
|
|
||||||
setForm({
|
setForm({
|
||||||
name: '',
|
name: '',
|
||||||
folder: '',
|
folder: '',
|
||||||
@@ -242,7 +237,7 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
|
|||||||
onChange={(e) => setForm({
|
onChange={(e) => setForm({
|
||||||
...form,
|
...form,
|
||||||
rememberHost: e.target.checked,
|
rememberHost: e.target.checked,
|
||||||
// Reset auth fields if unchecking remember host
|
|
||||||
...((!e.target.checked) && {
|
...((!e.target.checked) && {
|
||||||
authMethod: 'Select Auth',
|
authMethod: 'Select Auth',
|
||||||
password: '',
|
password: '',
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
|
|||||||
port: hostConfig.port || 22,
|
port: hostConfig.port || 22,
|
||||||
authMethod: hostConfig.password ? 'password' : hostConfig.rsaKey ? 'rsaKey' : 'Select Auth',
|
authMethod: hostConfig.password ? 'password' : hostConfig.rsaKey ? 'rsaKey' : 'Select Auth',
|
||||||
rememberHost: true,
|
rememberHost: true,
|
||||||
storePassword: true,
|
storePassword: !!(hostConfig.password || hostConfig.rsaKey),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [isHidden, hostConfig]);
|
}, [isHidden, hostConfig]);
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
|||||||
const NoAuthenticationModal = ({ isHidden, form, setForm, setIsNoAuthHidden, handleAuthSubmit }) => {
|
const NoAuthenticationModal = ({ isHidden, form, setForm, setIsNoAuthHidden, handleAuthSubmit }) => {
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
|
||||||
// Initialize form with default values if not provided
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!form.authMethod) {
|
if (!form.authMethod) {
|
||||||
setForm(prev => ({
|
setForm(prev => ({
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Modal, Typography, Button } from "@mui/joy";
|
import { Modal, Button } from "@mui/joy";
|
||||||
import LogoutIcon from "@mui/icons-material/Logout";
|
import LogoutIcon from "@mui/icons-material/Logout";
|
||||||
import DeleteForeverIcon from "@mui/icons-material/DeleteForever";
|
import DeleteForeverIcon from "@mui/icons-material/DeleteForever";
|
||||||
import AccountCircleIcon from "@mui/icons-material/AccountCircle";
|
|
||||||
import theme from "../theme";
|
import theme from "../theme";
|
||||||
|
|
||||||
export default function ProfileModal({
|
export default function ProfileModal({
|
||||||
isHidden,
|
isHidden,
|
||||||
getUser,
|
|
||||||
handleDeleteUser,
|
handleDeleteUser,
|
||||||
handleLogoutUser,
|
handleLogoutUser,
|
||||||
setIsProfileHidden,
|
setIsProfileHidden,
|
||||||
|
|||||||
123
src/modals/ShareHostModal.jsx
Normal file
123
src/modals/ShareHostModal.jsx
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { CssVarsProvider } from '@mui/joy/styles';
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
Input,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
ModalDialog,
|
||||||
|
} from '@mui/joy';
|
||||||
|
import theme from '/src/theme';
|
||||||
|
|
||||||
|
const ShareHostModal = ({ isHidden, setIsHidden, handleShare, hostConfig }) => {
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmit = async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
if (isLoading || !username.trim()) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await handleShare(hostConfig._id, username.trim());
|
||||||
|
setUsername('');
|
||||||
|
setIsHidden(true);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleModalClick = (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CssVarsProvider theme={theme}>
|
||||||
|
<Modal
|
||||||
|
open={!isHidden}
|
||||||
|
onClose={() => !isLoading && setIsHidden(true)}
|
||||||
|
sx={{
|
||||||
|
position: 'fixed',
|
||||||
|
inset: 0,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backdropFilter: 'blur(5px)',
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.2)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalDialog
|
||||||
|
layout="center"
|
||||||
|
variant="outlined"
|
||||||
|
onClick={handleModalClick}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.tertiary,
|
||||||
|
borderColor: theme.palette.general.secondary,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
padding: 3,
|
||||||
|
borderRadius: 10,
|
||||||
|
maxWidth: '400px',
|
||||||
|
width: '100%',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
mx: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTitle sx={{ mb: 2 }}>Share Host</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<form onSubmit={handleSubmit} onClick={(e) => e.stopPropagation()}>
|
||||||
|
<FormControl error={!username.trim()}>
|
||||||
|
<FormLabel>Username to share with</FormLabel>
|
||||||
|
<Input
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
placeholder="Enter username"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.primary,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
mb: 2
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={!username.trim() || isLoading}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.primary,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: theme.palette.general.disabled
|
||||||
|
},
|
||||||
|
'&:disabled': {
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
color: 'rgba(255, 255, 255, 0.3)',
|
||||||
|
},
|
||||||
|
width: '100%',
|
||||||
|
height: '40px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isLoading ? "Sharing..." : "Share"}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</ModalDialog>
|
||||||
|
</Modal>
|
||||||
|
</CssVarsProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ShareHostModal.propTypes = {
|
||||||
|
isHidden: PropTypes.bool.isRequired,
|
||||||
|
setIsHidden: PropTypes.func.isRequired,
|
||||||
|
handleShare: PropTypes.func.isRequired,
|
||||||
|
hostConfig: PropTypes.object
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShareHostModal;
|
||||||
Reference in New Issue
Block a user