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
|
||||
- Split Screen (Up to 4) & Tab System
|
||||
- User Authentication
|
||||
- Data Persistence
|
||||
- Save Hosts (and easily view, connect, and manage them)
|
||||
|
||||
# Planned Features
|
||||
- Organize Hosts (folders, tags, etc.)
|
||||
- VNC
|
||||
- RDP
|
||||
- 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.)
|
||||
- SSH Tunneling
|
||||
- More Authentication Methods
|
||||
- Share Hosts
|
||||
- More Security Features (like 2FA, optionally store host passwords, etc.)
|
||||
- More Security Features (like 2FA, etc.)
|
||||
|
||||
# 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).
|
||||
|
||||
12
src/App.jsx
12
src/App.jsx
@@ -223,14 +223,12 @@ function App() {
|
||||
|
||||
const handleAddHost = () => {
|
||||
if (addHostForm.ip && addHostForm.user && addHostForm.port) {
|
||||
// If not remembering the host, just connect without auth validation
|
||||
if (!addHostForm.rememberHost) {
|
||||
connectToHost();
|
||||
setIsAddHostHidden(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// If remembering the host, validate auth method
|
||||
if (addHostForm.authMethod === 'Select Auth') {
|
||||
alert("Please select an authentication method.");
|
||||
return;
|
||||
@@ -244,7 +242,6 @@ function App() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Connect and save if all validation passes
|
||||
connectToHost();
|
||||
if (!addHostForm.storePassword) {
|
||||
addHostForm.password = '';
|
||||
@@ -257,7 +254,6 @@ function App() {
|
||||
};
|
||||
|
||||
const connectToHost = () => {
|
||||
// Create a clean host config object
|
||||
const hostConfig = {
|
||||
name: addHostForm.name || '',
|
||||
folder: addHostForm.folder || '',
|
||||
@@ -300,17 +296,14 @@ function App() {
|
||||
};
|
||||
|
||||
const connectToHostWithConfig = (hostConfig) => {
|
||||
// Ensure we have a valid host config
|
||||
if (!hostConfig || typeof hostConfig !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure all required fields are present
|
||||
if (!hostConfig.ip || !hostConfig.user) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a clean host config object with all required fields
|
||||
const cleanHostConfig = {
|
||||
name: hostConfig.name || '',
|
||||
folder: hostConfig.folder || '',
|
||||
@@ -459,8 +452,7 @@ function App() {
|
||||
oldHostConfig: oldConfig,
|
||||
newHostConfig: newConfig,
|
||||
});
|
||||
|
||||
// Keep the modal visible during the delay
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
} finally {
|
||||
setIsEditing(false);
|
||||
@@ -700,6 +692,8 @@ function App() {
|
||||
isErrorHidden={isErrorHidden}
|
||||
deleteHost={deleteHost}
|
||||
editHost={handleEditHost}
|
||||
shareHost={(hostId, username) => userRef.current?.shareHost(hostId, username)}
|
||||
userRef={userRef}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -16,10 +16,13 @@ function Launchpad({
|
||||
isErrorHidden,
|
||||
deleteHost,
|
||||
editHost,
|
||||
shareHost,
|
||||
userRef,
|
||||
}) {
|
||||
const launchpadRef = useRef(null);
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const [activeApp, setActiveApp] = useState('hostViewer');
|
||||
const [isAnyModalOpen, setIsAnyModalOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
@@ -28,7 +31,8 @@ function Launchpad({
|
||||
!launchpadRef.current.contains(event.target) &&
|
||||
isAddHostHidden &&
|
||||
isEditHostHidden &&
|
||||
isErrorHidden
|
||||
isErrorHidden &&
|
||||
!isAnyModalOpen
|
||||
) {
|
||||
onClose();
|
||||
}
|
||||
@@ -39,7 +43,15 @@ function Launchpad({
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, [onClose, isAddHostHidden, isEditHostHidden, isErrorHidden]);
|
||||
}, [onClose, isAddHostHidden, isEditHostHidden, isErrorHidden, isAnyModalOpen]);
|
||||
|
||||
const handleModalOpen = () => {
|
||||
setIsAnyModalOpen(true);
|
||||
};
|
||||
|
||||
const handleModalClose = () => {
|
||||
setIsAnyModalOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<CssVarsProvider theme={theme}>
|
||||
@@ -174,6 +186,10 @@ function Launchpad({
|
||||
deleteHost={deleteHost}
|
||||
editHost={editHost}
|
||||
openEditPanel={editHost}
|
||||
shareHost={shareHost}
|
||||
onModalOpen={handleModalOpen}
|
||||
onModalClose={handleModalClose}
|
||||
userRef={userRef}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -193,6 +209,8 @@ Launchpad.propTypes = {
|
||||
isErrorHidden: PropTypes.bool.isRequired,
|
||||
deleteHost: PropTypes.func.isRequired,
|
||||
editHost: PropTypes.func.isRequired,
|
||||
shareHost: PropTypes.func.isRequired,
|
||||
userRef: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default Launchpad;
|
||||
@@ -1,8 +1,9 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
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 [filteredHosts, setFilteredHosts] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
@@ -12,6 +13,8 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
|
||||
const [isDraggingOver, setIsDraggingOver] = useState(null);
|
||||
const isMounted = useRef(true);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isShareModalHidden, setIsShareModalHidden] = useState(true);
|
||||
const [selectedHostForShare, setSelectedHostForShare] = useState(null);
|
||||
|
||||
const fetchHosts = async () => {
|
||||
try {
|
||||
@@ -55,6 +58,14 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
|
||||
setFilteredHosts(filtered);
|
||||
}, [searchTerm, hosts]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isShareModalHidden) {
|
||||
onModalOpen();
|
||||
} else {
|
||||
onModalClose();
|
||||
}
|
||||
}, [isShareModalHidden, onModalOpen, onModalClose]);
|
||||
|
||||
const toggleFolder = (folderName) => {
|
||||
setCollapsedFolders(prev => {
|
||||
const newSet = new Set(prev);
|
||||
@@ -160,18 +171,33 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
|
||||
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
await deleteHost({ _id: hostWrapper._id });
|
||||
await new Promise(resolve => setTimeout(resolve, 500)); // Add a small delay for UX
|
||||
const isOwner = hostWrapper.createdBy?._id === userRef.current?.getUser()?.id;
|
||||
if (isOwner) {
|
||||
await deleteHost({ _id: hostWrapper._id });
|
||||
} else {
|
||||
await userRef.current.removeShare(hostWrapper._id);
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
await fetchHosts();
|
||||
} catch (error) {
|
||||
console.error('Failed to delete host:', error);
|
||||
console.error('Failed to delete/remove host:', error);
|
||||
} finally {
|
||||
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 hostConfig = hostWrapper.config || {};
|
||||
const isOwner = hostWrapper.createdBy?._id === userRef.current?.getUser()?.id;
|
||||
|
||||
if (!hostConfig) {
|
||||
return null;
|
||||
@@ -181,14 +207,21 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
|
||||
<div
|
||||
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' : ''}`}
|
||||
draggable
|
||||
onDragStart={(e) => handleDragStart(e, hostWrapper)}
|
||||
draggable={isOwner}
|
||||
onDragStart={(e) => isOwner && handleDragStart(e, hostWrapper)}
|
||||
onDragEnd={() => setDraggedHost(null)}
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<div className="text-neutral-500 cursor-grab active:cursor-grabbing">⋮⋮</div>
|
||||
<div>
|
||||
<p className="font-semibold">{hostConfig.name || hostConfig.ip}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<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">
|
||||
{hostConfig.user ? `${hostConfig.user}@${hostConfig.ip}` : `${hostConfig.ip}:${hostConfig.port}`}
|
||||
</p>
|
||||
@@ -214,35 +247,71 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
<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 ? "Deleting..." : "Delete"}
|
||||
</Button>
|
||||
<Button
|
||||
className="text-black"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openEditPanel(hostConfig);
|
||||
}}
|
||||
disabled={isDeleting}
|
||||
sx={{
|
||||
backgroundColor: "#6e6e6e",
|
||||
"&:hover": { backgroundColor: "#0f0f0f" },
|
||||
opacity: isDeleting ? 0.5 : 1,
|
||||
cursor: isDeleting ? "not-allowed" : "pointer"
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</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
|
||||
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 ? "Deleting..." : "Delete"}
|
||||
</Button>
|
||||
<Button
|
||||
className="text-black"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openEditPanel(hostConfig);
|
||||
}}
|
||||
disabled={isDeleting}
|
||||
sx={{
|
||||
backgroundColor: "#6e6e6e",
|
||||
"&:hover": { backgroundColor: "#0f0f0f" },
|
||||
opacity: isDeleting ? 0.5 : 1,
|
||||
cursor: isDeleting ? "not-allowed" : "pointer"
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</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>
|
||||
);
|
||||
@@ -328,6 +397,12 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
|
||||
<p className="text-gray-300">No hosts available...</p>
|
||||
)}
|
||||
</div>
|
||||
<ShareHostModal
|
||||
isHidden={isShareModalHidden}
|
||||
setIsHidden={setIsShareModalHidden}
|
||||
handleShare={handleShare}
|
||||
hostConfig={selectedHostForShare}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -339,6 +414,10 @@ HostViewer.propTypes = {
|
||||
deleteHost: PropTypes.func.isRequired,
|
||||
editHost: 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;
|
||||
@@ -87,14 +87,12 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde
|
||||
fitAddon.current.fit();
|
||||
resizeTerminal();
|
||||
const { cols, rows } = terminalInstance.current;
|
||||
|
||||
// Check if we have authentication
|
||||
|
||||
if (!hostConfig.password?.trim() && !hostConfig.rsaKey?.trim()) {
|
||||
setIsNoAuthHidden(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Only connect if we have authentication
|
||||
const sshConfig = {
|
||||
ip: hostConfig.ip,
|
||||
user: hostConfig.user,
|
||||
@@ -106,7 +104,6 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde
|
||||
socket.emit("connectToHost", cols, rows, sshConfig);
|
||||
});
|
||||
|
||||
// Fit and focus the terminal after it's opened
|
||||
setTimeout(() => {
|
||||
fitAddon.current.fit();
|
||||
resizeTerminal();
|
||||
@@ -175,7 +172,6 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde
|
||||
let authModalShown = false;
|
||||
|
||||
socket.on("noAuthRequired", () => {
|
||||
// Only show auth modal if we don't have valid credentials
|
||||
if (!hostConfig.password?.trim() && !hostConfig.rsaKey?.trim() && !authModalShown) {
|
||||
authModalShown = true;
|
||||
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, () => ({
|
||||
createUser,
|
||||
loginUser,
|
||||
@@ -272,6 +292,7 @@ export const User = forwardRef(({ onLoginSuccess, onCreateSuccess, onDeleteSucce
|
||||
deleteHost,
|
||||
shareHost,
|
||||
editHost,
|
||||
removeShare,
|
||||
getUser: () => currentUser.current,
|
||||
}));
|
||||
|
||||
|
||||
@@ -230,13 +230,32 @@ io.of('/database.io').on('connection', (socket) => {
|
||||
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);
|
||||
const hosts = await Host.find({ users: userId }).populate('createdBy');
|
||||
const decryptedHosts = await Promise.all(hosts.map(async host => {
|
||||
try {
|
||||
const ownerUser = host.createdBy;
|
||||
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) {
|
||||
logger.error('Get hosts error:', error);
|
||||
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) => {
|
||||
try {
|
||||
logger.debug(`Deleting user: ${userId}`);
|
||||
|
||||
@@ -32,7 +32,6 @@ io.on("connection", (socket) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Require authentication
|
||||
if (!hostConfig.password && !hostConfig.rsaKey) {
|
||||
logger.error("No authentication provided");
|
||||
socket.emit("error", "Authentication required");
|
||||
|
||||
@@ -53,12 +53,10 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
|
||||
};
|
||||
|
||||
const isFormValid = () => {
|
||||
// Basic validation for required fields
|
||||
if (!form.ip || !form.user || !form.port) return false;
|
||||
const portNum = Number(form.port);
|
||||
if (isNaN(portNum) || portNum < 1 || portNum > 65535) return false;
|
||||
|
||||
// Only validate auth method if rememberHost is true
|
||||
if (form.rememberHost) {
|
||||
if (form.authMethod === 'Select Auth') return false;
|
||||
if (form.authMethod === 'rsaKey' && !form.rsaKey) return false;
|
||||
@@ -71,15 +69,12 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
if (isFormValid()) {
|
||||
// If not remembering the host, only send basic connection info
|
||||
if (!form.rememberHost) {
|
||||
handleAddHost();
|
||||
} else {
|
||||
// Only include auth details if remembering the host
|
||||
handleAddHost();
|
||||
}
|
||||
|
||||
// Reset form after successful submission
|
||||
|
||||
setForm({
|
||||
name: '',
|
||||
folder: '',
|
||||
@@ -242,7 +237,7 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
|
||||
onChange={(e) => setForm({
|
||||
...form,
|
||||
rememberHost: e.target.checked,
|
||||
// Reset auth fields if unchecking remember host
|
||||
|
||||
...((!e.target.checked) && {
|
||||
authMethod: 'Select Auth',
|
||||
password: '',
|
||||
|
||||
@@ -41,7 +41,7 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
|
||||
port: hostConfig.port || 22,
|
||||
authMethod: hostConfig.password ? 'password' : hostConfig.rsaKey ? 'rsaKey' : 'Select Auth',
|
||||
rememberHost: true,
|
||||
storePassword: true,
|
||||
storePassword: !!(hostConfig.password || hostConfig.rsaKey),
|
||||
});
|
||||
}
|
||||
}, [isHidden, hostConfig]);
|
||||
|
||||
@@ -22,7 +22,6 @@ import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
||||
const NoAuthenticationModal = ({ isHidden, form, setForm, setIsNoAuthHidden, handleAuthSubmit }) => {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
// Initialize form with default values if not provided
|
||||
useEffect(() => {
|
||||
if (!form.authMethod) {
|
||||
setForm(prev => ({
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
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 DeleteForeverIcon from "@mui/icons-material/DeleteForever";
|
||||
import AccountCircleIcon from "@mui/icons-material/AccountCircle";
|
||||
import theme from "../theme";
|
||||
|
||||
export default function ProfileModal({
|
||||
isHidden,
|
||||
getUser,
|
||||
handleDeleteUser,
|
||||
handleLogoutUser,
|
||||
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