Updated various names for rsa keys to public keys, fixes ssh not connecting, better timing for editing host.

This commit is contained in:
Karmaa
2025-03-16 02:28:20 -05:00
parent fda8c7ce4b
commit bec8b67303
9 changed files with 372 additions and 164 deletions

View File

@@ -69,6 +69,7 @@ function App() {
const [isEditHostHidden, setIsEditHostHidden] = useState(true); const [isEditHostHidden, setIsEditHostHidden] = useState(true);
const [currentHostConfig, setCurrentHostConfig] = useState(null); const [currentHostConfig, setCurrentHostConfig] = useState(null);
const [isLoggingIn, setIsLoggingIn] = useState(true); const [isLoggingIn, setIsLoggingIn] = useState(true);
const [isEditing, setIsEditing] = useState(false);
useEffect(() => { useEffect(() => {
const handleKeyDown = (e) => { const handleKeyDown = (e) => {
@@ -221,38 +222,56 @@ function App() {
}, []); }, []);
const handleAddHost = () => { const handleAddHost = () => {
if (addHostForm.ip && addHostForm.user && addHostForm.port && addHostForm.authMethod !== 'Select Auth') { 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;
}
if (addHostForm.authMethod === 'password' && !addHostForm.password) { if (addHostForm.authMethod === 'password' && !addHostForm.password) {
setIsNoAuthHidden(false); setIsNoAuthHidden(false);
} else if (addHostForm.authMethod === 'rsaKey' && !addHostForm.rsaKey) { return;
}
if (addHostForm.authMethod === 'rsaKey' && !addHostForm.rsaKey) {
setIsNoAuthHidden(false); setIsNoAuthHidden(false);
} else { return;
}
// Connect and save if all validation passes
connectToHost(); connectToHost();
if (addHostForm.rememberHost) {
if (!addHostForm.storePassword) { if (!addHostForm.storePassword) {
addHostForm.password = ''; addHostForm.password = '';
} }
handleSaveHost(); handleSaveHost();
} setIsAddHostHidden(true);
}
} else { } else {
alert("Please fill out all fields."); alert("Please fill out all required fields (IP, User, Port).");
} }
}; };
const connectToHost = () => { const connectToHost = () => {
const newTerminal = { // Create a clean host config object
id: nextId, const hostConfig = {
title: addHostForm.name || addHostForm.ip, name: addHostForm.name || '',
hostConfig: { folder: addHostForm.folder || '',
name: addHostForm.name,
folder: addHostForm.folder,
ip: addHostForm.ip, ip: addHostForm.ip,
user: addHostForm.user, user: addHostForm.user,
password: addHostForm.authMethod === 'password' ? addHostForm.password : undefined,
rsaKey: addHostForm.authMethod === 'rsaKey' ? addHostForm.rsaKey : undefined,
port: String(addHostForm.port), port: String(addHostForm.port),
}, password: addHostForm.rememberHost && addHostForm.authMethod === 'password' ? addHostForm.password : undefined,
rsaKey: addHostForm.rememberHost && addHostForm.authMethod === 'rsaKey' ? addHostForm.rsaKey : undefined,
};
const newTerminal = {
id: nextId,
title: hostConfig.name || hostConfig.ip,
hostConfig,
terminalRef: null, terminalRef: null,
}; };
setTerminals([...terminals, newTerminal]); setTerminals([...terminals, newTerminal]);
@@ -281,10 +300,31 @@ function App() {
}; };
const connectToHostWithConfig = (hostConfig) => { 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 || '',
ip: hostConfig.ip.trim(),
user: hostConfig.user.trim(),
port: hostConfig.port || '22',
password: hostConfig.password?.trim(),
rsaKey: hostConfig.rsaKey?.trim(),
};
const newTerminal = { const newTerminal = {
id: nextId, id: nextId,
title: hostConfig.name || hostConfig.ip, title: cleanHostConfig.name || cleanHostConfig.ip,
hostConfig: hostConfig, hostConfig: cleanHostConfig,
terminalRef: null, terminalRef: null,
}; };
setTerminals([...terminals, newTerminal]); setTerminals([...terminals, newTerminal]);
@@ -411,10 +451,21 @@ function App() {
const handleEditHost = async (oldConfig, newConfig = null) => { const handleEditHost = async (oldConfig, newConfig = null) => {
try { try {
if (newConfig) { if (newConfig) {
if (isEditing) return;
setIsEditing(true);
try {
await userRef.current.editHost({ await userRef.current.editHost({
oldHostConfig: oldConfig, oldHostConfig: oldConfig,
newHostConfig: newConfig, newHostConfig: newConfig,
}); });
// Keep the modal visible during the delay
await new Promise(resolve => setTimeout(resolve, 3000));
} finally {
setIsEditing(false);
setIsEditHostHidden(true);
}
return; return;
} }
@@ -423,6 +474,7 @@ function App() {
console.error('Edit failed:', error); console.error('Edit failed:', error);
setErrorMessage(`Edit failed: ${error}`); setErrorMessage(`Edit failed: ${error}`);
setIsErrorHidden(false); setIsErrorHidden(false);
setIsEditing(false);
} }
}; };

View File

@@ -161,7 +161,15 @@ function Launchpad({
{activeApp === 'hostViewer' && ( {activeApp === 'hostViewer' && (
<HostViewer <HostViewer
getHosts={getHosts} getHosts={getHosts}
connectToHost={connectToHost} connectToHost={(hostConfig) => {
if (!hostConfig || typeof hostConfig !== 'object') {
return;
}
if (!hostConfig.ip || !hostConfig.user) {
return;
}
connectToHost(hostConfig);
}}
setIsAddHostHidden={setIsAddHostHidden} setIsAddHostHidden={setIsAddHostHidden}
deleteHost={deleteHost} deleteHost={deleteHost}
editHost={editHost} editHost={editHost}

View File

@@ -11,6 +11,7 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
const [draggedHost, setDraggedHost] = useState(null); const [draggedHost, setDraggedHost] = useState(null);
const [isDraggingOver, setIsDraggingOver] = useState(null); const [isDraggingOver, setIsDraggingOver] = useState(null);
const isMounted = useRef(true); const isMounted = useRef(true);
const [isDeleting, setIsDeleting] = useState(false);
const fetchHosts = async () => { const fetchHosts = async () => {
try { try {
@@ -153,6 +154,22 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
setDraggedHost(null); setDraggedHost(null);
}; };
const handleDelete = async (e, hostWrapper) => {
e.stopPropagation();
if (isDeleting) return;
setIsDeleting(true);
try {
await deleteHost({ _id: hostWrapper._id });
await new Promise(resolve => setTimeout(resolve, 500)); // Add a small delay for UX
await fetchHosts();
} catch (error) {
console.error('Failed to delete host:', error);
} finally {
setIsDeleting(false);
}
};
const renderHostItem = (hostWrapper) => { const renderHostItem = (hostWrapper) => {
const hostConfig = hostWrapper.config || {}; const hostConfig = hostWrapper.config || {};
@@ -182,27 +199,33 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
className="text-black" className="text-black"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
connectToHost(hostConfig); if (!hostWrapper.config || !hostWrapper.config.ip || !hostWrapper.config.user) {
return;
}
connectToHost(hostWrapper.config);
}} }}
disabled={isDeleting}
sx={{ sx={{
backgroundColor: "#6e6e6e", backgroundColor: "#6e6e6e",
"&:hover": { backgroundColor: "#0f0f0f" } "&:hover": { backgroundColor: "#0f0f0f" },
opacity: isDeleting ? 0.5 : 1,
cursor: isDeleting ? "not-allowed" : "pointer"
}} }}
> >
Connect Connect
</Button> </Button>
<Button <Button
className="text-black" className="text-black"
onClick={(e) => { onClick={(e) => handleDelete(e, hostWrapper)}
e.stopPropagation(); disabled={isDeleting}
deleteHost({ ...hostConfig, _id: hostWrapper._id });
}}
sx={{ sx={{
backgroundColor: "#6e6e6e", backgroundColor: "#6e6e6e",
"&:hover": { backgroundColor: "#0f0f0f" } "&:hover": { backgroundColor: "#0f0f0f" },
opacity: isDeleting ? 0.5 : 1,
cursor: isDeleting ? "not-allowed" : "pointer"
}} }}
> >
Delete {isDeleting ? "Deleting..." : "Delete"}
</Button> </Button>
<Button <Button
className="text-black" className="text-black"
@@ -210,9 +233,12 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
e.stopPropagation(); e.stopPropagation();
openEditPanel(hostConfig); openEditPanel(hostConfig);
}} }}
disabled={isDeleting}
sx={{ sx={{
backgroundColor: "#6e6e6e", backgroundColor: "#6e6e6e",
"&:hover": { backgroundColor: "#0f0f0f" } "&:hover": { backgroundColor: "#0f0f0f" },
opacity: isDeleting ? 0.5 : 1,
cursor: isDeleting ? "not-allowed" : "pointer"
}} }}
> >
Edit Edit

View File

@@ -55,12 +55,6 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde
terminalInstance.current.loadAddon(fitAddon.current); terminalInstance.current.loadAddon(fitAddon.current);
terminalInstance.current.open(terminalRef.current); terminalInstance.current.open(terminalRef.current);
setTimeout(() => {
fitAddon.current.fit();
resizeTerminal();
terminalInstance.current.focus();
}, 50);
const socket = io( const socket = io(
window.location.hostname === "localhost" window.location.hostname === "localhost"
? "http://localhost:8081" ? "http://localhost:8081"
@@ -72,17 +66,53 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde
); );
socketRef.current = socket; socketRef.current = socket;
socket.on("connect_error", (error) => {
terminalInstance.current.write(`\r\n*** Socket connection error: ${error.message} ***\r\n`);
});
socket.on("connect_timeout", () => {
terminalInstance.current.write(`\r\n*** Socket connection timeout ***\r\n`);
});
socket.on("error", (err) => {
const isAuthError = err.toLowerCase().includes("authentication") || err.toLowerCase().includes("auth");
if (isAuthError && !hostConfig.password?.trim() && !hostConfig.rsaKey?.trim() && !authModalShown) {
authModalShown = true;
setIsNoAuthHidden(false);
}
terminalInstance.current.write(`\r\n*** Error: ${err} ***\r\n`);
});
socket.on("connect", () => { socket.on("connect", () => {
fitAddon.current.fit(); fitAddon.current.fit();
resizeTerminal(); resizeTerminal();
const { cols, rows } = terminalInstance.current; const { cols, rows } = terminalInstance.current;
if (!hostConfig.password && !hostConfig.rsaKey) {
// Check if we have authentication
if (!hostConfig.password?.trim() && !hostConfig.rsaKey?.trim()) {
setIsNoAuthHidden(false); setIsNoAuthHidden(false);
} else { return;
socket.emit("connectToHost", cols, rows, hostConfig);
} }
// Only connect if we have authentication
const sshConfig = {
ip: hostConfig.ip,
user: hostConfig.user,
port: Number(hostConfig.port) || 22,
password: hostConfig.password?.trim(),
rsaKey: hostConfig.rsaKey?.trim()
};
socket.emit("connectToHost", cols, rows, sshConfig);
}); });
// Fit and focus the terminal after it's opened
setTimeout(() => {
fitAddon.current.fit();
resizeTerminal();
terminalInstance.current.focus();
}, 50);
socket.on("data", (data) => { socket.on("data", (data) => {
const decoder = new TextDecoder("utf-8"); const decoder = new TextDecoder("utf-8");
terminalInstance.current.write(decoder.decode(new Uint8Array(data))); terminalInstance.current.write(decoder.decode(new Uint8Array(data)));
@@ -142,17 +172,26 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde
} }
}); });
socket.on("noAuthRequired", () => { let authModalShown = false;
setIsNoAuthHidden(false);
});
socket.on("error", (err) => { socket.on("noAuthRequired", () => {
terminalInstance.current.write(`\r\n*** Error: ${err} ***\r\n`); // Only show auth modal if we don't have valid credentials
if (!hostConfig.password?.trim() && !hostConfig.rsaKey?.trim() && !authModalShown) {
authModalShown = true;
setIsNoAuthHidden(false);
}
}); });
return () => { return () => {
if (terminalInstance.current) {
terminalInstance.current.dispose(); terminalInstance.current.dispose();
socket.disconnect(); terminalInstance.current = null;
}
if (socketRef.current) {
socketRef.current.disconnect();
socketRef.current = null;
}
authModalShown = false;
}; };
}, [hostConfig]); }, [hostConfig]);
@@ -201,7 +240,7 @@ NewTerminal.propTypes = {
user: PropTypes.string.isRequired, user: PropTypes.string.isRequired,
password: PropTypes.string, password: PropTypes.string,
rsaKey: PropTypes.string, rsaKey: PropTypes.string,
port: PropTypes.number.isRequired, port: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
}).isRequired, }).isRequired,
isVisible: PropTypes.bool.isRequired, isVisible: PropTypes.bool.isRequired,
setIsNoAuthHidden: PropTypes.func.isRequired, setIsNoAuthHidden: PropTypes.func.isRequired,

View File

@@ -177,7 +177,18 @@ export const User = forwardRef(({ onLoginSuccess, onCreateSuccess, onDeleteSucce
}); });
if (response?.success) { if (response?.success) {
return response.hosts; return response.hosts.map(host => ({
...host,
config: host.config ? {
name: host.config.name || '',
folder: host.config.folder || '',
ip: host.config.ip || '',
user: host.config.user || '',
port: host.config.port || '22',
password: host.config.password || '',
rsaKey: host.config.rsaKey || '',
} : {}
})).filter(host => host.config && host.config.ip && host.config.user);
} else { } else {
throw new Error(response?.error || "Failed to fetch hosts"); throw new Error(response?.error || "Failed to fetch hosts");
} }

View File

@@ -26,9 +26,16 @@ io.on("connection", (socket) => {
let stream = null; let stream = null;
socket.on("connectToHost", (cols, rows, hostConfig) => { socket.on("connectToHost", (cols, rows, hostConfig) => {
if (!hostConfig || !hostConfig.ip || !hostConfig.user || (!hostConfig.password && !hostConfig.rsaKey) || !hostConfig.port) { if (!hostConfig || !hostConfig.ip || !hostConfig.user || !hostConfig.port) {
logger.error("Invalid hostConfig received:", hostConfig); logger.error("Invalid hostConfig received - missing required fields:", hostConfig);
socket.emit("noAuthRequired"); socket.emit("error", "Missing required connection details (IP, user, or port)");
return;
}
// Require authentication
if (!hostConfig.password && !hostConfig.rsaKey) {
logger.error("No authentication provided");
socket.emit("error", "Authentication required");
return; return;
} }
@@ -36,11 +43,10 @@ io.on("connection", (socket) => {
ip: hostConfig.ip, ip: hostConfig.ip,
port: hostConfig.port, port: hostConfig.port,
user: hostConfig.user, user: hostConfig.user,
password: hostConfig.password ? '***REDACTED***' : undefined, authType: hostConfig.password ? 'password' : 'public key',
rsaKey: hostConfig.rsaKey ? '***REDACTED***' : undefined,
}; };
logger.info("Received hostConfig:", safeHostConfig); logger.info("Connecting with config:", safeHostConfig);
const { ip, port, user, password, rsaKey } = hostConfig; const { ip, port, user, password, rsaKey } = hostConfig;
const conn = new SSHClient(); const conn = new SSHClient();
@@ -50,7 +56,7 @@ io.on("connection", (socket) => {
conn.shell({ term: "xterm-256color" }, function (err, newStream) { conn.shell({ term: "xterm-256color" }, function (err, newStream) {
if (err) { if (err) {
logger.error("Error:", err.message); logger.error("Shell error:", err.message);
socket.emit("error", err.message); socket.emit("error", err.message);
return; return;
} }

View File

@@ -31,32 +31,58 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
const handleFileChange = (e) => { const handleFileChange = (e) => {
const file = e.target.files[0]; const file = e.target.files[0];
if (file) { 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') || file.name.endsWith('.pub')) { if (file.name.endsWith('.key') || file.name.endsWith('.pem') || file.name.endsWith('.pub')) {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (event) => { reader.onload = (event) => {
setForm({ ...form, rsaKey: event.target.result }); setForm({ ...form, rsaKey: event.target.result });
}; };
reader.readAsText(file); reader.readAsText(file);
} else { } else {
alert("Please upload a valid RSA private key file."); alert("Please upload a valid public key file.");
} }
} }
}; };
const handleAuthChange = (newMethod) => {
setForm((prev) => ({
...prev,
authMethod: newMethod,
password: "",
rsaKey: ""
}));
};
const isFormValid = () => { const isFormValid = () => {
if (form.authMethod === 'Select Auth') return false; // 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);
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; if (form.authMethod === 'rsaKey' && !form.rsaKey) return false;
if (form.authMethod === 'password' && !form.password) return false; if (form.authMethod === 'password' && !form.password) return false;
}
return true; return true;
}; };
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) {
handleAddHost(); handleAddHost();
} else {
// Only include auth details if remembering the host
handleAddHost();
}
// Reset form after successful submission
setForm({ setForm({
name: '', name: '',
folder: '',
ip: '', ip: '',
user: '', user: '',
password: '', password: '',
@@ -66,6 +92,7 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
rememberHost: false, rememberHost: false,
storePassword: true, storePassword: true,
}); });
setIsAddHostHidden(true);
} }
}; };
@@ -212,7 +239,17 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
<FormLabel>Remember Host</FormLabel> <FormLabel>Remember Host</FormLabel>
<Checkbox <Checkbox
checked={form.rememberHost} checked={form.rememberHost}
onChange={(e) => setForm({ ...form, rememberHost: e.target.checked })} onChange={(e) => setForm({
...form,
rememberHost: e.target.checked,
// Reset auth fields if unchecking remember host
...((!e.target.checked) && {
authMethod: 'Select Auth',
password: '',
rsaKey: '',
storePassword: true
})
})}
sx={{ sx={{
color: theme.palette.text.primary, color: theme.palette.text.primary,
'&.Mui-checked': { '&.Mui-checked': {
@@ -239,44 +276,38 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
<FormControl error={!form.authMethod || form.authMethod === 'Select Auth'}> <FormControl error={!form.authMethod || form.authMethod === 'Select Auth'}>
<FormLabel>Authentication Method</FormLabel> <FormLabel>Authentication Method</FormLabel>
<Select <Select
value={form.authMethod || 'Select Auth'} value={form.authMethod}
onChange={(e, newValue) => setForm({ ...form, authMethod: newValue })} onChange={(e, val) => handleAuthChange(val)}
required
sx={{ sx={{
backgroundColor: !form.authMethod || form.authMethod === 'Select Auth' ? theme.palette.general.tertiary : theme.palette.general.primary, backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary, color: theme.palette.text.primary,
'&:hover': {
backgroundColor: theme.palette.general.disabled,
},
}} }}
> >
<Option value="Select Auth" disabled> <Option value="Select Auth" disabled>Select Auth</Option>
Select Auth
</Option>
<Option value="password">Password</Option> <Option value="password">Password</Option>
<Option value="rsaKey">RSA Key</Option> <Option value="rsaKey">Public Key</Option>
</Select> </Select>
</FormControl> </FormControl>
{form.authMethod === 'password' && ( {form.authMethod === 'password' && (
<FormControl error={!form.password}> <FormControl error={!form.password}>
<FormLabel>Host Password</FormLabel> <FormLabel>Password</FormLabel>
<div style={{ display: 'flex', alignItems: 'center' }}> <div style={{ display: 'flex', alignItems: 'center' }}>
<Input <Input
type={showPassword ? 'text' : 'password'} type={showPassword ? 'text' : 'password'}
value={form.password} value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })} onChange={(e) => setForm({ ...form, password: e.target.value })}
required
sx={{ sx={{
backgroundColor: theme.palette.general.primary, backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary, color: theme.palette.text.primary,
flex: 1, flex: 1
}} }}
/> />
<IconButton <IconButton
onClick={() => setShowPassword(!showPassword)} onClick={() => setShowPassword(!showPassword)}
sx={{ sx={{
color: theme.palette.text.primary, color: theme.palette.text.primary,
marginLeft: 1, marginLeft: 1
}} }}
> >
{showPassword ? <VisibilityOff /> : <Visibility />} {showPassword ? <VisibilityOff /> : <Visibility />}
@@ -284,9 +315,10 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
</div> </div>
</FormControl> </FormControl>
)} )}
{form.authMethod === 'rsaKey' && ( {form.authMethod === 'rsaKey' && (
<FormControl error={!form.rsaKey}> <FormControl error={!form.rsaKey}>
<FormLabel>RSA Key</FormLabel> <FormLabel>Public Key</FormLabel>
<Button <Button
component="label" component="label"
sx={{ sx={{
@@ -302,11 +334,10 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
}, },
}} }}
> >
{form.rsaKey ? 'Change RSA Key File' : 'Upload RSA Key File'} {form.rsaKey ? 'Change Public Key File' : 'Upload Public Key File'}
<Input <Input
type="file" type="file"
onChange={handleFileChange} onChange={handleFileChange}
required
sx={{ display: 'none' }} sx={{ display: 'none' }}
/> />
</Button> </Button>

View File

@@ -27,22 +27,24 @@ import VisibilityOff from '@mui/icons-material/VisibilityOff';
const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostHidden, hostConfig }) => { const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostHidden, hostConfig }) => {
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [activeTab, setActiveTab] = useState(0); const [activeTab, setActiveTab] = useState(0);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => { useEffect(() => {
if (hostConfig && !isHidden) { if (!isHidden && hostConfig) {
setForm({ setForm({
name: hostConfig.name || "", name: hostConfig.name || '',
folder: hostConfig.folder || "", folder: hostConfig.folder || '',
ip: hostConfig.ip || "", ip: hostConfig.ip || '',
user: hostConfig.user || "", user: hostConfig.user || '',
password: hostConfig.password || "", password: hostConfig.password || '',
rsaKey: hostConfig.rsaKey || '',
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: true,
}); });
} }
}, [hostConfig, isHidden]); }, [isHidden, hostConfig]);
const handleFileChange = (e) => { const handleFileChange = (e) => {
const file = e.target.files[0]; const file = e.target.files[0];
@@ -68,7 +70,9 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
setForm((prev) => ({ setForm((prev) => ({
...prev, ...prev,
storePassword: Boolean(checked), storePassword: Boolean(checked),
authMethod: checked ? 'password' : 'Select Auth' password: checked ? prev.password : "",
rsaKey: checked ? prev.rsaKey : "",
authMethod: checked ? prev.authMethod : "Select Auth"
})); }));
}; };
@@ -85,22 +89,23 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
return true; return true;
}; };
const handleSave = async (e) => { const handleSubmit = async (event) => {
e.preventDefault(); event.preventDefault();
if (isLoading) return;
setIsLoading(true);
try { try {
const newConfig = { await handleEditHost(hostConfig, {
...form, name: form.name || form.ip,
folder: form.folder,
ip: form.ip,
user: form.user,
password: form.authMethod === 'password' ? form.password : undefined,
rsaKey: form.authMethod === 'rsaKey' ? form.rsaKey : undefined,
port: String(form.port), port: String(form.port),
}; });
} finally {
if (form.authMethod === 'rsaKey' || !form.storePassword) { setIsLoading(false);
newConfig.password = '';
}
await handleEditHost(hostConfig, newConfig);
setIsEditHostHidden(true);
} catch (error) {
console.error('Failed to save:', error);
} }
}; };
@@ -108,15 +113,20 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
<CssVarsProvider theme={theme}> <CssVarsProvider theme={theme}>
<Modal <Modal
open={!isHidden} open={!isHidden}
onClose={() => setIsEditHostHidden(true)} onClose={() => !isLoading && setIsEditHostHidden(true)}
sx={{ sx={{
position: 'fixed',
inset: 0,
display: 'flex', display: 'flex',
justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center',
backdropFilter: 'blur(5px)',
backgroundColor: 'rgba(0, 0, 0, 0.2)',
}} }}
> >
<ModalDialog <ModalDialog
layout="center" layout="center"
variant="outlined"
sx={{ sx={{
backgroundColor: theme.palette.general.tertiary, backgroundColor: theme.palette.general.tertiary,
borderColor: theme.palette.general.secondary, borderColor: theme.palette.general.secondary,
@@ -133,7 +143,7 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
> >
<DialogTitle sx={{ mb: 2 }}>Edit Host</DialogTitle> <DialogTitle sx={{ mb: 2 }}>Edit Host</DialogTitle>
<DialogContent> <DialogContent>
<form onSubmit={handleSave}> <form onSubmit={handleSubmit}>
<Tabs <Tabs
value={activeTab} value={activeTab}
onChange={(e, val) => setActiveTab(val)} onChange={(e, val) => setActiveTab(val)}
@@ -270,7 +280,7 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
> >
<Option value="Select Auth" disabled>Select Auth</Option> <Option value="Select Auth" disabled>Select Auth</Option>
<Option value="password">Password</Option> <Option value="password">Password</Option>
<Option value="rsaKey">RSA Key</Option> <Option value="rsaKey">Public Key</Option>
</Select> </Select>
</FormControl> </FormControl>
)} )}
@@ -304,7 +314,7 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
{form.authMethod === 'rsaKey' && form.storePassword && ( {form.authMethod === 'rsaKey' && form.storePassword && (
<FormControl error={!form.rsaKey && !hostConfig?.rsaKey}> <FormControl error={!form.rsaKey && !hostConfig?.rsaKey}>
<FormLabel>RSA Key</FormLabel> <FormLabel>Public Key</FormLabel>
<Button <Button
component="label" component="label"
sx={{ sx={{
@@ -320,7 +330,7 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
}, },
}} }}
> >
{form.rsaKey ? 'Change RSA Key File' : 'Upload RSA Key File'} {form.rsaKey ? 'Change Public Key File' : 'Upload Public Key File'}
<Input <Input
type="file" type="file"
onChange={handleFileChange} onChange={handleFileChange}
@@ -348,7 +358,7 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
<Button <Button
type="submit" type="submit"
disabled={!isFormValid()} disabled={!isFormValid() || isLoading}
sx={{ sx={{
backgroundColor: theme.palette.general.primary, backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary, color: theme.palette.text.primary,
@@ -364,7 +374,7 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
height: '40px', height: '40px',
}} }}
> >
Save Changes {isLoading ? "Saving..." : "Save Changes"}
</Button> </Button>
</form> </form>
</DialogContent> </DialogContent>

View File

@@ -15,15 +15,25 @@ import {
Option, Option,
} from '@mui/joy'; } from '@mui/joy';
import theme from '/src/theme'; import theme from '/src/theme';
import { useState } from 'react'; import { useState, useEffect } from 'react';
import Visibility from '@mui/icons-material/Visibility'; import Visibility from '@mui/icons-material/Visibility';
import VisibilityOff from '@mui/icons-material/VisibilityOff'; 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(() => {
if (!form.authMethod) {
setForm(prev => ({
...prev,
authMethod: 'Select Auth'
}));
}
}, []);
const isFormValid = () => { const isFormValid = () => {
if (form.authMethod === 'Select Auth') return false; if (!form.authMethod || form.authMethod === 'Select Auth') return false;
if (form.authMethod === 'rsaKey' && !form.rsaKey) return false; if (form.authMethod === 'rsaKey' && !form.rsaKey) return false;
if (form.authMethod === 'password' && !form.password) return false; if (form.authMethod === 'password' && !form.password) return false;
return true; return true;
@@ -46,7 +56,11 @@ const NoAuthenticationModal = ({ isHidden, form, setForm, setIsNoAuthHidden, han
setIsNoAuthHidden(true); setIsNoAuthHidden(true);
} }
}} }}
disableBackdropClic sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
> >
<ModalDialog <ModalDialog
layout="center" layout="center"
@@ -56,58 +70,53 @@ const NoAuthenticationModal = ({ isHidden, form, setForm, setIsNoAuthHidden, han
color: theme.palette.text.primary, color: theme.palette.text.primary,
padding: 3, padding: 3,
borderRadius: 10, borderRadius: 10,
width: "auto", maxWidth: '500px',
maxWidth: "90vw", width: '100%',
minWidth: "fit-content", maxHeight: '80vh',
overflow: "hidden", overflow: 'auto',
display: "flex", boxSizing: 'border-box',
flexDirection: "column", mx: 2,
alignItems: "center",
}} }}
> >
<DialogTitle>Authentication Required</DialogTitle> <DialogTitle sx={{ mb: 2 }}>Authentication Required</DialogTitle>
<DialogContent> <DialogContent>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<Stack spacing={2} sx={{ width: "100%", maxWidth: "100%", overflow: "hidden" }}> <Stack spacing={2}>
<FormControl error={!form.authMethod || form.authMethod === 'Select Auth'}> <FormControl error={!form.authMethod || form.authMethod === 'Select Auth'}>
<FormLabel sx={{ color: theme.palette.text.primary }}>Authentication Method</FormLabel> <FormLabel>Authentication Method</FormLabel>
<Select <Select
value={form.authMethod || 'Select Auth'} value={form.authMethod || 'Select Auth'}
onChange={(e, newValue) => setForm({ ...form, authMethod: newValue })} onChange={(e, val) => setForm(prev => ({ ...prev, authMethod: val, password: '', rsaKey: '' }))}
required
sx={{ sx={{
backgroundColor: !form.authMethod || form.authMethod === 'Select Auth' ? theme.palette.general.tertiary : theme.palette.general.primary, backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary, color: theme.palette.text.primary,
'&:hover': {
backgroundColor: theme.palette.general.disabled,
},
}} }}
> >
<Option value="Select Auth" disabled>Select Auth</Option> <Option value="Select Auth" disabled>Select Auth</Option>
<Option value="password">Password</Option> <Option value="password">Password</Option>
<Option value="rsaKey">RSA Key</Option> <Option value="rsaKey">Public Key</Option>
</Select> </Select>
</FormControl> </FormControl>
{form.authMethod === 'password' && ( {form.authMethod === 'password' && (
<FormControl error={!form.password}> <FormControl error={!form.password}>
<FormLabel sx={{ color: theme.palette.text.primary }}>Password</FormLabel> <FormLabel>Password</FormLabel>
<div style={{ display: 'flex', alignItems: 'center' }}> <div style={{ display: 'flex', alignItems: 'center' }}>
<Input <Input
type={showPassword ? 'text' : 'password'} type={showPassword ? 'text' : 'password'}
value={form.password} value={form.password || ''}
onChange={(e) => setForm({ ...form, password: e.target.value })} onChange={(e) => setForm(prev => ({ ...prev, password: e.target.value }))}
required
sx={{ sx={{
backgroundColor: theme.palette.general.primary, backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary, color: theme.palette.text.primary,
flex: 1, flex: 1
}} }}
/> />
<IconButton <IconButton
onClick={() => setShowPassword(!showPassword)} onClick={() => setShowPassword(!showPassword)}
sx={{ sx={{
color: theme.palette.text.primary, color: theme.palette.text.primary,
marginLeft: 1, marginLeft: 1
}} }}
> >
{showPassword ? <VisibilityOff /> : <Visibility />} {showPassword ? <VisibilityOff /> : <Visibility />}
@@ -115,9 +124,26 @@ const NoAuthenticationModal = ({ isHidden, form, setForm, setIsNoAuthHidden, han
</div> </div>
</FormControl> </FormControl>
)} )}
{form.authMethod === 'rsaKey' && ( {form.authMethod === 'rsaKey' && (
<FormControl error={!form.rsaKey}> <FormControl error={!form.rsaKey}>
<FormLabel sx={{ color: theme.palette.text.primary }}>RSA Key</FormLabel> <FormLabel>Public Key</FormLabel>
<Button
component="label"
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '40px',
'&:hover': {
backgroundColor: theme.palette.general.disabled,
},
}}
>
{form.rsaKey ? 'Change Public Key File' : 'Upload Public Key File'}
<Input <Input
type="file" type="file"
onChange={(e) => { onChange={(e) => {
@@ -130,19 +156,12 @@ const NoAuthenticationModal = ({ isHidden, form, setForm, setIsNoAuthHidden, han
reader.readAsText(file); reader.readAsText(file);
} }
}} }}
required sx={{ display: 'none' }}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
padding: 1,
textAlign: 'center',
width: '100%',
minWidth: 'auto',
minHeight: 'auto',
}}
/> />
</Button>
</FormControl> </FormControl>
)} )}
<Button <Button
type="submit" type="submit"
disabled={!isFormValid()} disabled={!isFormValid()}
@@ -152,6 +171,12 @@ const NoAuthenticationModal = ({ isHidden, form, setForm, setIsNoAuthHidden, han
'&:hover': { '&:hover': {
backgroundColor: theme.palette.general.disabled, backgroundColor: theme.palette.general.disabled,
}, },
'&:disabled': {
backgroundColor: 'rgba(255, 255, 255, 0.1)',
color: 'rgba(255, 255, 255, 0.3)',
},
marginTop: 2,
height: '40px',
}} }}
> >
Connect Connect