From c910ecced81b06bd610480f055c6f5dd6c8f2260 Mon Sep 17 00:00:00 2001 From: Karmaa Date: Sun, 16 Mar 2025 19:29:34 -0500 Subject: [PATCH 01/47] Migrated MongoDB to version 4 to support RPI's and lower end processors. --- docker/Dockerfile | 2 +- docker/entrypoint.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 8db6b186..4637922b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -14,7 +14,7 @@ RUN npm install COPY src/backend/ ./src/backend/ # Stage 3: Final production image -FROM mongo:5 +FROM mongo:4.4 # Install Node.js RUN apt-get update && apt-get install -y \ curl \ diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index f372ddc8..99e7d3b2 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -7,7 +7,7 @@ mongod --fork --dbpath $MONGODB_DATA_DIR --logpath $MONGODB_LOG_DIR/mongodb.log # Wait for MongoDB to be ready echo "Waiting for MongoDB to start..." -until mongosh --eval "print(\"waited for connection\")" > /dev/null 2>&1; do +until mongo --eval "print(\"waited for connection\")" > /dev/null 2>&1; do sleep 0.5 done echo "MongoDB has started" -- 2.49.1 From 8f18cd14dd2149ebdc7e6db31644f71dba4235a1 Mon Sep 17 00:00:00 2001 From: Karmaa Date: Sun, 16 Mar 2025 19:30:41 -0500 Subject: [PATCH 02/47] Optimized Dockerfile to produce a smaller and faster build --- docker/Dockerfile | 63 ++++++++++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 4637922b..ca944504 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,53 +1,60 @@ # Stage 1: Build frontend -FROM --platform=$BUILDPLATFORM node:18 AS frontend-builder +FROM --platform=$BUILDPLATFORM node:18-alpine AS frontend-builder WORKDIR /app COPY package*.json ./ -RUN npm install +RUN npm ci --only=production COPY . . RUN npm run build # Stage 2: Build backend -FROM --platform=$BUILDPLATFORM node:18 AS backend-builder +FROM --platform=$BUILDPLATFORM node:18-alpine AS backend-builder WORKDIR /app COPY package*.json ./ -RUN npm install +RUN npm ci --only=production COPY src/backend/ ./src/backend/ -# Stage 3: Final production image -FROM mongo:4.4 -# Install Node.js -RUN apt-get update && apt-get install -y \ - curl \ - nginx \ - python3 \ - build-essential \ - && curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \ - && apt-get install -y nodejs \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* +# Stage 3: Build bcrypt for the target platform +FROM --platform=$TARGETPLATFORM node:18-alpine AS bcrypt-builder +WORKDIR /app +COPY package*.json ./ +RUN apk add --no-cache python3 make g++ \ + && npm ci --only=production bcrypt \ + && rm -rf /root/.npm -# Configure nginx +# Stage 4: Final production image +FROM mongo:4.4-focal +# Install Node.js and nginx, cleanup in the same layer +RUN apt-get update && apt-get install -y --no-install-recommends \ + nginx \ + curl \ + && curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \ + && apt-get install -y --no-install-recommends nodejs \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* \ + && rm -rf /var/cache/apt/* + +# Configure nginx and copy frontend COPY docker/nginx.conf /etc/nginx/nginx.conf COPY --from=frontend-builder /app/dist /usr/share/nginx/html -# Setup backend +# Setup backend with pre-built bcrypt WORKDIR /app COPY package*.json ./ -RUN npm install --omit=dev +RUN npm ci --only=production --ignore-scripts +COPY --from=bcrypt-builder /app/node_modules/bcrypt /app/node_modules/bcrypt COPY --from=backend-builder /app/src/backend ./src/backend -# Create directories for MongoDB and nginx -RUN mkdir -p /data/db && \ - mkdir -p /var/log/nginx && \ - mkdir -p /var/lib/nginx && \ - mkdir -p /var/log/mongodb && \ - chown -R mongodb:mongodb /data/db /var/log/mongodb && \ - chown -R www-data:www-data /var/log/nginx /var/lib/nginx +# Create necessary directories and set permissions +RUN mkdir -p /data/db /var/log/{nginx,mongodb} /var/lib/nginx \ + && chown -R mongodb:mongodb /data/db /var/log/mongodb \ + && chown -R www-data:www-data /var/log/nginx /var/lib/nginx \ + && rm -rf /root/.npm /tmp/* # Set environment variables ENV MONGO_URL=mongodb://localhost:27017/termix \ MONGODB_DATA_DIR=/data/db \ - MONGODB_LOG_DIR=/var/log/mongodb + MONGODB_LOG_DIR=/var/log/mongodb \ + NODE_ENV=production # Create volume for MongoDB data VOLUME ["/data/db"] @@ -55,7 +62,7 @@ VOLUME ["/data/db"] # Expose ports EXPOSE 8080 8081 8082 27017 -# Use a entrypoint script to run all services +# Copy and set entrypoint COPY docker/entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh CMD ["/entrypoint.sh"] \ No newline at end of file -- 2.49.1 From ce1ae3fb8392f49b44044af1e5a67840f991e31e Mon Sep 17 00:00:00 2001 From: Karmaa Date: Sun, 16 Mar 2025 19:38:03 -0500 Subject: [PATCH 03/47] Better auth key support and fix to dockerfile for mongoDB. --- docker/Dockerfile | 25 ++- src/backend/ssh.cjs | 32 +++- src/modals/AddHostModal.jsx | 139 +++++++++++----- src/modals/EditHostModal.jsx | 99 ++++++++++- src/modals/NoAuthenticationModal.jsx | 237 +++++++++++++++------------ 5 files changed, 369 insertions(+), 163 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index ca944504..b5153dc1 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -24,14 +24,23 @@ RUN apk add --no-cache python3 make g++ \ # Stage 4: Final production image FROM mongo:4.4-focal # Install Node.js and nginx, cleanup in the same layer -RUN apt-get update && apt-get install -y --no-install-recommends \ - nginx \ - curl \ - && curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \ - && apt-get install -y --no-install-recommends nodejs \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* \ - && rm -rf /var/cache/apt/* +RUN set -ex; \ + # Add MongoDB repository key + curl -fsSL https://pgp.mongodb.com/server-4.4.asc | gpg --dearmor -o /usr/share/keyrings/mongodb-archive-keyring.gpg; \ + echo "deb [signed-by=/usr/share/keyrings/mongodb-archive-keyring.gpg] http://repo.mongodb.org/apt/ubuntu focal/mongodb-org/4.4 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-4.4.list; \ + # Update and install packages + apt-get update && \ + apt-get install -y --no-install-recommends \ + nginx \ + curl \ + gnupg && \ + # Install Node.js + curl -fsSL https://deb.nodesource.com/setup_18.x | bash - && \ + apt-get install -y --no-install-recommends nodejs && \ + # Cleanup + apt-get clean && \ + rm -rf /var/lib/apt/lists/* && \ + rm -rf /var/cache/apt/* # Configure nginx and copy frontend COPY docker/nginx.conf /etc/nginx/nginx.conf diff --git a/src/backend/ssh.cjs b/src/backend/ssh.cjs index 0fd3ba16..af6b8c86 100644 --- a/src/backend/ssh.cjs +++ b/src/backend/ssh.cjs @@ -32,7 +32,7 @@ io.on("connection", (socket) => { return; } - if (!hostConfig.password && !hostConfig.rsaKey) { + if (!hostConfig.password && !hostConfig.privateKey) { logger.error("No authentication provided"); socket.emit("error", "Authentication required"); return; @@ -42,11 +42,12 @@ io.on("connection", (socket) => { ip: hostConfig.ip, port: hostConfig.port, user: hostConfig.user, - authType: hostConfig.password ? 'password' : 'public key', + authType: hostConfig.password ? 'password' : 'key', + keyType: hostConfig.keyType }; logger.info("Connecting with config:", safeHostConfig); - const { ip, port, user, password, rsaKey } = hostConfig; + const { ip, port, user, password, privateKey, passphrase } = hostConfig; const conn = new SSHClient(); conn @@ -98,7 +99,30 @@ io.on("connection", (socket) => { port: port, username: user, password: password, - privateKey: rsaKey ? Buffer.from(rsaKey) : undefined, + privateKey: privateKey ? Buffer.from(privateKey) : undefined, + passphrase: passphrase, + tryKeyboard: true, + algorithms: { + kex: [ + 'curve25519-sha256', + 'curve25519-sha256@libssh.org', + 'ecdh-sha2-nistp256', + 'ecdh-sha2-nistp384', + 'ecdh-sha2-nistp521', + 'diffie-hellman-group-exchange-sha256', + 'diffie-hellman-group14-sha256', + 'diffie-hellman-group14-sha1' + ], + serverHostKey: [ + 'ssh-ed25519', + 'ecdsa-sha2-nistp256', + 'ecdsa-sha2-nistp384', + 'ecdsa-sha2-nistp521', + 'rsa-sha2-512', + 'rsa-sha2-256', + 'ssh-rsa' + ] + } }); }); diff --git a/src/modals/AddHostModal.jsx b/src/modals/AddHostModal.jsx index 01880945..d0110386 100644 --- a/src/modals/AddHostModal.jsx +++ b/src/modals/AddHostModal.jsx @@ -26,20 +26,52 @@ import VisibilityOff from '@mui/icons-material/VisibilityOff'; const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidden }) => { const [showPassword, setShowPassword] = useState(false); + const [showPassphrase, setShowPassphrase] = useState(false); const [activeTab, setActiveTab] = useState(0); const handleFileChange = (e) => { const file = e.target.files[0]; - if (file) { - if (file.name.endsWith('.key') || file.name.endsWith('.pem') || file.name.endsWith('.pub')) { - const reader = new FileReader(); - reader.onload = (event) => { - setForm({ ...form, rsaKey: event.target.result }); - }; - reader.readAsText(file); - } else { - alert("Please upload a valid public key file."); - } + const supportedKeyTypes = { + 'id_rsa': 'RSA', + 'id_ed25519': 'ED25519', + 'id_ecdsa': 'ECDSA', + 'id_dsa': 'DSA', + '.pem': 'PEM', + '.key': 'KEY', + '.ppk': 'PPK' + }; + + const isValidKeyFile = Object.keys(supportedKeyTypes).some(ext => + file.name.toLowerCase().includes(ext) || file.name.endsWith('.pub') + ); + + if (isValidKeyFile) { + const reader = new FileReader(); + reader.onload = (event) => { + const keyContent = event.target.result; + let keyType = 'UNKNOWN'; + + // Detect key type from content + if (keyContent.includes('BEGIN RSA PRIVATE KEY') || keyContent.includes('BEGIN RSA PUBLIC KEY')) { + keyType = 'RSA'; + } else if (keyContent.includes('BEGIN OPENSSH PRIVATE KEY') && keyContent.includes('ssh-ed25519')) { + keyType = 'ED25519'; + } else if (keyContent.includes('BEGIN EC PRIVATE KEY') || keyContent.includes('BEGIN EC PUBLIC KEY')) { + keyType = 'ECDSA'; + } else if (keyContent.includes('BEGIN DSA PRIVATE KEY')) { + keyType = 'DSA'; + } + + setForm({ + ...form, + privateKey: keyContent, + keyType: keyType, + authMethod: 'key' + }); + }; + reader.readAsText(file); + } else { + alert('Please upload a valid SSH key file (RSA, ED25519, ECDSA, DSA, PEM, or PPK format).'); } }; @@ -48,7 +80,9 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd ...prev, authMethod: newMethod, password: "", - rsaKey: "" + privateKey: "", + keyType: "", + passphrase: "" })); }; @@ -59,7 +93,7 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd if (form.rememberHost) { if (form.authMethod === 'Select Auth') return false; - if (form.authMethod === 'rsaKey' && !form.rsaKey) return false; + if (form.authMethod === 'key' && !form.privateKey) return false; if (form.authMethod === 'password' && !form.password) return false; } @@ -81,7 +115,8 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd ip: '', user: '', password: '', - rsaKey: '', + privateKey: '', + keyType: '', port: 22, authMethod: 'Select Auth', rememberHost: false, @@ -241,7 +276,9 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd ...((!e.target.checked) && { authMethod: 'Select Auth', password: '', - rsaKey: '', + privateKey: '', + keyType: '', + passphrase: '', storePassword: true }) })} @@ -280,7 +317,7 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd > - + @@ -311,32 +348,49 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd )} - {form.authMethod === 'rsaKey' && ( - - Public Key - - + {form.authMethod === 'key' && ( + + + SSH Key + + + {form.privateKey && ( + + Key Passphrase (optional) + setForm(prev => ({ ...prev, passphrase: e.target.value }))} + endDecorator={ + setShowPassphrase(!showPassphrase)}> + {showPassphrase ? : } + + } + /> + + )} + )} )} @@ -380,7 +434,8 @@ AddHostModal.propTypes = { ip: PropTypes.string.isRequired, user: PropTypes.string.isRequired, password: PropTypes.string, - rsaKey: PropTypes.string, + privateKey: PropTypes.string, + keyType: PropTypes.string, port: PropTypes.number.isRequired, authMethod: PropTypes.string.isRequired, rememberHost: PropTypes.bool, diff --git a/src/modals/EditHostModal.jsx b/src/modals/EditHostModal.jsx index 048fb12d..623b0529 100644 --- a/src/modals/EditHostModal.jsx +++ b/src/modals/EditHostModal.jsx @@ -28,6 +28,7 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH const [showPassword, setShowPassword] = useState(false); const [activeTab, setActiveTab] = useState(0); const [isLoading, setIsLoading] = useState(false); + const [showPassphrase, setShowPassphrase] = useState(false); useEffect(() => { if (!isHidden && hostConfig) { @@ -48,14 +49,47 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH const handleFileChange = (e) => { const file = e.target.files[0]; - if (file.name.endsWith('.rsa') || file.name.endsWith('.key') || file.name.endsWith('.pem') || file.name.endsWith('.der') || file.name.endsWith('.p8') || file.name.endsWith('.ssh') || file.name.endsWith('.pub')) { + const supportedKeyTypes = { + 'id_rsa': 'RSA', + 'id_ed25519': 'ED25519', + 'id_ecdsa': 'ECDSA', + 'id_dsa': 'DSA', + '.pem': 'PEM', + '.key': 'KEY', + '.ppk': 'PPK' + }; + + const isValidKeyFile = Object.keys(supportedKeyTypes).some(ext => + file.name.toLowerCase().includes(ext) || file.name.endsWith('.pub') + ); + + if (isValidKeyFile) { const reader = new FileReader(); reader.onload = (evt) => { - setForm((prev) => ({ ...prev, rsaKey: evt.target.result })); + const keyContent = evt.target.result; + let keyType = 'UNKNOWN'; + + // Detect key type from content + if (keyContent.includes('BEGIN RSA PRIVATE KEY') || keyContent.includes('BEGIN RSA PUBLIC KEY')) { + keyType = 'RSA'; + } else if (keyContent.includes('BEGIN OPENSSH PRIVATE KEY') && keyContent.includes('ssh-ed25519')) { + keyType = 'ED25519'; + } else if (keyContent.includes('BEGIN EC PRIVATE KEY') || keyContent.includes('BEGIN EC PUBLIC KEY')) { + keyType = 'ECDSA'; + } else if (keyContent.includes('BEGIN DSA PRIVATE KEY')) { + keyType = 'DSA'; + } + + setForm((prev) => ({ + ...prev, + privateKey: keyContent, + keyType: keyType, + authMethod: 'key' + })); }; reader.readAsText(file); } else { - alert('Please upload a valid RSA private key file.'); + alert('Please upload a valid SSH key file (RSA, ED25519, ECDSA, DSA, PEM, or PPK format).'); } }; @@ -281,6 +315,7 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH + )} @@ -352,6 +387,64 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH )} )} + + {form.authMethod === 'key' && form.storePassword && ( + + + SSH Key + + {hostConfig?.privateKey && !form.privateKey && ( + + Existing {hostConfig.keyType || 'SSH'} key detected. Upload to replace. + + )} + + {form.privateKey && ( + + Key Passphrase (optional) + setForm(prev => ({ ...prev, passphrase: e.target.value }))} + endDecorator={ + setShowPassphrase(!showPassphrase)}> + {showPassphrase ? : } + + } + /> + + )} + + )} diff --git a/src/modals/NoAuthenticationModal.jsx b/src/modals/NoAuthenticationModal.jsx index 6829aaf0..813f1e02 100644 --- a/src/modals/NoAuthenticationModal.jsx +++ b/src/modals/NoAuthenticationModal.jsx @@ -19,30 +19,72 @@ import { useState, useEffect } from 'react'; import Visibility from '@mui/icons-material/Visibility'; import VisibilityOff from '@mui/icons-material/VisibilityOff'; -const NoAuthenticationModal = ({ isHidden, form, setForm, setIsNoAuthHidden, handleAuthSubmit }) => { +const NoAuthenticationModal = ({ isHidden, setIsHidden, onAuthenticate }) => { + const [form, setForm] = useState({ + authMethod: 'Select Auth', + password: '', + privateKey: '', + keyType: '', + passphrase: '' + }); const [showPassword, setShowPassword] = useState(false); + const [showPassphrase, setShowPassphrase] = useState(false); - useEffect(() => { - if (!form.authMethod) { - setForm(prev => ({ - ...prev, - authMethod: 'Select Auth' - })); - } - }, []); - - const isFormValid = () => { - if (!form.authMethod || form.authMethod === 'Select Auth') return false; - if (form.authMethod === 'rsaKey' && !form.rsaKey) return false; - if (form.authMethod === 'password' && !form.password) return false; - return true; + const handleSubmit = (e) => { + e.preventDefault(); + onAuthenticate({ + authMethod: form.authMethod, + password: form.password, + privateKey: form.privateKey, + keyType: form.keyType, + passphrase: form.passphrase + }); + setIsHidden(true); }; - const handleSubmit = (event) => { - event.preventDefault(); - if (isFormValid()) { - handleAuthSubmit(form); - setForm({ authMethod: 'Select Auth', password: '', rsaKey: '' }); + const handleFileChange = (e) => { + const file = e.target.files[0]; + const supportedKeyTypes = { + 'id_rsa': 'RSA', + 'id_ed25519': 'ED25519', + 'id_ecdsa': 'ECDSA', + 'id_dsa': 'DSA', + '.pem': 'PEM', + '.key': 'KEY', + '.ppk': 'PPK' + }; + + const isValidKeyFile = Object.keys(supportedKeyTypes).some(ext => + file.name.toLowerCase().includes(ext) || file.name.endsWith('.pub') + ); + + if (isValidKeyFile) { + const reader = new FileReader(); + reader.onload = (event) => { + const keyContent = event.target.result; + let keyType = 'UNKNOWN'; + + // Detect key type from content + if (keyContent.includes('BEGIN RSA PRIVATE KEY') || keyContent.includes('BEGIN RSA PUBLIC KEY')) { + keyType = 'RSA'; + } else if (keyContent.includes('BEGIN OPENSSH PRIVATE KEY') && keyContent.includes('ssh-ed25519')) { + keyType = 'ED25519'; + } else if (keyContent.includes('BEGIN EC PRIVATE KEY') || keyContent.includes('BEGIN EC PUBLIC KEY')) { + keyType = 'ECDSA'; + } else if (keyContent.includes('BEGIN DSA PRIVATE KEY')) { + keyType = 'DSA'; + } + + setForm({ + ...form, + privateKey: keyContent, + keyType: keyType, + authMethod: 'key' + }); + }; + reader.readAsText(file); + } else { + alert('Please upload a valid SSH key file (RSA, ED25519, ECDSA, DSA, PEM, or PPK format).'); } }; @@ -50,31 +92,12 @@ const NoAuthenticationModal = ({ isHidden, form, setForm, setIsNoAuthHidden, han { - if (reason !== 'backdropClick') { - setIsNoAuthHidden(true); - } - }} - sx={{ - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - }} + onClose={() => setIsHidden(true)} > Authentication Required @@ -84,8 +107,15 @@ const NoAuthenticationModal = ({ isHidden, form, setForm, setIsNoAuthHidden, han Authentication Method {form.authMethod === 'password' && ( Password -
- setForm(prev => ({ ...prev, password: e.target.value }))} - sx={{ - backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary, - flex: 1 - }} - /> - setShowPassword(!showPassword)} - sx={{ - color: theme.palette.text.primary, - marginLeft: 1 - }} - > - {showPassword ? : } - -
+ setForm({ ...form, password: e.target.value })} + endDecorator={ + setShowPassword(!showPassword)}> + {showPassword ? : } + + } + />
)} - {form.authMethod === 'rsaKey' && ( - - Public Key - - + > + {form.privateKey ? `Change ${form.keyType || 'SSH'} Key File` : 'Upload SSH Key File'} + + + + {form.privateKey && ( + + Key Passphrase (optional) + setForm(prev => ({ ...prev, passphrase: e.target.value }))} + endDecorator={ + setShowPassphrase(!showPassphrase)}> + {showPassphrase ? : } + + } + /> + + )} + )} diff --git a/src/modals/ProfileModal.jsx b/src/modals/ProfileModal.jsx index 4b69d391..177a23ad 100644 --- a/src/modals/ProfileModal.jsx +++ b/src/modals/ProfileModal.jsx @@ -71,6 +71,10 @@ export default function ProfileModal({ > Delete Account + +
+ v2.0.1 +
-- 2.49.1 From 0a50d5c85c697663bf05a12501468c745062fdb3 Mon Sep 17 00:00:00 2001 From: Karmaa Date: Mon, 17 Mar 2025 23:38:43 -0500 Subject: [PATCH 30/47] Improved and fixed editing and adding host UI --- src/App.jsx | 84 +++-- src/apps/user/User.jsx | 1 - src/modals/AddHostModal.jsx | 582 +++++++++++++++++------------------ src/modals/EditHostModal.jsx | 553 ++++++++++++++++----------------- 4 files changed, 597 insertions(+), 623 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index b15ae738..6bb46c5d 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -222,13 +222,12 @@ function App() { }, []); const handleAddHost = () => { - if (addHostForm.ip && addHostForm.user && addHostForm.port) { - if (!addHostForm.rememberHost) { - connectToHost(); - setIsAddHostHidden(true); - return; - } + if (!addHostForm.ip?.trim() || !addHostForm.user?.trim() || !addHostForm.port) { + alert("Please fill out all required fields (IP, User, Port)."); + return; + } + if (addHostForm.rememberHost) { if (addHostForm.authMethod === 'Select Auth') { alert("Please select an authentication method."); return; @@ -237,33 +236,42 @@ function App() { setIsNoAuthHidden(false); return; } - if (addHostForm.authMethod === 'rsaKey' && !addHostForm.rsaKey) { + if (addHostForm.authMethod === 'key' && !addHostForm.privateKey) { setIsNoAuthHidden(false); return; } + } - connectToHost(); + connectToHost(); + if (addHostForm.rememberHost) { if (!addHostForm.storePassword) { addHostForm.password = ''; + addHostForm.privateKey = ''; } handleSaveHost(); - setIsAddHostHidden(true); - } else { - alert("Please fill out all required fields (IP, User, Port)."); } + setIsAddHostHidden(true); }; const connectToHost = () => { const hostConfig = { name: addHostForm.name || '', folder: addHostForm.folder || '', - ip: addHostForm.ip, - user: addHostForm.user, + ip: addHostForm.ip.trim(), + user: addHostForm.user.trim(), port: String(addHostForm.port), - password: addHostForm.rememberHost && addHostForm.authMethod === 'password' ? addHostForm.password : undefined, - rsaKey: addHostForm.rememberHost && addHostForm.authMethod === 'rsaKey' ? addHostForm.rsaKey : undefined, }; + if (addHostForm.rememberHost && addHostForm.storePassword) { + if (addHostForm.authMethod === 'password') { + hostConfig.password = addHostForm.password; + } else if (addHostForm.authMethod === 'key') { + hostConfig.privateKey = addHostForm.privateKey; + hostConfig.keyType = addHostForm.keyType; + hostConfig.passphrase = addHostForm.passphrase; + } + } + const newTerminal = { id: nextId, title: hostConfig.name || hostConfig.ip, @@ -273,9 +281,21 @@ function App() { setTerminals([...terminals, newTerminal]); setActiveTab(nextId); setNextId(nextId + 1); - setIsAddHostHidden(true); - setAddHostForm({ name: "", folder: "", ip: "", user: "", password: "", rsaKey: "", port: 22, authMethod: "Select Auth", rememberHost: false, storePassword: true }); - } + setAddHostForm({ + name: "", + folder: "", + ip: "", + user: "", + password: "", + privateKey: "", + keyType: "", + passphrase: "", + port: 22, + authMethod: "Select Auth", + rememberHost: true, + storePassword: true + }); + }; const handleAuthSubmit = (form) => { const updatedTerminals = terminals.map((terminal) => { @@ -327,21 +347,30 @@ function App() { } const handleSaveHost = () => { - let hostConfig = { + const hostConfig = { name: addHostForm.name || addHostForm.ip, folder: addHostForm.folder, - ip: addHostForm.ip, - user: addHostForm.user, - password: addHostForm.authMethod === 'password' ? addHostForm.password : undefined, - rsaKey: addHostForm.authMethod === 'rsaKey' ? addHostForm.rsaKey : undefined, + ip: addHostForm.ip.trim(), + user: addHostForm.user.trim(), port: String(addHostForm.port), + }; + + if (addHostForm.storePassword) { + if (addHostForm.authMethod === 'password') { + hostConfig.password = addHostForm.password; + } else if (addHostForm.authMethod === 'key') { + hostConfig.privateKey = addHostForm.privateKey; + hostConfig.keyType = addHostForm.keyType; + hostConfig.passphrase = addHostForm.passphrase; + } } + if (userRef.current) { userRef.current.saveHost({ hostConfig, }); } - } + }; const handleLoginUser = ({ username, password, sessionToken, onSuccess, onFailure }) => { if (userRef.current) { @@ -472,7 +501,6 @@ function App() { updateEditHostForm(oldConfig); } catch (error) { - console.error('Edit failed:', error); setErrorMessage(`Edit failed: ${error}`); setIsErrorHidden(false); setIsEditing(false); @@ -658,10 +686,10 @@ function App() { )} @@ -681,7 +709,7 @@ function App() { setForm={setEditHostForm} handleEditHost={handleEditHost} setIsEditHostHidden={setIsEditHostHidden} - hostConfig={currentHostConfig} + hostConfig={currentHostConfig || {}} /> { socketRef.current.emit("editHost", { userId: currentUser.current.id, diff --git a/src/modals/AddHostModal.jsx b/src/modals/AddHostModal.jsx index ed21a4b7..03043b11 100644 --- a/src/modals/AddHostModal.jsx +++ b/src/modals/AddHostModal.jsx @@ -24,20 +24,7 @@ import { useState } from 'react'; import Visibility from '@mui/icons-material/Visibility'; import VisibilityOff from '@mui/icons-material/VisibilityOff'; -const AddHostModal = ({ isHidden, setIsAddHostHidden, handleAddHost }) => { - const [form, setForm] = useState({ - name: '', - folder: '', - ip: '', - user: '', - port: 22, - password: '', - privateKey: '', - keyType: '', - passphrase: '', - authMethod: 'Select Auth', - rememberHost: true - }); +const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidden }) => { const [showPassword, setShowPassword] = useState(false); const [showPassphrase, setShowPassphrase] = useState(false); const [activeTab, setActiveTab] = useState(0); @@ -100,14 +87,23 @@ const AddHostModal = ({ isHidden, setIsAddHostHidden, handleAddHost }) => { }; const isFormValid = () => { - if (!form.ip || !form.user || !form.port) return false; - const portNum = Number(form.port); + const { ip, user, port, authMethod, password, privateKey } = form; + + // Basic validation for required fields + if (!ip?.trim() || !user?.trim() || !port) return false; + + // Port validation + const portNum = Number(port); if (isNaN(portNum) || portNum < 1 || portNum > 65535) return false; + // If not remembering host, only basic fields are required + if (!form.rememberHost) return true; + + // Auth method validation only if remembering host if (form.rememberHost) { - if (form.authMethod === 'Select Auth') return false; - if (form.authMethod === 'key' && !form.privateKey) return false; - if (form.authMethod === 'password' && !form.password) return false; + if (authMethod === 'Select Auth') return false; + if (authMethod === 'password' && !password?.trim()) return false; + if (authMethod === 'key' && !privateKey?.trim()) return false; } return true; @@ -115,28 +111,11 @@ const AddHostModal = ({ isHidden, setIsAddHostHidden, handleAddHost }) => { const handleSubmit = (event) => { event.preventDefault(); - if (isFormValid()) { - if (!form.rememberHost) { - handleAddHost(); - } else { - handleAddHost(); - } - - setForm({ - name: '', - folder: '', - ip: '', - user: '', - password: '', - privateKey: '', - keyType: '', - port: 22, - authMethod: 'Select Auth', - rememberHost: false, - storePassword: true, - }); - setIsAddHostHidden(true); + if (!form.ip?.trim() || !form.user?.trim() || !form.port) { + alert("Please fill out all required fields (IP, User, Port)."); + return; } + handleAddHost(); }; return ( @@ -146,15 +125,18 @@ const AddHostModal = ({ isHidden, setIsAddHostHidden, handleAddHost }) => { display: 'flex', justifyContent: 'center', alignItems: 'center', + backdropFilter: 'blur(5px)', + backgroundColor: 'rgba(0, 0, 0, 0.2)', }} > { mx: 2, }} > - Add Host - -
- setActiveTab(val)} - sx={{ - backgroundColor: theme.palette.general.disabled, - borderRadius: '8px', - padding: '8px', - marginBottom: '16px', - width: '100%', - }} - > - setActiveTab(val)} + sx={{ + width: '100%', + mb: 0, + backgroundColor: theme.palette.general.tertiary, + }} + > + - Basic Info - Connection - Authentication - + }, + }, + }} + > + Basic Info + Connection + Authentication + - - - - Host Name - setForm({ ...form, name: e.target.value })} - sx={{ - backgroundColor: theme.palette.general.primary, +
+ + + + Host Name + setForm({ ...form, name: e.target.value })} + sx={{ + backgroundColor: theme.palette.general.primary, + color: theme.palette.text.primary, + }} + /> + + + Folder + setForm({ ...form, folder: e.target.value })} + sx={{ + backgroundColor: theme.palette.general.primary, + color: theme.palette.text.primary, + }} + /> + + + Remember Host + setForm({ + ...form, + rememberHost: e.target.checked, + })} + sx={{ + color: theme.palette.text.primary, + '&.Mui-checked': { color: theme.palette.text.primary, - }} - /> - - - Folder - setForm({ ...form, folder: e.target.value })} - sx={{ - backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary, - }} - /> - - - + }, + }} + /> + + + - - - - Host IP - setForm({ ...form, ip: e.target.value })} - required - sx={{ - backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary, - }} - /> - - - Host User - setForm({ ...form, user: e.target.value })} - required - sx={{ - backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary, - }} - /> - - 65535}> - Host Port - setForm({ ...form, port: e.target.value })} - min={1} - max={65535} - required - sx={{ - backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary, - }} - /> - - - + + + + Host IP + setForm({ ...form, ip: e.target.value })} + required + sx={{ + backgroundColor: theme.palette.general.primary, + color: theme.palette.text.primary, + }} + /> + + + Host User + setForm({ ...form, user: e.target.value })} + required + sx={{ + backgroundColor: theme.palette.general.primary, + color: theme.palette.text.primary, + }} + /> + + 65535}> + Host Port + setForm({ ...form, port: e.target.value })} + min={1} + max={65535} + required + sx={{ + backgroundColor: theme.palette.general.primary, + color: theme.palette.text.primary, + }} + /> + + + - - + + + + Authentication Method + + + + {form.authMethod === 'password' && ( + + Password +
+ setForm({ ...form, password: e.target.value })} + sx={{ + backgroundColor: theme.palette.general.primary, + color: theme.palette.text.primary, + flex: 1 + }} + /> + setShowPassword(!showPassword)} + sx={{ + color: theme.palette.text.primary, + marginLeft: 1 + }} + > + {showPassword ? : } + +
+
+ )} + + {form.authMethod === 'key' && ( + + + SSH Key + + + {form.privateKey && ( + + Key Passphrase (optional) +
+ setForm(prev => ({ ...prev, passphrase: e.target.value }))} + sx={{ + backgroundColor: theme.palette.general.primary, + color: theme.palette.text.primary, + flex: 1 + }} + /> + setShowPassphrase(!showPassphrase)} + sx={{ + color: theme.palette.text.primary, + marginLeft: 1 + }} + > + {showPassphrase ? : } + +
+
+ )} +
+ )} + + {form.rememberHost && ( - Remember Host + Store Password setForm({ - ...form, - rememberHost: e.target.checked, - - ...((!e.target.checked) && { - authMethod: 'Select Auth', - password: '', - privateKey: '', - keyType: '', - passphrase: '', - storePassword: true - }) - })} + checked={Boolean(form.storePassword)} + onChange={(e) => setForm({ ...form, storePassword: e.target.checked })} sx={{ color: theme.palette.text.primary, '&.Mui-checked': { @@ -303,147 +387,31 @@ const AddHostModal = ({ isHidden, setIsAddHostHidden, handleAddHost }) => { }} /> - {form.rememberHost && ( - <> - - Store Password - setForm({ ...form, storePassword: e.target.checked })} - sx={{ - color: theme.palette.text.primary, - '&.Mui-checked': { - color: theme.palette.text.primary, - }, - }} - /> - - - Authentication Method - - + )} +
+
+
- {form.authMethod === 'password' && ( - - Password -
- setForm({ ...form, password: e.target.value })} - sx={{ - backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary, - flex: 1 - }} - /> - setShowPassword(!showPassword)} - sx={{ - color: theme.palette.text.primary, - marginLeft: 1 - }} - > - {showPassword ? : } - -
-
- )} - - {form.authMethod === 'key' && form.rememberHost && ( - - - SSH Key - - - {form.privateKey && ( - - Key Passphrase (optional) -
- setForm(prev => ({ ...prev, passphrase: e.target.value }))} - sx={{ - backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary, - flex: 1 - }} - /> - setShowPassphrase(!showPassphrase)} - sx={{ - color: theme.palette.text.primary, - marginLeft: 1 - }} - > - {showPassphrase ? : } - -
-
- )} -
- )} - - )} -
-
-
- - -
-
+ +
@@ -452,8 +420,10 @@ const AddHostModal = ({ isHidden, setIsAddHostHidden, handleAddHost }) => { AddHostModal.propTypes = { isHidden: PropTypes.bool.isRequired, - setIsAddHostHidden: PropTypes.func.isRequired, + form: PropTypes.object.isRequired, + setForm: PropTypes.func.isRequired, handleAddHost: PropTypes.func.isRequired, + setIsAddHostHidden: PropTypes.func.isRequired, }; export default AddHostModal; \ No newline at end of file diff --git a/src/modals/EditHostModal.jsx b/src/modals/EditHostModal.jsx index 80da2c2b..9608fbe4 100644 --- a/src/modals/EditHostModal.jsx +++ b/src/modals/EditHostModal.jsx @@ -36,7 +36,8 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo keyType: hostConfig?.keyType || '', passphrase: '', authMethod: hostConfig?.authMethod || 'Select Auth', - storePassword: true + storePassword: true, + rememberHost: true }); const [showPassword, setShowPassword] = useState(false); const [showPassphrase, setShowPassphrase] = useState(false); @@ -51,11 +52,13 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo ip: hostConfig.ip || '', user: hostConfig.user || '', password: hostConfig.password || '', - rsaKey: hostConfig.rsaKey || '', + privateKey: hostConfig.privateKey || '', + keyType: hostConfig.keyType || '', + passphrase: hostConfig.passphrase || '', port: hostConfig.port || 22, - authMethod: hostConfig.password ? 'password' : hostConfig.rsaKey ? 'rsaKey' : 'Select Auth', + authMethod: hostConfig.password ? 'password' : hostConfig.privateKey ? 'key' : 'Select Auth', rememberHost: true, - storePassword: !!(hostConfig.password || hostConfig.rsaKey), + storePassword: !!(hostConfig.password || hostConfig.privateKey), }); } }, [isHidden, hostConfig]); @@ -118,20 +121,28 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo ...prev, storePassword: Boolean(checked), password: checked ? prev.password : "", - rsaKey: checked ? prev.rsaKey : "", + privateKey: checked ? prev.privateKey : "", + passphrase: checked ? prev.passphrase : "", authMethod: checked ? prev.authMethod : "Select Auth" })); }; const isFormValid = () => { - const { ip, user, port, authMethod, password, rsaKey, storePassword } = form; + const { ip, user, port, authMethod, password, privateKey } = form; + + // Basic validation for required fields if (!ip?.trim() || !user?.trim() || !port) return false; + + // Port validation const portNum = Number(port); if (isNaN(portNum) || portNum < 1 || portNum > 65535) return false; - if (Boolean(storePassword) && authMethod === 'password' && !password?.trim()) return false; - if (Boolean(storePassword) && authMethod === 'rsaKey' && !rsaKey && !hostConfig?.rsaKey) return false; - if (Boolean(storePassword) && authMethod === 'Select Auth') return false; + // Auth method validation only if storing password + if (form.storePassword) { + if (authMethod === 'Select Auth') return false; + if (authMethod === 'password' && !password?.trim()) return false; + if (authMethod === 'key' && !privateKey?.trim()) return false; + } return true; }; @@ -142,15 +153,25 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo setIsLoading(true); try { - await handleEditHost(hostConfig, { + const newConfig = { 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), - }); + }; + + if (form.storePassword) { + if (form.authMethod === 'password') { + newConfig.password = form.password; + } else if (form.authMethod === 'key') { + newConfig.privateKey = form.privateKey; + newConfig.keyType = form.keyType; + newConfig.passphrase = form.passphrase; + } + } + + await handleEditHost(hostConfig, newConfig); } finally { setIsLoading(false); } @@ -162,11 +183,9 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo open={!isHidden} onClose={() => !isLoading && setIsEditHostHidden(true)} sx={{ - position: 'fixed', - inset: 0, display: 'flex', - alignItems: 'center', justifyContent: 'center', + alignItems: 'center', backdropFilter: 'blur(5px)', backgroundColor: 'rgba(0, 0, 0, 0.2)', }} @@ -178,7 +197,7 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo backgroundColor: theme.palette.general.tertiary, borderColor: theme.palette.general.secondary, color: theme.palette.text.primary, - padding: 3, + padding: 0, borderRadius: 10, maxWidth: '500px', width: '100%', @@ -188,134 +207,133 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo mx: 2, }} > - Edit Host - -
- setActiveTab(val)} - sx={{ - backgroundColor: theme.palette.general.disabled, - borderRadius: '8px', - padding: '8px', - marginBottom: '16px', - width: '100%', - }} - > - setActiveTab(val)} + sx={{ + width: '100%', + mb: 0, + backgroundColor: theme.palette.general.tertiary, + }} + > + - Basic Info - Connection - Authentication - + }, + }, + }} + > + Basic Info + Connection + Authentication + - - - - Host Name - setForm((prev) => ({ ...prev, name: e.target.value }))} - sx={{ - backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary - }} - /> - +
+ + + + Host Name + setForm((prev) => ({ ...prev, name: e.target.value }))} + sx={{ + backgroundColor: theme.palette.general.primary, + color: theme.palette.text.primary + }} + /> + - - Folder - setForm((prev) => ({ ...prev, folder: e.target.value }))} - sx={{ - backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary - }} - /> - - - + + Folder + setForm((prev) => ({ ...prev, folder: e.target.value }))} + sx={{ + backgroundColor: theme.palette.general.primary, + color: theme.palette.text.primary + }} + /> + + + - - - - Host IP - setForm((prev) => ({ ...prev, ip: e.target.value }))} - sx={{ - backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary - }} - /> - + + + + Host IP + setForm((prev) => ({ ...prev, ip: e.target.value }))} + sx={{ + backgroundColor: theme.palette.general.primary, + color: theme.palette.text.primary + }} + /> + - 65535}> - Host Port - setForm((prev) => ({ ...prev, port: e.target.value }))} - sx={{ - backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary - }} - /> - + 65535}> + Host Port + setForm((prev) => ({ ...prev, port: e.target.value }))} + sx={{ + backgroundColor: theme.palette.general.primary, + color: theme.palette.text.primary + }} + /> + - - Host User - setForm((prev) => ({ ...prev, user: e.target.value }))} - sx={{ - backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary - }} - /> - - - + + Host User + setForm((prev) => ({ ...prev, user: e.target.value }))} + sx={{ + backgroundColor: theme.palette.general.primary, + color: theme.palette.text.primary + }} + /> + + + - - - - Store Password - handleStorePasswordChange(e.target.checked)} - sx={{ + + + + Store Password + handleStorePasswordChange(e.target.checked)} + sx={{ + color: theme.palette.text.primary, + '&.Mui-checked': { color: theme.palette.text.primary, - '&.Mui-checked': { - color: theme.palette.text.primary - } - }} - /> - + }, + }} + /> + - {form.storePassword && ( - + {form.storePassword && ( + <> + Authentication Method - )} - {form.authMethod === 'password' && form.storePassword && ( - - Password -
- setForm((prev) => ({ ...prev, password: e.target.value }))} - sx={{ - backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary, - flex: 1 - }} - /> - setShowPassword(!showPassword)} - sx={{ - color: theme.palette.text.primary, - marginLeft: 1 - }} - > - {showPassword ? : } - -
-
- )} - - {form.authMethod === 'rsaKey' && form.storePassword && ( - - Public Key - - {hostConfig?.rsaKey && !form.rsaKey && ( - - Existing key detected. Upload to replace. - - )} - - )} - - {form.authMethod === 'key' && form.storePassword && ( - - - SSH Key - - {hostConfig?.privateKey && !form.privateKey && ( - setShowPassword(!showPassword)} + sx={{ + color: theme.palette.text.primary, + marginLeft: 1 }} > - Existing {hostConfig.keyType || 'SSH'} key detected. Upload to replace. - - )} + {showPassword ? : } + +
- {form.privateKey && ( - - Key Passphrase (optional) -
+ )} + + {form.authMethod === 'key' && ( + + + SSH Key + + {hostConfig?.privateKey && !form.privateKey && ( + - {showPassphrase ? : } - -
+ Existing {hostConfig.keyType || 'SSH'} key detected. Upload to replace. + + )}
- )} -
- )} - -
-
+ {form.privateKey && ( + + Key Passphrase (optional) +
+ setForm(prev => ({ ...prev, passphrase: e.target.value }))} + sx={{ + backgroundColor: theme.palette.general.primary, + color: theme.palette.text.primary, + flex: 1 + }} + /> + setShowPassphrase(!showPassphrase)} + sx={{ + color: theme.palette.text.primary, + marginLeft: 1 + }} + > + {showPassphrase ? : } + +
+
+ )} + + )} + + )} + + + - -
-
+ + -- 2.49.1 From b5af67bdd66aba6b9c79300e95b16cab0b79d913 Mon Sep 17 00:00:00 2001 From: Karmaa Date: Wed, 19 Mar 2025 22:02:44 -0500 Subject: [PATCH 31/47] Improved add host UI --- src/App.jsx | 122 +++++++++---------------- src/modals/AddHostModal.jsx | 3 +- src/modals/AuthModal.jsx | 174 ++++++++++++++++++++++++++++++++++++ 3 files changed, 219 insertions(+), 80 deletions(-) create mode 100644 src/modals/AuthModal.jsx diff --git a/src/App.jsx b/src/App.jsx index 6bb46c5d..2a7ed548 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -2,7 +2,7 @@ import { useState, useEffect, useRef } from "react"; import { NewTerminal } from "./apps/ssh/Terminal.jsx"; import { User } from "./apps/user/User.jsx"; import AddHostModal from "./modals/AddHostModal.jsx"; -import LoginUserModal from "./modals/LoginUserModal.jsx"; +import AuthModal from "./modals/AuthModal.jsx"; import { Button } from "@mui/joy"; import { CssVarsProvider } from "@mui/joy"; import theme from "./theme"; @@ -12,7 +12,6 @@ import { Debounce } from './other/Utils.jsx'; import TermixIcon from "./images/termix_icon.png"; import RocketIcon from './images/launchpad_rocket.png'; import ProfileIcon from './images/profile_icon.png'; -import CreateUserModal from "./modals/CreateUserModal.jsx"; import ProfileModal from "./modals/ProfileModal.jsx"; import ErrorModal from "./modals/ErrorModal.jsx"; import EditHostModal from "./modals/EditHostModal.jsx"; @@ -20,8 +19,7 @@ import NoAuthenticationModal from "./modals/NoAuthenticationModal.jsx"; function App() { const [isAddHostHidden, setIsAddHostHidden] = useState(true); - const [isLoginUserHidden, setIsLoginUserHidden] = useState(true); - const [isCreateUserHidden, setIsCreateUserHidden] = useState(true); + const [isAuthModalHidden, setIsAuthModalHidden] = useState(true); const [isProfileHidden, setIsProfileHidden] = useState(true); const [isErrorHidden, setIsErrorHidden] = useState(true); const [errorMessage, setErrorMessage] = useState(''); @@ -37,7 +35,7 @@ function App() { password: "", port: 22, authMethod: "Select Auth", - rememberHost: false, + rememberHost: true, storePassword: true, }); const [editHostForm, setEditHostForm] = useState({ @@ -53,14 +51,6 @@ function App() { }); const [isNoAuthHidden, setIsNoAuthHidden] = useState(true); const [authForm, setAuthForm] = useState({ - password: "", - rsaKey: "", - }); - const [loginUserForm, setLoginUserForm] = useState({ - username: "", - password: "", - }); - const [createUserForm, setCreateUserForm] = useState({ username: "", password: "", }); @@ -133,13 +123,13 @@ function App() { if (userRef.current?.getUser()) { setIsLoggingIn(false); - setIsLoginUserHidden(true); + setIsAuthModalHidden(true); return; } if (!sessionToken) { setIsLoggingIn(false); - setIsLoginUserHidden(false); + setIsAuthModalHidden(false); return; } @@ -153,7 +143,7 @@ function App() { clearInterval(attemptLoginInterval); if (!userRef.current?.getUser()) { localStorage.removeItem('sessionToken'); - setIsLoginUserHidden(false); + setIsAuthModalHidden(false); setIsLoggingIn(false); setErrorMessage('Login timed out. Please try again.'); setIsErrorHidden(false); @@ -170,7 +160,7 @@ function App() { if (!userRef.current?.getUser()) { localStorage.removeItem('sessionToken'); - setIsLoginUserHidden(false); + setIsAuthModalHidden(false); setIsLoggingIn(false); setErrorMessage('Login timed out. Please try again.'); setIsErrorHidden(false); @@ -186,7 +176,7 @@ function App() { if (isComponentMounted) { clearTimeout(loginTimeout); clearInterval(attemptLoginInterval); - setIsLoginUserHidden(true); + setIsAuthModalHidden(true); setIsLoggingIn(false); setIsErrorHidden(true); } @@ -200,7 +190,7 @@ function App() { localStorage.removeItem('sessionToken'); setErrorMessage(`Auto-login failed: ${error}`); setIsErrorHidden(false); - setIsLoginUserHidden(false); + setIsAuthModalHidden(false); setIsLoggingIn(false); } } @@ -372,39 +362,20 @@ function App() { } }; - const handleLoginUser = ({ username, password, sessionToken, onSuccess, onFailure }) => { + const handleLoginUser = ({ username, password, onSuccess, onFailure }) => { if (userRef.current) { - if (sessionToken) { - userRef.current.loginUser({ - sessionToken, - onSuccess: () => { - setIsLoginUserHidden(true); - setIsLoggingIn(false); - if (onSuccess) onSuccess(); - }, - onFailure: (error) => { - localStorage.removeItem('sessionToken'); - setIsLoginUserHidden(false); - setIsLoggingIn(false); - if (onFailure) onFailure(error); - }, - }); - } else { - userRef.current.loginUser({ - username, - password, - onSuccess: () => { - setIsLoginUserHidden(true); - setIsLoggingIn(false); - if (onSuccess) onSuccess(); - }, - onFailure: (error) => { - setIsLoginUserHidden(false); - setIsLoggingIn(false); - if (onFailure) onFailure(error); - }, - }); - } + userRef.current.loginUser({ + username, + password, + onSuccess: () => { + setIsAuthModalHidden(true); + if (onSuccess) onSuccess(); + }, + onFailure: (error) => { + setIsAuthModalHidden(false); + if (onFailure) onFailure(error); + }, + }); } }; @@ -412,24 +383,28 @@ function App() { if (userRef.current) { try { await userRef.current.loginAsGuest(); - setIsLoginUserHidden(true); - setIsLoggingIn(false); + setIsAuthModalHidden(true); if (onSuccess) onSuccess(); } catch (error) { - setIsLoginUserHidden(false); - setIsLoggingIn(false); + setIsAuthModalHidden(false); if (onFailure) onFailure(error); } } - } + }; const handleCreateUser = ({ username, password, onSuccess, onFailure }) => { if (userRef.current) { userRef.current.createUser({ username, password, - onSuccess, - onFailure, + onSuccess: () => { + setIsAuthModalHidden(true); + if (onSuccess) onSuccess(); + }, + onFailure: (error) => { + setIsAuthModalHidden(false); + if (onFailure) onFailure(error); + }, }); } }; @@ -622,7 +597,7 @@ function App() { {/* Profile Button */} + + + + + + + + Username + setForm({ ...form, username: event.target.value })} + disabled={isLoading} + /> + + + Password +
+ setForm({ ...form, password: event.target.value })} + disabled={isLoading} + /> + setShowPassword(!showPassword)}> + {showPassword ? : } + +
+
+ + Confirm Password + setConfirmPassword(event.target.value)} + disabled={isLoading} + /> + + +
+
+ + + + + + ); +}; + +AuthModal.propTypes = { + isHidden: PropTypes.bool.isRequired, + form: PropTypes.object.isRequired, + setForm: PropTypes.func.isRequired, + handleLoginUser: PropTypes.func.isRequired, + handleGuestLogin: PropTypes.func.isRequired, + handleCreateUser: PropTypes.func.isRequired, + setIsLoginUserHidden: PropTypes.func.isRequired, +}; + +export default AuthModal; \ No newline at end of file -- 2.49.1 From 28081ad6b280b933927879019bfb9ef61a2261cc Mon Sep 17 00:00:00 2001 From: Karmaa Date: Thu, 20 Mar 2025 20:41:45 -0500 Subject: [PATCH 32/47] Revamped login system, fixed no auth modal errors, changed many UI's. --- package-lock.json | 7 + package.json | 1 + src/App.jsx | 195 ++++++++--------- src/apps/ssh/Terminal.jsx | 10 +- src/apps/user/User.jsx | 2 +- src/backend/database.cjs | 4 +- src/backend/ssh.cjs | 5 +- src/modals/AddHostModal.jsx | 74 ++----- src/modals/AuthModal.jsx | 306 +++++++++++++++++++-------- src/modals/CreateUserModal.jsx | 207 ------------------ src/modals/EditHostModal.jsx | 71 ++----- src/modals/LoginUserModal.jsx | 199 ----------------- src/modals/NoAuthenticationModal.jsx | 150 +++++++------ 13 files changed, 458 insertions(+), 773 deletions(-) delete mode 100644 src/modals/CreateUserModal.jsx delete mode 100644 src/modals/LoginUserModal.jsx diff --git a/package-lock.json b/package-lock.json index 7615c7d0..d54d6ee6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "express": "^4.21.2", "is-stream": "^4.0.1", "make-dir": "^5.0.0", + "mitt": "^3.0.1", "mongoose": "^8.12.1", "node-ssh": "^13.2.0", "prop-types": "^15.8.1", @@ -6341,6 +6342,12 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "license": "ISC" }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", diff --git a/package.json b/package.json index 0ab6013a..5a14a389 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "express": "^4.21.2", "is-stream": "^4.0.1", "make-dir": "^5.0.0", + "mitt": "^3.0.1", "mongoose": "^8.12.1", "node-ssh": "^13.2.0", "prop-types": "^15.8.1", diff --git a/src/App.jsx b/src/App.jsx index 2a7ed548..6d3b3b2b 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -16,6 +16,7 @@ import ProfileModal from "./modals/ProfileModal.jsx"; import ErrorModal from "./modals/ErrorModal.jsx"; import EditHostModal from "./modals/EditHostModal.jsx"; import NoAuthenticationModal from "./modals/NoAuthenticationModal.jsx"; +import eventBus from "./other/eventBus.jsx"; function App() { const [isAddHostHidden, setIsAddHostHidden] = useState(true); @@ -33,6 +34,7 @@ function App() { ip: "", user: "", password: "", + sshKey: "", port: 22, authMethod: "Select Auth", rememberHost: true, @@ -44,6 +46,7 @@ function App() { ip: "", user: "", password: "", + sshKey: "", port: 22, authMethod: "Select Auth", rememberHost: true, @@ -51,9 +54,16 @@ function App() { }); const [isNoAuthHidden, setIsNoAuthHidden] = useState(true); const [authForm, setAuthForm] = useState({ - username: "", - password: "", + username: '', + password: '', + confirmPassword: '' }); + const [noAuthenticationForm, setNoAuthenticationForm] = useState({ + authMethod: 'Select Auth', + password: '', + sshKey: '', + keyType: '', + }) const [isLaunchpadOpen, setIsLaunchpadOpen] = useState(false); const [splitTabIds, setSplitTabIds] = useState([]); const [isEditHostHidden, setIsEditHostHidden] = useState(true); @@ -137,7 +147,7 @@ function App() { let loginAttempts = 0; const maxAttempts = 50; let attemptLoginInterval; - + const loginTimeout = setTimeout(() => { if (isComponentMounted) { clearInterval(attemptLoginInterval); @@ -153,11 +163,11 @@ function App() { const attemptLogin = () => { if (!isComponentMounted || isLoginInProgress) return; - + if (loginAttempts >= maxAttempts || userRef.current?.getUser()) { clearTimeout(loginTimeout); clearInterval(attemptLoginInterval); - + if (!userRef.current?.getUser()) { localStorage.removeItem('sessionToken'); setIsAuthModalHidden(false); @@ -212,12 +222,13 @@ function App() { }, []); const handleAddHost = () => { - if (!addHostForm.ip?.trim() || !addHostForm.user?.trim() || !addHostForm.port) { - alert("Please fill out all required fields (IP, User, Port)."); - return; - } + if (addHostForm.ip && addHostForm.user && addHostForm.port) { + if (!addHostForm.rememberHost) { + connectToHost(); + setIsAddHostHidden(true); + return; + } - if (addHostForm.rememberHost) { if (addHostForm.authMethod === 'Select Auth') { alert("Please select an authentication method."); return; @@ -226,42 +237,33 @@ function App() { setIsNoAuthHidden(false); return; } - if (addHostForm.authMethod === 'key' && !addHostForm.privateKey) { + if (addHostForm.authMethod === 'sshKey' && !addHostForm.sshKey) { setIsNoAuthHidden(false); return; } - } - connectToHost(); - if (addHostForm.rememberHost) { + connectToHost(); if (!addHostForm.storePassword) { addHostForm.password = ''; - addHostForm.privateKey = ''; } handleSaveHost(); + setIsAddHostHidden(true); + } else { + alert("Please fill out all required fields (IP, User, Port)."); } - setIsAddHostHidden(true); }; const connectToHost = () => { const hostConfig = { name: addHostForm.name || '', folder: addHostForm.folder || '', - ip: addHostForm.ip.trim(), - user: addHostForm.user.trim(), + ip: addHostForm.ip, + user: addHostForm.user, port: String(addHostForm.port), + password: addHostForm.rememberHost && addHostForm.authMethod === 'password' ? addHostForm.password : undefined, + sshKey: addHostForm.rememberHost && addHostForm.authMethod === 'sshKey' ? addHostForm.sshKey : undefined, }; - if (addHostForm.rememberHost && addHostForm.storePassword) { - if (addHostForm.authMethod === 'password') { - hostConfig.password = addHostForm.password; - } else if (addHostForm.authMethod === 'key') { - hostConfig.privateKey = addHostForm.privateKey; - hostConfig.keyType = addHostForm.keyType; - hostConfig.passphrase = addHostForm.passphrase; - } - } - const newTerminal = { id: nextId, title: hostConfig.name || hostConfig.ip, @@ -271,21 +273,9 @@ function App() { setTerminals([...terminals, newTerminal]); setActiveTab(nextId); setNextId(nextId + 1); - setAddHostForm({ - name: "", - folder: "", - ip: "", - user: "", - password: "", - privateKey: "", - keyType: "", - passphrase: "", - port: 22, - authMethod: "Select Auth", - rememberHost: true, - storePassword: true - }); - }; + setIsAddHostHidden(true); + setAddHostForm({ name: "", folder: "", ip: "", user: "", password: "", sshKey: "", port: 22, authMethod: "Select Auth", rememberHost: true, storePassword: true }); + } const handleAuthSubmit = (form) => { const updatedTerminals = terminals.map((terminal) => { @@ -295,7 +285,7 @@ function App() { hostConfig: { ...terminal.hostConfig, password: form.password, - rsaKey: form.rsaKey + sshKey: form.sshKey } }; } @@ -321,7 +311,7 @@ function App() { user: hostConfig.user.trim(), port: hostConfig.port || '22', password: hostConfig.password?.trim(), - rsaKey: hostConfig.rsaKey?.trim(), + sshKey: hostConfig.sshKey?.trim(), }; const newTerminal = { @@ -337,74 +327,71 @@ function App() { } const handleSaveHost = () => { - const hostConfig = { + let hostConfig = { name: addHostForm.name || addHostForm.ip, folder: addHostForm.folder, - ip: addHostForm.ip.trim(), - user: addHostForm.user.trim(), + ip: addHostForm.ip, + user: addHostForm.user, + password: addHostForm.authMethod === 'password' ? addHostForm.password : undefined, + sshKey: addHostForm.authMethod === 'sshKey' ? addHostForm.sshKey : undefined, port: String(addHostForm.port), - }; - - if (addHostForm.storePassword) { - if (addHostForm.authMethod === 'password') { - hostConfig.password = addHostForm.password; - } else if (addHostForm.authMethod === 'key') { - hostConfig.privateKey = addHostForm.privateKey; - hostConfig.keyType = addHostForm.keyType; - hostConfig.passphrase = addHostForm.passphrase; - } } - if (userRef.current) { userRef.current.saveHost({ hostConfig, }); } - }; + } - const handleLoginUser = ({ username, password, onSuccess, onFailure }) => { + const handleLoginUser = ({ username, password, sessionToken, onSuccess, onFailure }) => { if (userRef.current) { - userRef.current.loginUser({ - username, - password, - onSuccess: () => { - setIsAuthModalHidden(true); - if (onSuccess) onSuccess(); - }, - onFailure: (error) => { - setIsAuthModalHidden(false); - if (onFailure) onFailure(error); - }, - }); - } - }; - - const handleGuestLogin = async ({ onSuccess, onFailure }) => { - if (userRef.current) { - try { - await userRef.current.loginAsGuest(); - setIsAuthModalHidden(true); - if (onSuccess) onSuccess(); - } catch (error) { - setIsAuthModalHidden(false); - if (onFailure) onFailure(error); + if (sessionToken) { + userRef.current.loginUser({ + sessionToken, + onSuccess: () => { + setIsAuthModalHidden(true); + setIsLoggingIn(false); + if (onSuccess) onSuccess(); + }, + onFailure: (error) => { + localStorage.removeItem('sessionToken'); + setIsAuthModalHidden(false); + setIsLoggingIn(false); + if (onFailure) onFailure(error); + }, + }); + } else { + userRef.current.loginUser({ + username, + password, + onSuccess: () => { + setIsAuthModalHidden(true); + setIsLoggingIn(false); + if (onSuccess) onSuccess(); + }, + onFailure: (error) => { + setIsAuthModalHidden(false); + setIsLoggingIn(false); + if (onFailure) onFailure(error); + }, + }); } } }; + const handleGuestLogin = () => { + if (userRef.current) { + userRef.current.loginAsGuest(); + } + } + const handleCreateUser = ({ username, password, onSuccess, onFailure }) => { if (userRef.current) { userRef.current.createUser({ username, password, - onSuccess: () => { - setIsAuthModalHidden(true); - if (onSuccess) onSuccess(); - }, - onFailure: (error) => { - setIsAuthModalHidden(false); - if (onFailure) onFailure(error); - }, + onSuccess, + onFailure, }); } }; @@ -459,7 +446,7 @@ function App() { if (newConfig) { if (isEditing) return; setIsEditing(true); - + try { await userRef.current.editHost({ oldHostConfig: oldConfig, @@ -476,6 +463,7 @@ function App() { updateEditHostForm(oldConfig); } catch (error) { + console.error('Edit failed:', error); setErrorMessage(`Edit failed: ${error}`); setIsErrorHidden(false); setIsEditing(false); @@ -661,10 +649,10 @@ function App() { )} @@ -684,7 +672,7 @@ function App() { setForm={setEditHostForm} handleEditHost={handleEditHost} setIsEditHostHidden={setIsEditHostHidden} - hostConfig={currentHostConfig || {}} + hostConfig={currentHostConfig} /> {/* User component */} @@ -737,8 +725,8 @@ function App() { }} onCreateSuccess={() => { setIsAuthModalHidden(true); - handleLoginUser({ - username: authForm.username, + handleLoginUser({ + username: authForm.username, password: authForm.password, onSuccess: () => { setIsAuthModalHidden(true); @@ -759,6 +747,7 @@ function App() { setErrorMessage(`Action failed: ${error}`); setIsErrorHidden(false); setIsLoggingIn(false); + eventBus.emit('failedLoginUser'); }} /> diff --git a/src/apps/ssh/Terminal.jsx b/src/apps/ssh/Terminal.jsx index b50f90f3..e96ae1df 100644 --- a/src/apps/ssh/Terminal.jsx +++ b/src/apps/ssh/Terminal.jsx @@ -76,7 +76,7 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde socket.on("error", (err) => { const isAuthError = err.toLowerCase().includes("authentication") || err.toLowerCase().includes("auth"); - if (isAuthError && !hostConfig.password?.trim() && !hostConfig.rsaKey?.trim() && !authModalShown) { + if (isAuthError && !hostConfig.password?.trim() && !hostConfig.sshKey?.trim() && !authModalShown) { authModalShown = true; setIsNoAuthHidden(false); } @@ -88,7 +88,7 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde resizeTerminal(); const { cols, rows } = terminalInstance.current; - if (!hostConfig.password?.trim() && !hostConfig.rsaKey?.trim()) { + if (!hostConfig.password?.trim() && !hostConfig.sshKey?.trim()) { setIsNoAuthHidden(false); return; } @@ -98,7 +98,7 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde user: hostConfig.user, port: Number(hostConfig.port) || 22, password: hostConfig.password?.trim(), - rsaKey: hostConfig.rsaKey?.trim() + sshKey: hostConfig.sshKey?.trim() }; socket.emit("connectToHost", cols, rows, sshConfig); @@ -172,7 +172,7 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde let authModalShown = false; socket.on("noAuthRequired", () => { - if (!hostConfig.password?.trim() && !hostConfig.rsaKey?.trim() && !authModalShown) { + if (!hostConfig.password?.trim() && !hostConfig.sshKey?.trim() && !authModalShown) { authModalShown = true; setIsNoAuthHidden(false); } @@ -235,7 +235,7 @@ NewTerminal.propTypes = { ip: PropTypes.string.isRequired, user: PropTypes.string.isRequired, password: PropTypes.string, - rsaKey: PropTypes.string, + sshKey: PropTypes.string, port: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, }).isRequired, isVisible: PropTypes.bool.isRequired, diff --git a/src/apps/user/User.jsx b/src/apps/user/User.jsx index bc8b88d8..ab22ec4f 100644 --- a/src/apps/user/User.jsx +++ b/src/apps/user/User.jsx @@ -186,7 +186,7 @@ export const User = forwardRef(({ onLoginSuccess, onCreateSuccess, onDeleteSucce user: host.config.user || '', port: host.config.port || '22', password: host.config.password || '', - rsaKey: host.config.rsaKey || '', + sshKey: host.config.sshKey || '', } : {} })).filter(host => host.config && host.config.ip && host.config.user); } else { diff --git a/src/backend/database.cjs b/src/backend/database.cjs index 1e453db3..6d99d81a 100644 --- a/src/backend/database.cjs +++ b/src/backend/database.cjs @@ -185,7 +185,7 @@ io.of('/database.io').on('connection', (socket) => { user: hostConfig.user.trim(), port: hostConfig.port || 22, password: hostConfig.password?.trim() || undefined, - rsaKey: hostConfig.rsaKey?.trim() || undefined + sshKey: hostConfig.sshKey?.trim() || undefined, }; const finalName = cleanConfig.name || cleanConfig.ip; @@ -415,7 +415,7 @@ io.of('/database.io').on('connection', (socket) => { user: newHostConfig.user.trim(), port: newHostConfig.port || 22, password: newHostConfig.password?.trim() || undefined, - rsaKey: newHostConfig.rsaKey?.trim() || undefined + sshKey: newHostConfig.sshKey?.trim() || undefined, }; const encryptedConfig = encryptData(cleanConfig, userId, sessionToken); diff --git a/src/backend/ssh.cjs b/src/backend/ssh.cjs index af6b8c86..427e9edb 100644 --- a/src/backend/ssh.cjs +++ b/src/backend/ssh.cjs @@ -47,7 +47,7 @@ io.on("connection", (socket) => { }; logger.info("Connecting with config:", safeHostConfig); - const { ip, port, user, password, privateKey, passphrase } = hostConfig; + const { ip, port, user, password, sshKey, } = hostConfig; const conn = new SSHClient(); conn @@ -99,8 +99,7 @@ io.on("connection", (socket) => { port: port, username: user, password: password, - privateKey: privateKey ? Buffer.from(privateKey) : undefined, - passphrase: passphrase, + sshKey: sshKey ? Buffer.from(sshKey) : undefined, tryKeyboard: true, algorithms: { kex: [ diff --git a/src/modals/AddHostModal.jsx b/src/modals/AddHostModal.jsx index dd322a4b..d8bdf8c7 100644 --- a/src/modals/AddHostModal.jsx +++ b/src/modals/AddHostModal.jsx @@ -24,7 +24,6 @@ import VisibilityOff from '@mui/icons-material/VisibilityOff'; const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidden }) => { const [showPassword, setShowPassword] = useState(false); - const [showPassphrase, setShowPassphrase] = useState(false); const [activeTab, setActiveTab] = useState(0); const handleFileChange = (e) => { @@ -39,7 +38,7 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd '.ppk': 'PPK' }; - const isValidKeyFile = Object.keys(supportedKeyTypes).some(ext => + const isValidKeyFile = Object.keys(supportedKeyTypes).some(ext => file.name.toLowerCase().includes(ext) || file.name.endsWith('.pub') ); @@ -48,8 +47,7 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd reader.onload = (event) => { const keyContent = event.target.result; let keyType = 'UNKNOWN'; - - // Detect key type from content + if (keyContent.includes('BEGIN RSA PRIVATE KEY') || keyContent.includes('BEGIN RSA PUBLIC KEY')) { keyType = 'RSA'; } else if (keyContent.includes('BEGIN OPENSSH PRIVATE KEY') && keyContent.includes('ssh-ed25519')) { @@ -60,11 +58,11 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd keyType = 'DSA'; } - setForm(prev => ({ - ...prev, - privateKey: keyContent, + setForm(prev => ({ + ...prev, + sshKey: keyContent, keyType: keyType, - authMethod: 'key' + authMethod: 'sshKey' })); }; reader.readAsText(file); @@ -78,30 +76,25 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd ...prev, authMethod: newMethod, password: "", - privateKey: "", + sshKey: "", keyType: "", - passphrase: "" })); }; const isFormValid = () => { - const { ip, user, port, authMethod, password, privateKey } = form; - - // Basic validation for required fields + const { ip, user, port, authMethod, password, sshKey } = form; + if (!ip?.trim() || !user?.trim() || !port) return false; - - // Port validation + const portNum = Number(port); if (isNaN(portNum) || portNum < 1 || portNum > 65535) return false; - // If not remembering host, only basic fields are required if (!form.rememberHost) return true; - // Auth method validation only if remembering host if (form.rememberHost) { if (authMethod === 'Select Auth') return false; if (authMethod === 'password' && !password?.trim()) return false; - if (authMethod === 'key' && !privateKey?.trim()) return false; + if (authMethod === 'sshKey' && !sshKey?.trim()) return false; } return true; @@ -114,6 +107,7 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd return; } handleAddHost(); + setActiveTab(0); }; return ( @@ -144,10 +138,10 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd mx: 2, }} > - setActiveTab(val)} - sx={{ + sx={{ width: '100%', mb: 0, backgroundColor: theme.palette.general.tertiary, @@ -211,8 +205,8 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd Remember Host setForm({ - ...form, + onChange={(e) => setForm({ + ...form, rememberHost: e.target.checked, })} sx={{ @@ -284,7 +278,7 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd > - + @@ -315,9 +309,9 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd )} - {form.authMethod === 'key' && ( + {form.authMethod === 'sshKey' && ( - + SSH Key - {form.privateKey && ( - - Key Passphrase (optional) -
- setForm(prev => ({ ...prev, passphrase: e.target.value }))} - sx={{ - backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary, - flex: 1 - }} - /> - setShowPassphrase(!showPassphrase)} - sx={{ - color: theme.palette.text.primary, - marginLeft: 1 - }} - > - {showPassphrase ? : } - -
-
- )}
)} diff --git a/src/modals/AuthModal.jsx b/src/modals/AuthModal.jsx index 47350958..93cb46ae 100644 --- a/src/modals/AuthModal.jsx +++ b/src/modals/AuthModal.jsx @@ -1,159 +1,267 @@ import PropTypes from 'prop-types'; import { CssVarsProvider } from '@mui/joy/styles'; -import { Modal, Button, FormControl, FormLabel, Input, Stack, ModalDialog, IconButton, Tabs, TabList, Tab, TabPanel } from '@mui/joy'; +import { + Modal, + Button, + FormControl, + FormLabel, + Input, + Stack, + DialogContent, + ModalDialog, + IconButton, + Tabs, + TabList, + Tab, + TabPanel +} from '@mui/joy'; import theme from '/src/theme'; import { useEffect, useState } from 'react'; import Visibility from '@mui/icons-material/Visibility'; import VisibilityOff from '@mui/icons-material/VisibilityOff'; +import eventBus from '/src/other/eventBus'; -const AuthModal = ({ isHidden, form, setForm, handleLoginUser, handleGuestLogin, handleCreateUser, setIsLoginUserHidden }) => { - const [showPassword, setShowPassword] = useState(false); - const [confirmPassword, setConfirmPassword] = useState(''); +const AuthModal = ({ + isHidden, + form, + setForm, + handleLoginUser, + handleCreateUser, + handleGuestLogin, + setIsAuthModalHidden + }) => { const [activeTab, setActiveTab] = useState(0); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); const [isLoading, setIsLoading] = useState(false); - const isLoginFormValid = () => { - return form.username?.trim() && form.password?.trim(); + useEffect(() => { + const loginErrorHandler = () => setIsLoading(false); + eventBus.on('failedLoginUser', loginErrorHandler); + return () => eventBus.off('failedLoginUser', loginErrorHandler); + }, []); + + const resetForm = () => { + setForm({ username: '', password: '' }); + setShowPassword(false); + setShowConfirmPassword(false); + setIsLoading(false); }; - const isCreateFormValid = () => { - return form.username?.trim() && form.password?.trim() && confirmPassword?.trim() && form.password === confirmPassword; - }; - - const handleLogin = () => { - if (!isLoginFormValid()) { - alert("Please fill out all fields"); - return; - } - + const handleLogin = async () => { setIsLoading(true); - handleLoginUser({ - username: form.username.trim(), - password: form.password.trim(), - onSuccess: () => { - setIsLoading(false); - setIsLoginUserHidden(true); - }, - onFailure: (error) => { - setIsLoading(false); - alert(error); - } - }); + try { + await handleLoginUser({ + ...form, + onSuccess: () => { + setIsLoading(false); + setIsAuthModalHidden(true); + }, + onFailure: () => setIsLoading(false), + }); + } catch (error) { + setIsLoading(false); + } }; - const handleCreate = () => { - if (!isCreateFormValid()) { - alert("Please fill out all fields and ensure passwords match"); - return; - } - + const handleCreate = async () => { setIsLoading(true); - handleCreateUser({ - username: form.username.trim(), - password: form.password.trim(), - onSuccess: () => { - setIsLoading(false); - setIsLoginUserHidden(true); - }, - onFailure: (error) => { - setIsLoading(false); - alert(error); - } - }); + try { + await handleCreateUser({ + ...form, + onSuccess: () => { + setIsLoading(false); + setActiveTab(0); + setIsAuthModalHidden(true); + }, + onFailure: () => setIsLoading(false), + }); + } catch (error) { + setIsLoading(false); + } + }; + + const handleGuest = async () => { + setIsLoading(true); + try { + await handleGuestLogin({ + onSuccess: () => { + setIsLoading(false); + setIsAuthModalHidden(true); + }, + onFailure: () => setIsLoading(false) + }); + } catch (error) { + setIsLoading(false); + } }; useEffect(() => { - if (isHidden) { - setForm({ username: '', password: '' }); - setConfirmPassword(''); - setIsLoading(false); - setActiveTab(0); - } + if (isHidden) resetForm(); }, [isHidden]); + const isLoginValid = !!form.username && !!form.password; + const isCreateValid = isLoginValid && form.password === form.confirmPassword; + return ( - setIsLoginUserHidden(true)}> - - setActiveTab(val)}> - - Login - Create Account + setIsAuthModalHidden(true)}> + + setActiveTab(val)} + sx={{ + width: '100%', + backgroundColor: theme.palette.general.tertiary, + }} + > + + Login + Create -
- - + + + { e.preventDefault(); handleLogin(); }}> Username setForm({ ...form, username: event.target.value })} disabled={isLoading} + value={form.username} + onChange={(e) => setForm({ ...form, username: e.target.value })} + sx={inputStyle} /> Password
setForm({ ...form, password: event.target.value })} - disabled={isLoading} + onChange={(e) => setForm({ ...form, password: e.target.value })} + sx={{ ...inputStyle, flex: 1 }} /> - setShowPassword(!showPassword)}> + setShowPassword(!showPassword)} + sx={iconButtonStyle} + > {showPassword ? : }
- -
- - + + { e.preventDefault(); handleCreate(); }}> Username setForm({ ...form, username: event.target.value })} disabled={isLoading} + value={form.username} + onChange={(e) => setForm({ ...form, username: e.target.value })} + sx={inputStyle} /> Password
setForm({ ...form, password: event.target.value })} - disabled={isLoading} + onChange={(e) => setForm({ ...form, password: e.target.value })} + sx={{ ...inputStyle, flex: 1 }} /> - setShowPassword(!showPassword)}> + setShowPassword(!showPassword)} + sx={iconButtonStyle} + > {showPassword ? : }
Confirm Password - setConfirmPassword(event.target.value)} - disabled={isLoading} - /> +
+ setForm({ ...form, confirmPassword: e.target.value })} + sx={{ ...inputStyle, flex: 1 }} + /> + setShowConfirmPassword(!showConfirmPassword)} + sx={iconButtonStyle} + > + {showConfirmPassword ? : } + +
-
-
+
@@ -161,14 +269,38 @@ const AuthModal = ({ isHidden, form, setForm, handleLoginUser, handleGuestLogin, ); }; +const inputStyle = { + backgroundColor: theme.palette.general.primary, + color: theme.palette.text.primary, + '&:disabled': { + opacity: 0.5, + backgroundColor: theme.palette.general.primary, + }, +}; + +const iconButtonStyle = { + color: theme.palette.text.primary, + marginLeft: 1, + '&:disabled': { opacity: 0.5 }, +}; + +const buttonStyle = { + backgroundColor: theme.palette.general.primary, + '&:hover': { backgroundColor: theme.palette.general.disabled }, + '&:disabled': { + opacity: 0.5, + backgroundColor: theme.palette.general.primary, + }, +}; + AuthModal.propTypes = { isHidden: PropTypes.bool.isRequired, form: PropTypes.object.isRequired, setForm: PropTypes.func.isRequired, handleLoginUser: PropTypes.func.isRequired, - handleGuestLogin: PropTypes.func.isRequired, handleCreateUser: PropTypes.func.isRequired, - setIsLoginUserHidden: PropTypes.func.isRequired, + handleGuestLogin: PropTypes.func.isRequired, + setIsAuthModalHidden: PropTypes.func.isRequired, }; export default AuthModal; \ No newline at end of file diff --git a/src/modals/CreateUserModal.jsx b/src/modals/CreateUserModal.jsx deleted file mode 100644 index cb65a5aa..00000000 --- a/src/modals/CreateUserModal.jsx +++ /dev/null @@ -1,207 +0,0 @@ -import PropTypes from 'prop-types'; -import { CssVarsProvider } from '@mui/joy/styles'; -import { Modal, Button, FormControl, FormLabel, Input, Stack, DialogTitle, DialogContent, ModalDialog, IconButton } from '@mui/joy'; -import theme from '/src/theme'; -import { useEffect, useState } from 'react'; -import Visibility from '@mui/icons-material/Visibility'; -import VisibilityOff from '@mui/icons-material/VisibilityOff'; - -const CreateUserModal = ({ isHidden, form, setForm, handleCreateUser, setIsCreateUserHidden, setIsLoginUserHidden }) => { - const [confirmPassword, setConfirmPassword] = useState(''); - const [showPassword, setShowPassword] = useState(false); - const [showConfirmPassword, setShowConfirmPassword] = useState(false); - const [isLoading, setIsLoading] = useState(false); - - const isFormValid = () => { - if (!form.username || !form.password || form.password !== confirmPassword) return false; - return true; - }; - - const handleCreate = async () => { - setIsLoading(true); - try { - await handleCreateUser({ - ...form, - onSuccess: () => setIsLoading(false), - onFailure: () => setIsLoading(false) - }); - } catch (error) { - setIsLoading(false); - } - }; - - useEffect(() => { - if (isHidden) { - setForm({ username: '', password: '' }); - setConfirmPassword(''); - setIsLoading(false); - } - }, [isHidden]); - - return ( - - {}}> - - Create - -
{ - event.preventDefault(); - if (isFormValid() && !isLoading) handleCreate(); - }} - > - - - Username - setForm({ ...form, username: event.target.value })} - sx={{ - backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary, - '&:disabled': { - opacity: 0.5, - backgroundColor: theme.palette.general.primary, - }, - }} - /> - - - Password -
- setForm({ ...form, password: event.target.value })} - sx={{ - backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary, - flex: 1, - '&:disabled': { - opacity: 0.5, - backgroundColor: theme.palette.general.primary, - }, - }} - /> - setShowPassword(!showPassword)} - sx={{ - color: theme.palette.text.primary, - marginLeft: 1, - '&:disabled': { - opacity: 0.5, - }, - }} - > - {showPassword ? : } - -
-
- - Confirm Password -
- setConfirmPassword(event.target.value)} - sx={{ - backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary, - flex: 1, - '&:disabled': { - opacity: 0.5, - backgroundColor: theme.palette.general.primary, - }, - }} - /> - setShowConfirmPassword(!showConfirmPassword)} - sx={{ - color: theme.palette.text.primary, - marginLeft: 1, - '&:disabled': { - opacity: 0.5, - }, - }} - > - {showConfirmPassword ? : } - -
-
- - -
-
-
-
-
-
- ); -}; - -CreateUserModal.propTypes = { - isHidden: PropTypes.bool.isRequired, - form: PropTypes.object.isRequired, - setForm: PropTypes.func.isRequired, - handleCreateUser: PropTypes.func.isRequired, - setIsCreateUserHidden: PropTypes.func.isRequired, - setIsLoginUserHidden: PropTypes.func.isRequired, -}; - -export default CreateUserModal; \ No newline at end of file diff --git a/src/modals/EditHostModal.jsx b/src/modals/EditHostModal.jsx index 9608fbe4..bd0a2e33 100644 --- a/src/modals/EditHostModal.jsx +++ b/src/modals/EditHostModal.jsx @@ -8,8 +8,6 @@ import { FormLabel, Input, Stack, - DialogTitle, - DialogContent, ModalDialog, Select, Option, @@ -32,15 +30,13 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo user: hostConfig?.user || '', port: hostConfig?.port || '', password: '', - privateKey: hostConfig?.privateKey || '', + sshKey: hostConfig?.sshKey || '', keyType: hostConfig?.keyType || '', - passphrase: '', authMethod: hostConfig?.authMethod || 'Select Auth', storePassword: true, rememberHost: true }); const [showPassword, setShowPassword] = useState(false); - const [showPassphrase, setShowPassphrase] = useState(false); const [activeTab, setActiveTab] = useState(0); const [isLoading, setIsLoading] = useState(false); @@ -52,13 +48,12 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo ip: hostConfig.ip || '', user: hostConfig.user || '', password: hostConfig.password || '', - privateKey: hostConfig.privateKey || '', + sshKey: hostConfig.sshKey || '', keyType: hostConfig.keyType || '', - passphrase: hostConfig.passphrase || '', port: hostConfig.port || 22, - authMethod: hostConfig.password ? 'password' : hostConfig.privateKey ? 'key' : 'Select Auth', + authMethod: hostConfig.password ? 'password' : hostConfig.sshKey ? 'key' : 'Select Auth', rememberHost: true, - storePassword: !!(hostConfig.password || hostConfig.privateKey), + storePassword: !!(hostConfig.password || hostConfig.sshKey), }); } }, [isHidden, hostConfig]); @@ -84,8 +79,7 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo reader.onload = (evt) => { const keyContent = evt.target.result; let keyType = 'UNKNOWN'; - - // Detect key type from content + if (keyContent.includes('BEGIN RSA PRIVATE KEY') || keyContent.includes('BEGIN RSA PUBLIC KEY')) { keyType = 'RSA'; } else if (keyContent.includes('BEGIN OPENSSH PRIVATE KEY') && keyContent.includes('ssh-ed25519')) { @@ -98,7 +92,7 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo setForm((prev) => ({ ...prev, - privateKey: keyContent, + sshKey: keyContent, keyType: keyType, authMethod: 'key' })); @@ -121,27 +115,23 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo ...prev, storePassword: Boolean(checked), password: checked ? prev.password : "", - privateKey: checked ? prev.privateKey : "", - passphrase: checked ? prev.passphrase : "", + sshKey: checked ? prev.sshKey : "", authMethod: checked ? prev.authMethod : "Select Auth" })); }; const isFormValid = () => { - const { ip, user, port, authMethod, password, privateKey } = form; - - // Basic validation for required fields + const { ip, user, port, authMethod, password, sshKey } = form; + if (!ip?.trim() || !user?.trim() || !port) return false; - - // Port validation + const portNum = Number(port); if (isNaN(portNum) || portNum < 1 || portNum > 65535) return false; - // Auth method validation only if storing password if (form.storePassword) { if (authMethod === 'Select Auth') return false; if (authMethod === 'password' && !password?.trim()) return false; - if (authMethod === 'key' && !privateKey?.trim()) return false; + if (authMethod === 'sshKey' && !sshKey?.trim()) return false; } return true; @@ -150,7 +140,7 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo const handleSubmit = async (event) => { event.preventDefault(); if (isLoading) return; - + setIsLoading(true); try { const newConfig = { @@ -165,9 +155,8 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo if (form.authMethod === 'password') { newConfig.password = form.password; } else if (form.authMethod === 'key') { - newConfig.privateKey = form.privateKey; + newConfig.sshKey = form.sshKey; newConfig.keyType = form.keyType; - newConfig.passphrase = form.passphrase; } } @@ -378,7 +367,7 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo {form.authMethod === 'key' && ( - + SSH Key - {hostConfig?.privateKey && !form.privateKey && ( + {hostConfig?.sshKey && !form.sshKey && ( )} - {form.privateKey && ( - - Key Passphrase (optional) -
- setForm(prev => ({ ...prev, passphrase: e.target.value }))} - sx={{ - backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary, - flex: 1 - }} - /> - setShowPassphrase(!showPassphrase)} - sx={{ - color: theme.palette.text.primary, - marginLeft: 1 - }} - > - {showPassphrase ? : } - -
-
- )}
)} @@ -452,7 +415,7 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo - - - - - -
-
-
- ); -}; - -LoginUserModal.propTypes = { - isHidden: PropTypes.bool.isRequired, - form: PropTypes.object.isRequired, - setForm: PropTypes.func.isRequired, - handleLoginUser: PropTypes.func.isRequired, - handleGuestLogin: PropTypes.func.isRequired, - setIsLoginUserHidden: PropTypes.func.isRequired, - setIsCreateUserHidden: PropTypes.func.isRequired, -}; - -export default LoginUserModal; \ No newline at end of file diff --git a/src/modals/NoAuthenticationModal.jsx b/src/modals/NoAuthenticationModal.jsx index 813f1e02..c930a7a2 100644 --- a/src/modals/NoAuthenticationModal.jsx +++ b/src/modals/NoAuthenticationModal.jsx @@ -15,31 +15,44 @@ import { Option, } from '@mui/joy'; import theme from '/src/theme'; -import { useState, useEffect } from 'react'; +import {useEffect, useState} from 'react'; import Visibility from '@mui/icons-material/Visibility'; import VisibilityOff from '@mui/icons-material/VisibilityOff'; -const NoAuthenticationModal = ({ isHidden, setIsHidden, onAuthenticate }) => { - const [form, setForm] = useState({ - authMethod: 'Select Auth', - password: '', - privateKey: '', - keyType: '', - passphrase: '' - }); +const NoAuthenticationModal = ({ isHidden, form, setForm, setIsNoAuthHidden, handleAuthSubmit }) => { const [showPassword, setShowPassword] = useState(false); - const [showPassphrase, setShowPassphrase] = useState(false); + + useEffect(() => { + if (!form.authMethod) { + setForm(prev => ({ + ...prev, + authMethod: 'Select Auth', + password: '', + sshKey: '', + keyType: '', + })); + } + }, []); + + const isFormValid = () => { + if (!form.authMethod || form.authMethod === 'Select Auth') return false; + if (form.authMethod === 'sshKey' && !form.sshKey) return false; + if (form.authMethod === 'password' && !form.password) return false; + return true; + }; const handleSubmit = (e) => { e.preventDefault(); - onAuthenticate({ - authMethod: form.authMethod, - password: form.password, - privateKey: form.privateKey, - keyType: form.keyType, - passphrase: form.passphrase - }); - setIsHidden(true); + if(isFormValid()) { + handleAuthSubmit(form); + setForm (prev => ({ + ...prev, + authMethod: 'Select Auth', + password: '', + sshKey: '', + keyType: '', + })) + } }; const handleFileChange = (e) => { @@ -77,9 +90,9 @@ const NoAuthenticationModal = ({ isHidden, setIsHidden, onAuthenticate }) => { setForm({ ...form, - privateKey: keyContent, + sshKey: keyContent, keyType: keyType, - authMethod: 'key' + authMethod: 'sshKey' }); }; reader.readAsText(file); @@ -92,12 +105,31 @@ const NoAuthenticationModal = ({ isHidden, setIsHidden, onAuthenticate }) => { setIsHidden(true)} + onClose={(e, reason) => { + if (reason !== 'backdropClick') { + setIsNoAuthHidden(true); + } + }} + sx={{ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + }} > Authentication Required @@ -107,14 +139,13 @@ const NoAuthenticationModal = ({ isHidden, setIsHidden, onAuthenticate }) => { Authentication Method {form.authMethod === 'password' && ( Password - setForm({ ...form, password: e.target.value })} - endDecorator={ - setShowPassword(!showPassword)}> - {showPassword ? : } - - } - /> +
+ setForm({...form, password: e.target.value})} + sx={{ + backgroundColor: theme.palette.general.primary, + color: theme.palette.text.primary, + flex: 1 + }} + /> + setShowPassword(!showPassword)} + sx={{ + color: theme.palette.text.primary, + marginLeft: 1, + '&:disabled': { + opacity: 0.5, + }, + }} + > + {showPassword ? : } + +
)} - {form.authMethod === 'key' && ( + {form.authMethod === 'sshKey' && ( - + SSH Key - {form.privateKey && ( - - Key Passphrase (optional) - setForm(prev => ({ ...prev, passphrase: e.target.value }))} - endDecorator={ - setShowPassphrase(!showPassphrase)}> - {showPassphrase ? : } - - } - /> - - )} )} - {isOwner && ( - <> - - - - - )} - {!isOwner && ( - - )} + { + e.stopPropagation(); + setSelectedHost(hostWrapper); + setIsMenuOpen(!isMenuOpen); + anchorEl.current = e.currentTarget; + }} + disabled={isDeleting} + sx={{ + backgroundColor: "#6e6e6e", + "&:hover": { backgroundColor: "#0f0f0f" }, + opacity: isDeleting ? 0.5 : 1, + cursor: isDeleting ? "not-allowed" : "pointer", + borderColor: "#3d3d3d", + borderWidth: "2px", + color: "#fff", + }} + > + â‹® + ); @@ -352,7 +344,6 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e return ( <> - {/* Render hosts without folders first */}
handleDragOver(e, 'no-folder')} @@ -362,7 +353,6 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e {noFolder.map((host) => renderHostItem(host))}
- {/* Render folders and their hosts */} {sortedFolders.map((folderName) => (
+ { + setIsMenuOpen(false); + setSelectedHost(null); + }} + sx={{ + "& .MuiMenu-list": { + backgroundColor: "#6e6e6e", + color: "white" + } + }} + > + {selectedHost && ( + selectedHost.createdBy?._id === userRef.current?.getUser()?.id ? ( + <> + { + e.stopPropagation(); + setSelectedHostForShare(selectedHost); + setIsShareModalHidden(false); + setIsMenuOpen(false); + }} + > + Share + + { + e.stopPropagation(); + openEditPanel(selectedHost.config); + setIsMenuOpen(false); + }} + > + Edit + + { + e.stopPropagation(); + handleDelete(e, selectedHost); + setIsMenuOpen(false); + }} + disabled={isDeleting} + > + {isDeleting ? "Deleting..." : "Delete"} + + + ) : ( + { + e.stopPropagation(); + handleDelete(e, selectedHost); + setIsMenuOpen(false); + }} + disabled={isDeleting} + > + {isDeleting ? "Removing..." : "Remove Share"} + + ) + )} +
); } @@ -418,6 +470,8 @@ HostViewer.propTypes = { onModalOpen: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired, userRef: PropTypes.object.isRequired, + isMenuOpen: PropTypes.bool.isRequired, + setIsMenuOpen: PropTypes.func.isRequired, }; export default HostViewer; \ No newline at end of file diff --git a/src/backend/ssh.cjs b/src/backend/ssh.cjs index 427e9edb..400db2b1 100644 --- a/src/backend/ssh.cjs +++ b/src/backend/ssh.cjs @@ -32,7 +32,7 @@ io.on("connection", (socket) => { return; } - if (!hostConfig.password && !hostConfig.privateKey) { + if (!hostConfig.password && !hostConfig.sshKey) { logger.error("No authentication provided"); socket.emit("error", "Authentication required"); return; @@ -43,7 +43,6 @@ io.on("connection", (socket) => { port: hostConfig.port, user: hostConfig.user, authType: hostConfig.password ? 'password' : 'key', - keyType: hostConfig.keyType }; logger.info("Connecting with config:", safeHostConfig); @@ -98,29 +97,11 @@ io.on("connection", (socket) => { host: ip, port: port, username: user, - password: password, - sshKey: sshKey ? Buffer.from(sshKey) : undefined, - tryKeyboard: true, + password: password || undefined, + privateKey: sshKey ? Buffer.from(sshKey) : undefined, algorithms: { - kex: [ - 'curve25519-sha256', - 'curve25519-sha256@libssh.org', - 'ecdh-sha2-nistp256', - 'ecdh-sha2-nistp384', - 'ecdh-sha2-nistp521', - 'diffie-hellman-group-exchange-sha256', - 'diffie-hellman-group14-sha256', - 'diffie-hellman-group14-sha1' - ], - serverHostKey: [ - 'ssh-ed25519', - 'ecdsa-sha2-nistp256', - 'ecdsa-sha2-nistp384', - 'ecdsa-sha2-nistp521', - 'rsa-sha2-512', - 'rsa-sha2-256', - 'ssh-rsa' - ] + kex: ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'ecdh-sha2-nistp256'], + serverHostKey: ['ssh-ed25519', 'ecdsa-sha2-nistp256'] } }); }); diff --git a/src/modals/EditHostModal.jsx b/src/modals/EditHostModal.jsx index bd0a2e33..25810fe7 100644 --- a/src/modals/EditHostModal.jsx +++ b/src/modals/EditHostModal.jsx @@ -442,7 +442,7 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo EditHostModal.propTypes = { isHidden: PropTypes.bool.isRequired, - hostConfig: PropTypes.object.isRequired, + hostConfig: PropTypes.object, setIsEditHostHidden: PropTypes.func.isRequired, handleEditHost: PropTypes.func.isRequired }; -- 2.49.1 From 4f384dd60b42345056058439f2f59702a854b371 Mon Sep 17 00:00:00 2001 From: Karmaa Date: Fri, 21 Mar 2025 00:50:26 -0500 Subject: [PATCH 35/47] Keep SSH session alive. --- src/apps/ssh/Terminal.jsx | 9 +++++++++ src/backend/ssh.cjs | 9 +++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/apps/ssh/Terminal.jsx b/src/apps/ssh/Terminal.jsx index e96ae1df..db9e78ca 100644 --- a/src/apps/ssh/Terminal.jsx +++ b/src/apps/ssh/Terminal.jsx @@ -178,7 +178,16 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde } }); + const pingInterval = setInterval(() => { + socketRef.current.emit("ping"); + }, 5000); + + socketRef.current.on("pong", () => { + console.log("Received pong from server."); + }); + return () => { + clearInterval(pingInterval); if (terminalInstance.current) { terminalInstance.current.dispose(); terminalInstance.current = null; diff --git a/src/backend/ssh.cjs b/src/backend/ssh.cjs index 400db2b1..b4281c3e 100644 --- a/src/backend/ssh.cjs +++ b/src/backend/ssh.cjs @@ -53,7 +53,7 @@ io.on("connection", (socket) => { .on("ready", function () { logger.info("SSH connection established"); - conn.shell({ term: "xterm-256color" }, function (err, newStream) { + conn.shell({ term: "xterm-256color", keepaliveInterval: 30000 }, function (err, newStream) { if (err) { logger.error("Shell error:", err.message); socket.emit("error", err.message); @@ -93,6 +93,9 @@ io.on("connection", (socket) => { logger.error("Error:", err.message); socket.emit("error", err.message); }) + .on("ping", function () { + socket.emit("ping"); + }) .connect({ host: ip, port: port, @@ -102,7 +105,9 @@ io.on("connection", (socket) => { algorithms: { kex: ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'ecdh-sha2-nistp256'], serverHostKey: ['ssh-ed25519', 'ecdsa-sha2-nistp256'] - } + }, + keepaliveInterval: 30000, + keepaliveCountMax: 3, }); }); -- 2.49.1 From 9a30c49b66f6bd4e91ba84bc709030ddce08d00a Mon Sep 17 00:00:00 2001 From: Karmaa Date: Fri, 21 Mar 2025 01:09:41 -0500 Subject: [PATCH 36/47] Rename version (to version 2.1) --- src/modals/ProfileModal.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modals/ProfileModal.jsx b/src/modals/ProfileModal.jsx index 177a23ad..43a4ba21 100644 --- a/src/modals/ProfileModal.jsx +++ b/src/modals/ProfileModal.jsx @@ -73,7 +73,7 @@ export default function ProfileModal({
- v2.0.1 + v2.1
-- 2.49.1 From f743f8fa9c7e400b143003b5ec6fa83e668ff646 Mon Sep 17 00:00:00 2001 From: Karmaa Date: Fri, 21 Mar 2025 15:38:12 -0500 Subject: [PATCH 37/47] Optimize ssh connection (faster). Renamed all versions to reflect that this project is still under development (expect bugs). --- src/apps/ssh/Terminal.jsx | 12 +++++++++++- src/backend/ssh.cjs | 10 +++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/apps/ssh/Terminal.jsx b/src/apps/ssh/Terminal.jsx index db9e78ca..4fbf0600 100644 --- a/src/apps/ssh/Terminal.jsx +++ b/src/apps/ssh/Terminal.jsx @@ -117,8 +117,18 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde let isPasting = false; + let buffer = ""; + let bufferTimeout = null; + terminalInstance.current.onData((data) => { - socketRef.current.emit("data", data); + buffer += data; + if (!bufferTimeout) { + bufferTimeout = setTimeout(() => { + socketRef.current.emit("data", buffer); + buffer = ""; + bufferTimeout = null; + }, 20); + } }); terminalInstance.current.attachCustomKeyEventHandler((event) => { diff --git a/src/backend/ssh.cjs b/src/backend/ssh.cjs index b4281c3e..775a6149 100644 --- a/src/backend/ssh.cjs +++ b/src/backend/ssh.cjs @@ -10,7 +10,10 @@ const io = socketIo(server, { methods: ["GET", "POST"], credentials: true }, - allowEIO3: true + allowEIO3: true, + pingInterval: 2500, + pingTimeout: 5000, + maxHttpBufferSize: 1e7, }); const logger = { @@ -106,8 +109,9 @@ io.on("connection", (socket) => { kex: ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'ecdh-sha2-nistp256'], serverHostKey: ['ssh-ed25519', 'ecdsa-sha2-nistp256'] }, - keepaliveInterval: 30000, - keepaliveCountMax: 3, + keepaliveInterval: 10000, + keepaliveCountMax: 5, + readyTimeout: 5000, }); }); -- 2.49.1 From af134642001f9113fc73017b82315b62f0f8c8be Mon Sep 17 00:00:00 2001 From: Karmaa Date: Fri, 21 Mar 2025 15:39:19 -0500 Subject: [PATCH 38/47] Optimize ssh connection (faster). Renamed all versions to reflect that this project is still under development (expect bugs). --- src/modals/ProfileModal.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modals/ProfileModal.jsx b/src/modals/ProfileModal.jsx index 43a4ba21..0677c794 100644 --- a/src/modals/ProfileModal.jsx +++ b/src/modals/ProfileModal.jsx @@ -73,7 +73,7 @@ export default function ProfileModal({
- v2.1 + v0.21
-- 2.49.1 From d1db9b79724b532050ef2627ead0629277b85208 Mon Sep 17 00:00:00 2001 From: Karmaa Date: Fri, 21 Mar 2025 15:43:08 -0500 Subject: [PATCH 39/47] Fixed naming discrepency --- src/modals/ProfileModal.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modals/ProfileModal.jsx b/src/modals/ProfileModal.jsx index 0677c794..af349406 100644 --- a/src/modals/ProfileModal.jsx +++ b/src/modals/ProfileModal.jsx @@ -73,7 +73,7 @@ export default function ProfileModal({
- v0.21 + v0.2.1
-- 2.49.1 From 652b8d2da2e9ff631c6179b41ab2d1ce862fa821 Mon Sep 17 00:00:00 2001 From: Karmaa Date: Fri, 21 Mar 2025 16:02:01 -0500 Subject: [PATCH 40/47] Optimize pasting (faster, works better with vim) --- src/apps/ssh/Terminal.jsx | 41 +++++++++++++-------------------------- 1 file changed, 13 insertions(+), 28 deletions(-) diff --git a/src/apps/ssh/Terminal.jsx b/src/apps/ssh/Terminal.jsx index 4fbf0600..f2498b0a 100644 --- a/src/apps/ssh/Terminal.jsx +++ b/src/apps/ssh/Terminal.jsx @@ -115,8 +115,6 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde terminalInstance.current.write(decoder.decode(new Uint8Array(data))); }); - let isPasting = false; - let buffer = ""; let bufferTimeout = null; @@ -131,38 +129,25 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde } }); - terminalInstance.current.attachCustomKeyEventHandler((event) => { + terminalInstance.current.attachCustomKeyEventHandler(async (event) => { if ((event.ctrlKey || event.metaKey) && event.key === "v") { - if (isPasting) return false; - isPasting = true; - event.preventDefault(); - navigator.clipboard.readText().then((text) => { - text = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); - const lines = text.split("\n"); + if (!socketRef.current) return false; - if (socketRef.current) { - let index = 0; + try { + const text = await navigator.clipboard.readText(); + const lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n"); - const sendLine = () => { - if (index < lines.length) { - socketRef.current.emit("data", lines[index] + "\r"); - index++; - setTimeout(sendLine, 10); - } else { - isPasting = false; - } - }; - - sendLine(); - } else { - isPasting = false; - } - }).catch((err) => { + await Promise.all(lines.map(line => { + return new Promise(resolve => { + socketRef.current.emit("data", line + "\r"); + resolve(); + }); + })); + } catch (err) { console.error("Failed to read clipboard contents:", err); - isPasting = false; - }); + } return false; } -- 2.49.1 From 3d1ec6660d972f247578f5ae3b7b7c90d517f136 Mon Sep 17 00:00:00 2001 From: LukeGus Date: Sun, 23 Mar 2025 01:03:42 -0500 Subject: [PATCH 41/47] Optimized SSH (delay), and fixed pasting issues --- src/apps/ssh/Terminal.jsx | 160 +++++++++++++++++++++++++++++++------- 1 file changed, 131 insertions(+), 29 deletions(-) diff --git a/src/apps/ssh/Terminal.jsx b/src/apps/ssh/Terminal.jsx index f2498b0a..89f177ca 100644 --- a/src/apps/ssh/Terminal.jsx +++ b/src/apps/ssh/Terminal.jsx @@ -18,17 +18,31 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde if (!parentContainer || parentContainer.clientWidth === 0) return; - const parentWidth = parentContainer.clientWidth - 10; - const parentHeight = parentContainer.clientHeight - 10; + const parentWidth = parentContainer.clientWidth - 8; + const parentHeight = parentContainer.clientHeight - 12; terminalContainer.style.width = `${parentWidth}px`; terminalContainer.style.height = `${parentHeight}px`; requestAnimationFrame(() => { - fitAddon.current.fit(); - if (socketRef.current && terminalInstance.current) { - const { cols, rows } = terminalInstance.current; - socketRef.current.emit("resize", { cols, rows }); + if (fitAddon.current && terminalInstance.current) { + fitAddon.current.fit(); + + if (socketRef.current) { + let { cols, rows } = terminalInstance.current; + const originalCols = cols; + const originalRows = rows; + + cols += 1; + + try { + terminalInstance.current.resize(cols, rows); + socketRef.current.emit("resize", { cols, rows }); + } catch (e) { + terminalInstance.current.resize(originalCols, originalRows); + socketRef.current.emit("resize", { cols: originalCols, rows: originalRows }); + } + } } }); }; @@ -50,6 +64,11 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde fontSize: 14, scrollback: 1000, ignoreBracketedPasteMode: true, + fastScrollModifier: 'alt', + fastScrollSensitivity: 5, + letterSpacing: 0, + lineHeight: 1, + padding: 2, }); terminalInstance.current.loadAddon(fitAddon.current); @@ -115,20 +134,48 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde terminalInstance.current.write(decoder.decode(new Uint8Array(data))); }); - let buffer = ""; - let bufferTimeout = null; - terminalInstance.current.onData((data) => { - buffer += data; - if (!bufferTimeout) { - bufferTimeout = setTimeout(() => { - socketRef.current.emit("data", buffer); - buffer = ""; - bufferTimeout = null; - }, 20); + if (data.length === 1) { + socketRef.current.emit("data", data); + return; + } + + if (socketRef.current) { + socketRef.current.emit("data", data); } }); + const getClipboardText = async () => { + try { + if (navigator.clipboard && navigator.clipboard.readText) { + return await navigator.clipboard.readText(); + } + + if (document.queryCommandSupported && document.queryCommandSupported('paste')) { + const textarea = document.createElement('textarea'); + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + textarea.focus(); + + try { + document.execCommand('paste'); + const text = textarea.value; + document.body.removeChild(textarea); + return text; + } catch (e) { + document.body.removeChild(textarea); + throw new Error('Fallback clipboard paste failed'); + } + } + + throw new Error('No clipboard access methods available'); + } catch (err) { + console.error("Failed to read clipboard contents:", err); + return null; + } + }; + terminalInstance.current.attachCustomKeyEventHandler(async (event) => { if ((event.ctrlKey || event.metaKey) && event.key === "v") { event.preventDefault(); @@ -136,17 +183,33 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde if (!socketRef.current) return false; try { - const text = await navigator.clipboard.readText(); + const text = await getClipboardText(); + if (!text) { + terminalInstance.current.write("\r\nClipboard access denied or empty\r\n"); + return false; + } + const lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n"); - - await Promise.all(lines.map(line => { - return new Promise(resolve => { + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + if (i === 0) { + socketRef.current.emit("data", line); + } + else if (i < lines.length - 1) { + socketRef.current.emit("data", "\r" + line); + } + else if (i > 0) { + const endsWithNewline = text.endsWith("\n") || text.endsWith("\r\n") || text.endsWith("\r"); + socketRef.current.emit("data", "\r" + line + (endsWithNewline ? "\r" : "")); + } + else if (lines.length === 1 && (text.endsWith("\n") || text.endsWith("\r\n") || text.endsWith("\r"))) { socketRef.current.emit("data", line + "\r"); - resolve(); - }); - })); + } + } } catch (err) { - console.error("Failed to read clipboard contents:", err); + console.error("Failed to process clipboard contents:", err); } return false; @@ -155,11 +218,43 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde return true; }); + const setClipboardText = (text) => { + try { + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text); + return true; + } + + if (document.queryCommandSupported && document.queryCommandSupported('copy')) { + const textarea = document.createElement('textarea'); + textarea.value = text; + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + textarea.select(); + + try { + document.execCommand('copy'); + document.body.removeChild(textarea); + return true; + } catch (e) { + document.body.removeChild(textarea); + return false; + } + } + + return false; + } catch (err) { + console.error("Failed to write to clipboard:", err); + return false; + } + }; + terminalInstance.current.onKey(({ domEvent }) => { if (domEvent.key === "c" && (domEvent.ctrlKey || domEvent.metaKey)) { const selection = terminalInstance.current.getSelection(); if (selection) { - navigator.clipboard.writeText(selection); + setClipboardText(selection); } } }); @@ -206,14 +301,21 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde const parentContainer = terminalContainer.parentElement; if (!parentContainer) return; - const observer = new ResizeObserver(() => { + const resizeObserver = new ResizeObserver(() => { resizeTerminal(); }); - observer.observe(parentContainer); + resizeObserver.observe(parentContainer); + + const handleWindowResize = () => { + resizeTerminal(); + }; + + window.addEventListener('resize', handleWindowResize); return () => { - observer.disconnect(); + resizeObserver.disconnect(); + window.removeEventListener('resize', handleWindowResize); }; }, []); @@ -226,7 +328,7 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde position: 'absolute', width: '100%', height: '100%', - transform: 'translateY(5px) translateX(5px)', + transform: 'translateY(2px) translateX(3px)', }} /> ); -- 2.49.1 From 71ffb74c285012cc440fd732fc92608ddef8908d Mon Sep 17 00:00:00 2001 From: LukeGus Date: Sun, 23 Mar 2025 01:26:33 -0500 Subject: [PATCH 42/47] Fixed pasting permissions --- src/apps/ssh/Terminal.jsx | 67 +++++++++++++++++++++++++++++++++++---- 1 file changed, 61 insertions(+), 6 deletions(-) diff --git a/src/apps/ssh/Terminal.jsx b/src/apps/ssh/Terminal.jsx index 89f177ca..d674f0d3 100644 --- a/src/apps/ssh/Terminal.jsx +++ b/src/apps/ssh/Terminal.jsx @@ -147,10 +147,17 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde const getClipboardText = async () => { try { + // Modern Clipboard API - this will work in secure contexts (HTTPS or localhost) if (navigator.clipboard && navigator.clipboard.readText) { - return await navigator.clipboard.readText(); + try { + return await navigator.clipboard.readText(); + } catch (clipboardErr) { + console.warn("Navigator clipboard API failed:", clipboardErr); + // Continue to fallback methods + } } + // Fallback method using document.execCommand if (document.queryCommandSupported && document.queryCommandSupported('paste')) { const textarea = document.createElement('textarea'); textarea.style.position = 'fixed'; @@ -162,12 +169,48 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde document.execCommand('paste'); const text = textarea.value; document.body.removeChild(textarea); - return text; - } catch (e) { + if (text) return text; + } catch (execErr) { document.body.removeChild(textarea); - throw new Error('Fallback clipboard paste failed'); + console.warn("execCommand paste failed:", execErr); + // Continue to next fallback } } + + // Fallback UI prompt for non-secure contexts where clipboard API is restricted + if (!window.location.hostname.includes('localhost') && + !window.location.protocol.includes('https')) { + + // Display input prompt in the terminal itself + terminalInstance.current.write("\r\n\r\nPaste access denied. Please type or paste content here:\r\n"); + + // Use a terminal-based input method + return new Promise(resolve => { + let inputText = ''; + const dataHandler = terminalInstance.current.onData(data => { + // Check for enter key (carriage return) + if (data === '\r') { + terminalInstance.current.write('\r\n'); + dataHandler.dispose(); // Remove the handler + resolve(inputText); + return; + } + + // Handle backspace + if (data === '\x7f') { + if (inputText.length > 0) { + inputText = inputText.slice(0, -1); + terminalInstance.current.write('\b \b'); // Erase the character + } + return; + } + + // Normal character input + inputText += data; + terminalInstance.current.write(data); + }); + }); + } throw new Error('No clipboard access methods available'); } catch (err) { @@ -176,16 +219,23 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde } }; + // Track if paste is in progress to prevent double paste + let pasteInProgress = false; + terminalInstance.current.attachCustomKeyEventHandler(async (event) => { if ((event.ctrlKey || event.metaKey) && event.key === "v") { event.preventDefault(); - if (!socketRef.current) return false; - + // Prevent double paste execution + if (pasteInProgress || !socketRef.current) return false; + + pasteInProgress = true; + try { const text = await getClipboardText(); if (!text) { terminalInstance.current.write("\r\nClipboard access denied or empty\r\n"); + pasteInProgress = false; return false; } @@ -211,6 +261,11 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde } catch (err) { console.error("Failed to process clipboard contents:", err); } + + // Set timeout to reset paste lock + setTimeout(() => { + pasteInProgress = false; + }, 100); return false; } -- 2.49.1 From b80e71015e3b116deff633549b0617b61192f50d Mon Sep 17 00:00:00 2001 From: LukeGus Date: Sun, 23 Mar 2025 01:52:24 -0500 Subject: [PATCH 43/47] Fixed pasting permissions --- src/apps/ssh/Terminal.jsx | 243 +++++++++++--------------------------- 1 file changed, 67 insertions(+), 176 deletions(-) diff --git a/src/apps/ssh/Terminal.jsx b/src/apps/ssh/Terminal.jsx index d674f0d3..3011d35e 100644 --- a/src/apps/ssh/Terminal.jsx +++ b/src/apps/ssh/Terminal.jsx @@ -25,24 +25,10 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde terminalContainer.style.height = `${parentHeight}px`; requestAnimationFrame(() => { - if (fitAddon.current && terminalInstance.current) { - fitAddon.current.fit(); - - if (socketRef.current) { - let { cols, rows } = terminalInstance.current; - const originalCols = cols; - const originalRows = rows; - - cols += 1; - - try { - terminalInstance.current.resize(cols, rows); - socketRef.current.emit("resize", { cols, rows }); - } catch (e) { - terminalInstance.current.resize(originalCols, originalRows); - socketRef.current.emit("resize", { cols: originalCols, rows: originalRows }); - } - } + fitAddon.current.fit(); + if (socketRef.current && terminalInstance.current) { + const { cols, rows } = terminalInstance.current; + socketRef.current.emit("resize", { cols, rows }); } }); }; @@ -64,8 +50,6 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde fontSize: 14, scrollback: 1000, ignoreBracketedPasteMode: true, - fastScrollModifier: 'alt', - fastScrollSensitivity: 5, letterSpacing: 0, lineHeight: 1, padding: 2, @@ -81,19 +65,26 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde { path: "/ssh.io/socket.io", transports: ["websocket", "polling"], + reconnection: true, + reconnectionAttempts: 5, + reconnectionDelay: 1000, + timeout: 20000, } ); socketRef.current = socket; socket.on("connect_error", (error) => { terminalInstance.current.write(`\r\n*** Socket connection error: ${error.message} ***\r\n`); + console.error("Socket connection error:", error); }); socket.on("connect_timeout", () => { terminalInstance.current.write(`\r\n*** Socket connection timeout ***\r\n`); + console.error("Socket connection timeout"); }); socket.on("error", (err) => { + console.error("SSH connection error:", err); const isAuthError = err.toLowerCase().includes("authentication") || err.toLowerCase().includes("auth"); if (isAuthError && !hostConfig.password?.trim() && !hostConfig.sshKey?.trim() && !authModalShown) { authModalShown = true; @@ -103,23 +94,37 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde }); socket.on("connect", () => { + console.log("Socket connected, attempting SSH connection..."); + fitAddon.current.fit(); resizeTerminal(); const { cols, rows } = terminalInstance.current; + // Check for authentication details if (!hostConfig.password?.trim() && !hostConfig.sshKey?.trim()) { + console.log("No authentication provided, showing modal"); setIsNoAuthHidden(false); return; } + // Ensure we have proper SSH config with both key field names for backward compatibility const sshConfig = { ip: hostConfig.ip, user: hostConfig.user, port: Number(hostConfig.port) || 22, password: hostConfig.password?.trim(), - sshKey: hostConfig.sshKey?.trim() + sshKey: hostConfig.sshKey?.trim(), + rsaKey: hostConfig.sshKey?.trim() || hostConfig.rsaKey?.trim(), }; + console.log("Connecting to SSH with config:", { + ip: sshConfig.ip, + user: sshConfig.user, + port: sshConfig.port, + hasPassword: !!sshConfig.password, + hasKey: !!sshConfig.sshKey, + }); + socket.emit("connectToHost", cols, rows, sshConfig); }); @@ -134,138 +139,38 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde terminalInstance.current.write(decoder.decode(new Uint8Array(data))); }); - terminalInstance.current.onData((data) => { - if (data.length === 1) { - socketRef.current.emit("data", data); - return; - } + let isPasting = false; - if (socketRef.current) { + terminalInstance.current.onData((data) => { + if (socketRef.current && socketRef.current.connected) { socketRef.current.emit("data", data); } }); - const getClipboardText = async () => { - try { - // Modern Clipboard API - this will work in secure contexts (HTTPS or localhost) - if (navigator.clipboard && navigator.clipboard.readText) { - try { - return await navigator.clipboard.readText(); - } catch (clipboardErr) { - console.warn("Navigator clipboard API failed:", clipboardErr); - // Continue to fallback methods - } - } - - // Fallback method using document.execCommand - if (document.queryCommandSupported && document.queryCommandSupported('paste')) { - const textarea = document.createElement('textarea'); - textarea.style.position = 'fixed'; - textarea.style.opacity = '0'; - document.body.appendChild(textarea); - textarea.focus(); - - try { - document.execCommand('paste'); - const text = textarea.value; - document.body.removeChild(textarea); - if (text) return text; - } catch (execErr) { - document.body.removeChild(textarea); - console.warn("execCommand paste failed:", execErr); - // Continue to next fallback - } - } - - // Fallback UI prompt for non-secure contexts where clipboard API is restricted - if (!window.location.hostname.includes('localhost') && - !window.location.protocol.includes('https')) { - - // Display input prompt in the terminal itself - terminalInstance.current.write("\r\n\r\nPaste access denied. Please type or paste content here:\r\n"); - - // Use a terminal-based input method - return new Promise(resolve => { - let inputText = ''; - const dataHandler = terminalInstance.current.onData(data => { - // Check for enter key (carriage return) - if (data === '\r') { - terminalInstance.current.write('\r\n'); - dataHandler.dispose(); // Remove the handler - resolve(inputText); - return; - } - - // Handle backspace - if (data === '\x7f') { - if (inputText.length > 0) { - inputText = inputText.slice(0, -1); - terminalInstance.current.write('\b \b'); // Erase the character - } - return; - } - - // Normal character input - inputText += data; - terminalInstance.current.write(data); - }); - }); - } - - throw new Error('No clipboard access methods available'); - } catch (err) { - console.error("Failed to read clipboard contents:", err); - return null; - } - }; - - // Track if paste is in progress to prevent double paste - let pasteInProgress = false; - - terminalInstance.current.attachCustomKeyEventHandler(async (event) => { + terminalInstance.current.attachCustomKeyEventHandler((event) => { if ((event.ctrlKey || event.metaKey) && event.key === "v") { + if (isPasting) return false; + isPasting = true; + event.preventDefault(); - // Prevent double paste execution - if (pasteInProgress || !socketRef.current) return false; - - pasteInProgress = true; - - try { - const text = await getClipboardText(); - if (!text) { - terminalInstance.current.write("\r\nClipboard access denied or empty\r\n"); - pasteInProgress = false; - return false; - } + navigator.clipboard.readText().then((text) => { + text = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); - const lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n"); - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; + if (socketRef.current && socketRef.current.connected) { + const processedText = text.replace(/\n/g, "\r"); + socketRef.current.emit("data", processedText); - if (i === 0) { - socketRef.current.emit("data", line); - } - else if (i < lines.length - 1) { - socketRef.current.emit("data", "\r" + line); - } - else if (i > 0) { - const endsWithNewline = text.endsWith("\n") || text.endsWith("\r\n") || text.endsWith("\r"); - socketRef.current.emit("data", "\r" + line + (endsWithNewline ? "\r" : "")); - } - else if (lines.length === 1 && (text.endsWith("\n") || text.endsWith("\r\n") || text.endsWith("\r"))) { - socketRef.current.emit("data", line + "\r"); - } + setTimeout(() => { + isPasting = false; + }, 50); + } else { + isPasting = false; } - } catch (err) { - console.error("Failed to process clipboard contents:", err); - } - - // Set timeout to reset paste lock - setTimeout(() => { - pasteInProgress = false; - }, 100); + }).catch((err) => { + console.error("Failed to read clipboard contents:", err); + isPasting = false; + }); return false; } @@ -273,43 +178,11 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde return true; }); - const setClipboardText = (text) => { - try { - if (navigator.clipboard && navigator.clipboard.writeText) { - navigator.clipboard.writeText(text); - return true; - } - - if (document.queryCommandSupported && document.queryCommandSupported('copy')) { - const textarea = document.createElement('textarea'); - textarea.value = text; - textarea.style.position = 'fixed'; - textarea.style.opacity = '0'; - document.body.appendChild(textarea); - textarea.select(); - - try { - document.execCommand('copy'); - document.body.removeChild(textarea); - return true; - } catch (e) { - document.body.removeChild(textarea); - return false; - } - } - - return false; - } catch (err) { - console.error("Failed to write to clipboard:", err); - return false; - } - }; - terminalInstance.current.onKey(({ domEvent }) => { if (domEvent.key === "c" && (domEvent.ctrlKey || domEvent.metaKey)) { const selection = terminalInstance.current.getSelection(); if (selection) { - setClipboardText(selection); + navigator.clipboard.writeText(selection); } } }); @@ -323,8 +196,25 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde } }); + socket.on("disconnect", (reason) => { + console.log("Socket disconnected:", reason); + terminalInstance.current.write(`\r\n*** Socket disconnected: ${reason} ***\r\n`); + }); + + socket.on("reconnect", (attemptNumber) => { + console.log("Socket reconnected after", attemptNumber, "attempts"); + terminalInstance.current.write(`\r\n*** Socket reconnected after ${attemptNumber} attempts ***\r\n`); + }); + + socket.on("reconnect_error", (error) => { + console.error("Socket reconnect error:", error); + terminalInstance.current.write(`\r\n*** Socket reconnect error: ${error.message} ***\r\n`); + }); + const pingInterval = setInterval(() => { - socketRef.current.emit("ping"); + if (socketRef.current && socketRef.current.connected) { + socketRef.current.emit("ping"); + } }, 5000); socketRef.current.on("pong", () => { @@ -397,6 +287,7 @@ NewTerminal.propTypes = { user: PropTypes.string.isRequired, password: PropTypes.string, sshKey: PropTypes.string, + rsaKey: PropTypes.string, port: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, }).isRequired, isVisible: PropTypes.bool.isRequired, -- 2.49.1 From 2295ac9b705b58c6614299aed96963fd2ed4c175 Mon Sep 17 00:00:00 2001 From: LukeGus Date: Sun, 23 Mar 2025 02:16:26 -0500 Subject: [PATCH 44/47] Optimized pasting --- src/apps/ssh/Terminal.jsx | 85 +++++++++++++++++++++++++++------------ 1 file changed, 59 insertions(+), 26 deletions(-) diff --git a/src/apps/ssh/Terminal.jsx b/src/apps/ssh/Terminal.jsx index 3011d35e..7a254b2f 100644 --- a/src/apps/ssh/Terminal.jsx +++ b/src/apps/ssh/Terminal.jsx @@ -117,14 +117,6 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde rsaKey: hostConfig.sshKey?.trim() || hostConfig.rsaKey?.trim(), }; - console.log("Connecting to SSH with config:", { - ip: sshConfig.ip, - user: sshConfig.user, - port: sshConfig.port, - hasPassword: !!sshConfig.password, - hasKey: !!sshConfig.sshKey, - }); - socket.emit("connectToHost", cols, rows, sshConfig); }); @@ -154,23 +146,30 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde event.preventDefault(); - navigator.clipboard.readText().then((text) => { - text = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); - - if (socketRef.current && socketRef.current.connected) { - const processedText = text.replace(/\n/g, "\r"); - socketRef.current.emit("data", processedText); + // Check if clipboard API is available + if (navigator.clipboard && navigator.clipboard.readText) { + navigator.clipboard.readText().then((text) => { + text = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); - setTimeout(() => { + if (socketRef.current && socketRef.current.connected) { + const processedText = text.replace(/\n/g, "\r"); + socketRef.current.emit("data", processedText); + + setTimeout(() => { + isPasting = false; + }, 50); + } else { isPasting = false; - }, 50); - } else { - isPasting = false; - } - }).catch((err) => { - console.error("Failed to read clipboard contents:", err); - isPasting = false; - }); + } + }).catch((err) => { + console.error("Failed to read clipboard contents:", err); + // Try to handle paste manually using execCommand for fallback + tryFallbackPaste(); + }); + } else { + // Fallback for browsers where clipboard API is not yet available + tryFallbackPaste(); + } return false; } @@ -178,6 +177,42 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde return true; }); + // Add fallback paste method using execCommand or input element + const tryFallbackPaste = () => { + try { + // Create temporary textarea for paste operation + const textarea = document.createElement('textarea'); + textarea.style.position = 'absolute'; + textarea.style.left = '-9999px'; + textarea.style.top = '0px'; + document.body.appendChild(textarea); + textarea.focus(); + + // Try execCommand paste (works in some browsers) + const successful = document.execCommand('paste'); + if (successful) { + const text = textarea.value; + if (text && socketRef.current && socketRef.current.connected) { + const processedText = text.replace(/\r\n/g, "\r").replace(/\n/g, "\r"); + socketRef.current.emit("data", processedText); + } + } else { + console.log("Fallback paste failed, clipboard permissions may be needed"); + terminalInstance.current.write("\r\n*** Paste failed: Please try again or grant clipboard permissions ***\r\n"); + } + + // Clean up + document.body.removeChild(textarea); + setTimeout(() => { + isPasting = false; + }, 50); + } catch (err) { + console.error("Fallback paste failed:", err); + terminalInstance.current.write("\r\n*** Paste failed: Try clicking in the terminal first ***\r\n"); + isPasting = false; + } + }; + terminalInstance.current.onKey(({ domEvent }) => { if (domEvent.key === "c" && (domEvent.ctrlKey || domEvent.metaKey)) { const selection = terminalInstance.current.getSelection(); @@ -217,9 +252,7 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde } }, 5000); - socketRef.current.on("pong", () => { - console.log("Received pong from server."); - }); + socketRef.current.on("pong", () => {}); return () => { clearInterval(pingInterval); -- 2.49.1 From ea631bd02343e4e23857665184075ad3a864bcef Mon Sep 17 00:00:00 2001 From: LukeGus Date: Sun, 23 Mar 2025 02:37:24 -0500 Subject: [PATCH 45/47] Optimized pasting --- src/apps/ssh/Terminal.jsx | 271 +++++++++++++++++++++++++++++--------- 1 file changed, 211 insertions(+), 60 deletions(-) diff --git a/src/apps/ssh/Terminal.jsx b/src/apps/ssh/Terminal.jsx index 7a254b2f..41f0c2d9 100644 --- a/src/apps/ssh/Terminal.jsx +++ b/src/apps/ssh/Terminal.jsx @@ -146,78 +146,108 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde event.preventDefault(); - // Check if clipboard API is available - if (navigator.clipboard && navigator.clipboard.readText) { - navigator.clipboard.readText().then((text) => { - text = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); - - if (socketRef.current && socketRef.current.connected) { - const processedText = text.replace(/\n/g, "\r"); - socketRef.current.emit("data", processedText); - - setTimeout(() => { - isPasting = false; - }, 50); - } else { - isPasting = false; + // Use a multi-layered approach for clipboard access + const pasteFromClipboard = async () => { + try { + // Try modern Clipboard API first + if (navigator.clipboard && navigator.clipboard.readText) { + try { + const text = await navigator.clipboard.readText(); + if (text && socketRef.current?.connected) { + const processedText = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n/g, "\r"); + socketRef.current.emit("data", processedText); + return true; + } + } catch (clipboardErr) { + console.warn("Clipboard API failed:", clipboardErr); + // Continue to fallbacks + } } - }).catch((err) => { - console.error("Failed to read clipboard contents:", err); - // Try to handle paste manually using execCommand for fallback - tryFallbackPaste(); - }); - } else { - // Fallback for browsers where clipboard API is not yet available - tryFallbackPaste(); - } + // Try execCommand fallback + if (document.queryCommandSupported && document.queryCommandSupported('paste')) { + const textarea = document.createElement('textarea'); + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + textarea.focus(); + + try { + const successful = document.execCommand('paste'); + if (successful) { + const text = textarea.value; + if (text && socketRef.current?.connected) { + const processedText = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n/g, "\r"); + socketRef.current.emit("data", processedText); + document.body.removeChild(textarea); + return true; + } + } + } catch (execErr) { + console.warn("execCommand paste failed:", execErr); + } + document.body.removeChild(textarea); + } + + // Show permissions warning and instructions + terminalInstance.current.write("\r\n*** To paste: Right-click in terminal and select Paste from context menu ***\r\n"); + return false; + } finally { + setTimeout(() => { + isPasting = false; + }, 100); + } + }; + + pasteFromClipboard(); return false; } return true; }); - // Add fallback paste method using execCommand or input element - const tryFallbackPaste = () => { - try { - // Create temporary textarea for paste operation - const textarea = document.createElement('textarea'); - textarea.style.position = 'absolute'; - textarea.style.left = '-9999px'; - textarea.style.top = '0px'; - document.body.appendChild(textarea); - textarea.focus(); - - // Try execCommand paste (works in some browsers) - const successful = document.execCommand('paste'); - if (successful) { - const text = textarea.value; - if (text && socketRef.current && socketRef.current.connected) { - const processedText = text.replace(/\r\n/g, "\r").replace(/\n/g, "\r"); - socketRef.current.emit("data", processedText); - } - } else { - console.log("Fallback paste failed, clipboard permissions may be needed"); - terminalInstance.current.write("\r\n*** Paste failed: Please try again or grant clipboard permissions ***\r\n"); - } - - // Clean up - document.body.removeChild(textarea); - setTimeout(() => { - isPasting = false; - }, 50); - } catch (err) { - console.error("Fallback paste failed:", err); - terminalInstance.current.write("\r\n*** Paste failed: Try clicking in the terminal first ***\r\n"); - isPasting = false; - } - }; - terminalInstance.current.onKey(({ domEvent }) => { if (domEvent.key === "c" && (domEvent.ctrlKey || domEvent.metaKey)) { const selection = terminalInstance.current.getSelection(); if (selection) { - navigator.clipboard.writeText(selection); + // Use a try-catch to handle clipboard failures + try { + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(selection) + .catch(err => { + console.warn("Clipboard write failed:", err); + terminalInstance.current.write("\r\n*** Copy failed: Text copied to internal buffer ***\r\n"); + // Store selection in a variable as fallback + window.termixInternalClipboard = selection; + }); + } else { + // Fallback for browsers without clipboard API + const textarea = document.createElement('textarea'); + textarea.value = selection; + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + textarea.select(); + + try { + const successful = document.execCommand('copy'); + if (!successful) { + terminalInstance.current.write("\r\n*** Copy failed: Text copied to internal buffer ***\r\n"); + window.termixInternalClipboard = selection; + } + } catch (err) { + console.warn("execCommand copy failed:", err); + terminalInstance.current.write("\r\n*** Copy failed: Text copied to internal buffer ***\r\n"); + window.termixInternalClipboard = selection; + } + + document.body.removeChild(textarea); + } + } catch (err) { + console.error("Copy failed:", err); + terminalInstance.current.write("\r\n*** Copy failed: Text copied to internal buffer ***\r\n"); + window.termixInternalClipboard = selection; + } } } }); @@ -254,6 +284,127 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde socketRef.current.on("pong", () => {}); + // Add right-click context menu for paste + const element = terminalInstance.current.element; + if (element) { + element.addEventListener('contextmenu', (event) => { + event.preventDefault(); + + // Create and show context menu + const contextMenu = document.createElement('div'); + contextMenu.className = 'terminal-context-menu'; + contextMenu.style.position = 'fixed'; + contextMenu.style.left = `${event.clientX}px`; + contextMenu.style.top = `${event.clientY}px`; + contextMenu.style.backgroundColor = '#1e1e1e'; + contextMenu.style.border = '1px solid #555'; + contextMenu.style.borderRadius = '4px'; + contextMenu.style.padding = '4px 0'; + contextMenu.style.boxShadow = '0 2px 10px rgba(0,0,0,0.2)'; + contextMenu.style.zIndex = '1000'; + + // Create copy option + const copyOption = document.createElement('div'); + copyOption.innerText = 'Copy'; + copyOption.className = 'terminal-context-menu-item'; + copyOption.style.padding = '6px 12px'; + copyOption.style.cursor = 'pointer'; + copyOption.style.color = 'white'; + copyOption.style.fontSize = '14px'; + copyOption.onmouseover = () => { + copyOption.style.backgroundColor = '#3a3a3a'; + }; + copyOption.onmouseout = () => { + copyOption.style.backgroundColor = 'transparent'; + }; + + // Handle copy action + copyOption.onclick = () => { + const selection = terminalInstance.current.getSelection(); + if (selection) { + // Try to copy using clipboard API + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(selection) + .catch(err => { + console.warn("Clipboard write failed:", err); + window.termixInternalClipboard = selection; + terminalInstance.current.write("\r\n*** Copied to internal clipboard ***\r\n"); + }); + } else { + // Store in internal clipboard + window.termixInternalClipboard = selection; + terminalInstance.current.write("\r\n*** Copied to internal clipboard ***\r\n"); + } + } + document.body.removeChild(contextMenu); + }; + + // Create paste option + const pasteOption = document.createElement('div'); + pasteOption.innerText = 'Paste'; + pasteOption.className = 'terminal-context-menu-item'; + pasteOption.style.padding = '6px 12px'; + pasteOption.style.cursor = 'pointer'; + pasteOption.style.color = 'white'; + pasteOption.style.fontSize = '14px'; + pasteOption.onmouseover = () => { + pasteOption.style.backgroundColor = '#3a3a3a'; + }; + pasteOption.onmouseout = () => { + pasteOption.style.backgroundColor = 'transparent'; + }; + + // Handle paste action + pasteOption.onclick = async () => { + try { + // Try clipboard API first + if (navigator.clipboard && navigator.clipboard.readText) { + try { + const text = await navigator.clipboard.readText(); + if (text && socketRef.current?.connected) { + const processedText = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n/g, "\r"); + socketRef.current.emit("data", processedText); + } + } catch (err) { + // Use fallback or internal clipboard + if (window.termixInternalClipboard) { + const processedText = window.termixInternalClipboard.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n/g, "\r"); + socketRef.current.emit("data", processedText); + } else { + terminalInstance.current.write("\r\n*** Paste failed: No clipboard content available ***\r\n"); + } + } + } else if (window.termixInternalClipboard) { + // Use internal clipboard if available + const processedText = window.termixInternalClipboard.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n/g, "\r"); + socketRef.current.emit("data", processedText); + } else { + terminalInstance.current.write("\r\n*** Paste failed: No clipboard content available ***\r\n"); + } + } finally { + document.body.removeChild(contextMenu); + } + }; + + // Add options to menu + contextMenu.appendChild(copyOption); + contextMenu.appendChild(pasteOption); + document.body.appendChild(contextMenu); + + // Remove menu when clicking elsewhere + const removeMenu = (e) => { + if (!contextMenu.contains(e.target)) { + document.body.removeChild(contextMenu); + document.removeEventListener('click', removeMenu); + } + }; + + setTimeout(() => { + document.addEventListener('click', removeMenu); + }, 0); + }); + } + return () => { clearInterval(pingInterval); if (terminalInstance.current) { -- 2.49.1 From 4d336136795c506344204a17e6d21802605a1843 Mon Sep 17 00:00:00 2001 From: LukeGus Date: Sun, 23 Mar 2025 21:16:51 -0500 Subject: [PATCH 46/47] Optimized pasting, fixed host naming. --- package-lock.json | 234 +++++++++++++------------- package.json | 4 +- src/App.jsx | 146 +++++++++++------ src/apps/ssh/Terminal.jsx | 235 +++++++++------------------ src/apps/user/User.jsx | 33 ++++ src/backend/database.cjs | 62 ++++++- src/modals/AddHostModal.jsx | 40 ++++- src/modals/EditHostModal.jsx | 165 +++++++++++-------- src/modals/NoAuthenticationModal.jsx | 35 ++-- 9 files changed, 540 insertions(+), 414 deletions(-) diff --git a/package-lock.json b/package-lock.json index d54d6ee6..43a23c8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@fontsource/inter": "^5.1.1", "@mui/icons-material": "^6.4.7", "@mui/joy": "^5.0.0-beta.51", - "@tailwindcss/vite": "^4.0.8", + "@tailwindcss/vite": "^4.0.15", "@tiptap/extension-link": "^2.11.5", "@tiptap/pm": "^2.11.5", "@tiptap/react": "^2.11.5", @@ -42,7 +42,7 @@ "socket.io": "^4.8.1", "socket.io-client": "^4.8.1", "ssh2": "^1.16.0", - "tailwindcss": "^4.0.8" + "tailwindcss": "^4.0.15" }, "devDependencies": { "@eslint/js": "^9.17.0", @@ -1226,15 +1226,6 @@ "node-pre-gyp": "bin/node-pre-gyp" } }, - "node_modules/@mapbox/node-pre-gyp/node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -2012,42 +2003,42 @@ "license": "MIT" }, "node_modules/@tailwindcss/node": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.8.tgz", - "integrity": "sha512-FKArQpbrbwv08TNT0k7ejYXpF+R8knZFAatNc0acOxbgeqLzwb86r+P3LGOjIeI3Idqe9CVkZrh4GlsJLJKkkw==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.15.tgz", + "integrity": "sha512-IODaJjNmiasfZX3IoS+4Em3iu0fD2HS0/tgrnkYfW4hyUor01Smnr5eY3jc4rRgaTDrJlDmBTHbFO0ETTDaxWA==", "license": "MIT", "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", - "tailwindcss": "4.0.8" + "tailwindcss": "4.0.15" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.0.8.tgz", - "integrity": "sha512-KfMcuAu/Iw+DcV1e8twrFyr2yN8/ZDC/odIGta4wuuJOGkrkHZbvJvRNIbQNhGh7erZTYV6Ie0IeD6WC9Y8Hcw==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.0.15.tgz", + "integrity": "sha512-e0uHrKfPu7JJGMfjwVNyt5M0u+OP8kUmhACwIRlM+JNBuReDVQ63yAD1NWe5DwJtdaHjugNBil76j+ks3zlk6g==", "license": "MIT", "engines": { "node": ">= 10" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.0.8", - "@tailwindcss/oxide-darwin-arm64": "4.0.8", - "@tailwindcss/oxide-darwin-x64": "4.0.8", - "@tailwindcss/oxide-freebsd-x64": "4.0.8", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.8", - "@tailwindcss/oxide-linux-arm64-gnu": "4.0.8", - "@tailwindcss/oxide-linux-arm64-musl": "4.0.8", - "@tailwindcss/oxide-linux-x64-gnu": "4.0.8", - "@tailwindcss/oxide-linux-x64-musl": "4.0.8", - "@tailwindcss/oxide-win32-arm64-msvc": "4.0.8", - "@tailwindcss/oxide-win32-x64-msvc": "4.0.8" + "@tailwindcss/oxide-android-arm64": "4.0.15", + "@tailwindcss/oxide-darwin-arm64": "4.0.15", + "@tailwindcss/oxide-darwin-x64": "4.0.15", + "@tailwindcss/oxide-freebsd-x64": "4.0.15", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.15", + "@tailwindcss/oxide-linux-arm64-gnu": "4.0.15", + "@tailwindcss/oxide-linux-arm64-musl": "4.0.15", + "@tailwindcss/oxide-linux-x64-gnu": "4.0.15", + "@tailwindcss/oxide-linux-x64-musl": "4.0.15", + "@tailwindcss/oxide-win32-arm64-msvc": "4.0.15", + "@tailwindcss/oxide-win32-x64-msvc": "4.0.15" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.8.tgz", - "integrity": "sha512-We7K79+Sm4mwJHk26Yzu/GAj7C7myemm7PeXvpgMxyxO70SSFSL3uCcqFbz9JA5M5UPkrl7N9fkBe/Y0iazqpA==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.15.tgz", + "integrity": "sha512-EBuyfSKkom7N+CB3A+7c0m4+qzKuiN0WCvzPvj5ZoRu4NlQadg/mthc1tl5k9b5ffRGsbDvP4k21azU4VwVk3Q==", "cpu": [ "arm64" ], @@ -2061,9 +2052,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.8.tgz", - "integrity": "sha512-Lv9Isi2EwkCTG1sRHNDi0uRNN1UGFdEThUAGFrydRmQZnraGLMjN8gahzg2FFnOizDl7LB2TykLUuiw833DSNg==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.15.tgz", + "integrity": "sha512-ObVAnEpLepMhV9VoO0JSit66jiN5C4YCqW3TflsE9boo2Z7FIjV80RFbgeL2opBhtxbaNEDa6D0/hq/EP03kgQ==", "cpu": [ "arm64" ], @@ -2077,9 +2068,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.8.tgz", - "integrity": "sha512-fWfywfYIlSWtKoqWTjukTHLWV3ARaBRjXCC2Eo0l6KVpaqGY4c2y8snUjp1xpxUtpqwMvCvFWFaleMoz1Vhzlw==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.15.tgz", + "integrity": "sha512-IElwoFhUinOr9MyKmGTPNi1Rwdh68JReFgYWibPWTGuevkHkLWKEflZc2jtI5lWZ5U9JjUnUfnY43I4fEXrc4g==", "cpu": [ "x64" ], @@ -2093,9 +2084,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.8.tgz", - "integrity": "sha512-SO+dyvjJV9G94bnmq2288Ke0BIdvrbSbvtPLaQdqjqHR83v5L2fWADyFO+1oecHo9Owsk8MxcXh1agGVPIKIqw==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.15.tgz", + "integrity": "sha512-6BLLqyx7SIYRBOnTZ8wgfXANLJV5TQd3PevRJZp0vn42eO58A2LykRKdvL1qyPfdpmEVtF+uVOEZ4QTMqDRAWA==", "cpu": [ "x64" ], @@ -2109,9 +2100,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.8.tgz", - "integrity": "sha512-ZSHggWiEblQNV69V0qUK5vuAtHP+I+S2eGrKGJ5lPgwgJeAd6GjLsVBN+Mqn2SPVfYM3BOpS9jX/zVg9RWQVDQ==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.15.tgz", + "integrity": "sha512-Zy63EVqO9241Pfg6G0IlRIWyY5vNcWrL5dd2WAKVJZRQVeolXEf1KfjkyeAAlErDj72cnyXObEZjMoPEKHpdNw==", "cpu": [ "arm" ], @@ -2125,9 +2116,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.8.tgz", - "integrity": "sha512-xWpr6M0OZLDNsr7+bQz+3X7zcnDJZJ1N9gtBWCtfhkEtDjjxYEp+Lr5L5nc/yXlL4MyCHnn0uonGVXy3fhxaVA==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.15.tgz", + "integrity": "sha512-2NemGQeaTbtIp1Z2wyerbVEJZTkAWhMDOhhR5z/zJ75yMNf8yLnE+sAlyf6yGDNr+1RqvWrRhhCFt7i0CIxe4Q==", "cpu": [ "arm64" ], @@ -2141,9 +2132,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.8.tgz", - "integrity": "sha512-5tz2IL7LN58ssGEq7h/staD7pu/izF/KeMWdlJ86WDe2Ah46LF3ET6ZGKTr5eZMrnEA0M9cVFuSPprKRHNgjeg==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.15.tgz", + "integrity": "sha512-342GVnhH/6PkVgKtEzvNVuQ4D+Q7B7qplvuH20Cfz9qEtydG6IQczTZ5IT4JPlh931MG1NUCVxg+CIorr1WJyw==", "cpu": [ "arm64" ], @@ -2157,9 +2148,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.8.tgz", - "integrity": "sha512-KSzMkhyrxAQyY2o194NKVKU9j/c+NFSoMvnHWFaNHKi3P1lb+Vq1UC19tLHrmxSkKapcMMu69D7+G1+FVGNDXQ==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.15.tgz", + "integrity": "sha512-g76GxlKH124RuGqacCEFc2nbzRl7bBrlC8qDQMiUABkiifDRHOIUjgKbLNG4RuR9hQAD/MKsqZ7A8L08zsoBrw==", "cpu": [ "x64" ], @@ -2173,9 +2164,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.8.tgz", - "integrity": "sha512-yFYKG5UtHTRimjtqxUWXBgI4Tc6NJe3USjRIVdlTczpLRxq/SFwgzGl5JbatCxgSRDPBFwRrNPxq+ukfQFGdrw==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.15.tgz", + "integrity": "sha512-Gg/Y1XrKEvKpq6WeNt2h8rMIKOBj/W3mNa5NMvkQgMC7iO0+UNLrYmt6zgZufht66HozNpn+tJMbbkZ5a3LczA==", "cpu": [ "x64" ], @@ -2189,9 +2180,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.8.tgz", - "integrity": "sha512-tndGujmCSba85cRCnQzXgpA2jx5gXimyspsUYae5jlPyLRG0RjXbDshFKOheVXU4TLflo7FSG8EHCBJ0EHTKdQ==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.15.tgz", + "integrity": "sha512-7QtSSJwYZ7ZK1phVgcNZpuf7c7gaCj8Wb0xjliligT5qCGCp79OV2n3SJummVZdw4fbTNKUOYMO7m1GinppZyA==", "cpu": [ "arm64" ], @@ -2205,9 +2196,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.8.tgz", - "integrity": "sha512-T77jroAc0p4EHVVgTUiNeFn6Nj3jtD3IeNId2X+0k+N1XxfNipy81BEkYErpKLiOkNhpNFjPee8/ZVas29b2OQ==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.15.tgz", + "integrity": "sha512-JQ5H+5MLhOjpgNp6KomouE0ZuKmk3hO5h7/ClMNAQ8gZI2zkli3IH8ZqLbd2DVfXDbdxN2xvooIEeIlkIoSCqw==", "cpu": [ "x64" ], @@ -2221,15 +2212,15 @@ } }, "node_modules/@tailwindcss/vite": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.0.8.tgz", - "integrity": "sha512-+SAq44yLzYlzyrb7QTcFCdU8Xa7FOA0jp+Xby7fPMUie+MY9HhJysM7Vp+vL8qIp8ceQJfLD+FjgJuJ4lL6nyg==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.0.15.tgz", + "integrity": "sha512-JRexava80NijI8cTcLXNM3nQL5A0ptTHI8oJLLe8z1MpNB6p5J4WCdJJP8RoyHu8/eB1JzEdbpH86eGfbuaezQ==", "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.0.8", - "@tailwindcss/oxide": "4.0.8", - "lightningcss": "^1.29.1", - "tailwindcss": "4.0.8" + "@tailwindcss/node": "4.0.15", + "@tailwindcss/oxide": "4.0.15", + "lightningcss": "1.29.2", + "tailwindcss": "4.0.15" }, "peerDependencies": { "vite": "^5.2.0 || ^6" @@ -3933,15 +3924,12 @@ } }, "node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", "license": "Apache-2.0", - "bin": { - "detect-libc": "bin/detect-libc.js" - }, "engines": { - "node": ">=0.10" + "node": ">=8" } }, "node_modules/doctrine": { @@ -5881,12 +5869,12 @@ } }, "node_modules/lightningcss": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.1.tgz", - "integrity": "sha512-FmGoeD4S05ewj+AkhTY+D+myDvXI6eL27FjHIjoyUkO/uw7WZD1fBVs0QxeYWa7E17CUHJaYX/RUGISCtcrG4Q==", + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz", + "integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==", "license": "MPL-2.0", "dependencies": { - "detect-libc": "^1.0.3" + "detect-libc": "^2.0.3" }, "engines": { "node": ">= 12.0.0" @@ -5896,22 +5884,22 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-darwin-arm64": "1.29.1", - "lightningcss-darwin-x64": "1.29.1", - "lightningcss-freebsd-x64": "1.29.1", - "lightningcss-linux-arm-gnueabihf": "1.29.1", - "lightningcss-linux-arm64-gnu": "1.29.1", - "lightningcss-linux-arm64-musl": "1.29.1", - "lightningcss-linux-x64-gnu": "1.29.1", - "lightningcss-linux-x64-musl": "1.29.1", - "lightningcss-win32-arm64-msvc": "1.29.1", - "lightningcss-win32-x64-msvc": "1.29.1" + "lightningcss-darwin-arm64": "1.29.2", + "lightningcss-darwin-x64": "1.29.2", + "lightningcss-freebsd-x64": "1.29.2", + "lightningcss-linux-arm-gnueabihf": "1.29.2", + "lightningcss-linux-arm64-gnu": "1.29.2", + "lightningcss-linux-arm64-musl": "1.29.2", + "lightningcss-linux-x64-gnu": "1.29.2", + "lightningcss-linux-x64-musl": "1.29.2", + "lightningcss-win32-arm64-msvc": "1.29.2", + "lightningcss-win32-x64-msvc": "1.29.2" } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.1.tgz", - "integrity": "sha512-HtR5XJ5A0lvCqYAoSv2QdZZyoHNttBpa5EP9aNuzBQeKGfbyH5+UipLWvVzpP4Uml5ej4BYs5I9Lco9u1fECqw==", + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz", + "integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==", "cpu": [ "arm64" ], @@ -5929,9 +5917,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.1.tgz", - "integrity": "sha512-k33G9IzKUpHy/J/3+9MCO4e+PzaFblsgBjSGlpAaFikeBFm8B/CkO3cKU9oI4g+fjS2KlkLM/Bza9K/aw8wsNA==", + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz", + "integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==", "cpu": [ "x64" ], @@ -5949,9 +5937,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.1.tgz", - "integrity": "sha512-0SUW22fv/8kln2LnIdOCmSuXnxgxVC276W5KLTwoehiO0hxkacBxjHOL5EtHD8BAXg2BvuhsJPmVMasvby3LiQ==", + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz", + "integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==", "cpu": [ "x64" ], @@ -5969,9 +5957,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.1.tgz", - "integrity": "sha512-sD32pFvlR0kDlqsOZmYqH/68SqUMPNj+0pucGxToXZi4XZgZmqeX/NkxNKCPsswAXU3UeYgDSpGhu05eAufjDg==", + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz", + "integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==", "cpu": [ "arm" ], @@ -5989,9 +5977,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.1.tgz", - "integrity": "sha512-0+vClRIZ6mmJl/dxGuRsE197o1HDEeeRk6nzycSy2GofC2JsY4ifCRnvUWf/CUBQmlrvMzt6SMQNMSEu22csWQ==", + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz", + "integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==", "cpu": [ "arm64" ], @@ -6009,9 +5997,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.1.tgz", - "integrity": "sha512-UKMFrG4rL/uHNgelBsDwJcBqVpzNJbzsKkbI3Ja5fg00sgQnHw/VrzUTEc4jhZ+AN2BvQYz/tkHu4vt1kLuJyw==", + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz", + "integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==", "cpu": [ "arm64" ], @@ -6029,9 +6017,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.1.tgz", - "integrity": "sha512-u1S+xdODy/eEtjADqirA774y3jLcm8RPtYztwReEXoZKdzgsHYPl0s5V52Tst+GKzqjebkULT86XMSxejzfISw==", + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz", + "integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==", "cpu": [ "x64" ], @@ -6049,9 +6037,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.1.tgz", - "integrity": "sha512-L0Tx0DtaNUTzXv0lbGCLB/c/qEADanHbu4QdcNOXLIe1i8i22rZRpbT3gpWYsCh9aSL9zFujY/WmEXIatWvXbw==", + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz", + "integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==", "cpu": [ "x64" ], @@ -6069,9 +6057,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.1.tgz", - "integrity": "sha512-QoOVnkIEFfbW4xPi+dpdft/zAKmgLgsRHfJalEPYuJDOWf7cLQzYg0DEh8/sn737FaeMJxHZRc1oBreiwZCjog==", + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz", + "integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==", "cpu": [ "arm64" ], @@ -6089,9 +6077,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.1.tgz", - "integrity": "sha512-NygcbThNBe4JElP+olyTI/doBNGJvLs3bFCRPdvuCcxZCcCZ71B858IHpdm7L1btZex0FvCmM17FK98Y9MRy1Q==", + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.2.tgz", + "integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==", "cpu": [ "x64" ], @@ -8225,9 +8213,9 @@ } }, "node_modules/tailwindcss": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.8.tgz", - "integrity": "sha512-Me7N5CKR+D2A1xdWA5t5+kjjT7bwnxZOE6/yDI/ixJdJokszsn2n++mdU5yJwrsTpqFX2B9ZNMBJDwcqk9C9lw==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.15.tgz", + "integrity": "sha512-6ZMg+hHdMJpjpeCCFasX7K+U615U9D+7k5/cDK/iRwl6GptF24+I/AbKgOnXhVKePzrEyIXutLv36n4cRsq3Sg==", "license": "MIT" }, "node_modules/tapable": { diff --git a/package.json b/package.json index 5a14a389..0d46cd7b 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "@fontsource/inter": "^5.1.1", "@mui/icons-material": "^6.4.7", "@mui/joy": "^5.0.0-beta.51", - "@tailwindcss/vite": "^4.0.8", + "@tailwindcss/vite": "^4.0.15", "@tiptap/extension-link": "^2.11.5", "@tiptap/pm": "^2.11.5", "@tiptap/react": "^2.11.5", @@ -44,7 +44,7 @@ "socket.io": "^4.8.1", "socket.io-client": "^4.8.1", "ssh2": "^1.16.0", - "tailwindcss": "^4.0.8" + "tailwindcss": "^4.0.15" }, "devDependencies": { "@eslint/js": "^9.17.0", diff --git a/src/App.jsx b/src/App.jsx index 6cb4befb..36185043 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -39,6 +39,12 @@ function App() { authMethod: "Select Auth", rememberHost: true, storePassword: true, + connectionType: "ssh", + rdpDomain: "", + rdpWindowsAuthentication: true, + rdpConsole: false, + vncScaling: "100%", + vncQuality: "High" }); const [editHostForm, setEditHostForm] = useState({ name: "", @@ -223,34 +229,53 @@ function App() { }, []); const handleAddHost = () => { - if (addHostForm.ip && addHostForm.user && addHostForm.port) { + if (addHostForm.ip && addHostForm.port) { + if (addHostForm.connectionType === 'ssh' && !addHostForm.user) { + setErrorMessage("Please fill out all required fields (IP, User, Port)."); + setIsErrorHidden(false); + return; + } + if (!addHostForm.rememberHost) { connectToHost(); setIsAddHostHidden(true); return; } - if (addHostForm.authMethod === 'Select Auth') { - alert("Please select an authentication method."); - return; + if (addHostForm.connectionType === 'ssh') { + if (addHostForm.authMethod === 'Select Auth') { + setErrorMessage("Please select an authentication method."); + setIsErrorHidden(false); + return; + } + if (addHostForm.authMethod === 'password' && !addHostForm.password) { + setIsNoAuthHidden(false); + return; + } + if (addHostForm.authMethod === 'sshKey' && !addHostForm.sshKey) { + setIsNoAuthHidden(false); + return; + } } - if (addHostForm.authMethod === 'password' && !addHostForm.password) { - setIsNoAuthHidden(false); - return; - } - if (addHostForm.authMethod === 'sshKey' && !addHostForm.sshKey) { + else if (!addHostForm.password) { setIsNoAuthHidden(false); return; } - connectToHost(); - if (!addHostForm.storePassword) { - addHostForm.password = ''; + try { + connectToHost(); + if (!addHostForm.storePassword) { + addHostForm.password = ''; + } + handleSaveHost(); + setIsAddHostHidden(true); + } catch (error) { + setErrorMessage(error.message || "Failed to add host"); + setIsErrorHidden(false); } - handleSaveHost(); - setIsAddHostHidden(true); } else { - alert("Please fill out all required fields (IP, User, Port)."); + setErrorMessage("Please fill out all required fields."); + setIsErrorHidden(false); } }; @@ -275,25 +300,42 @@ function App() { setActiveTab(nextId); setNextId(nextId + 1); setIsAddHostHidden(true); - setAddHostForm({ name: "", folder: "", ip: "", user: "", password: "", sshKey: "", port: 22, authMethod: "Select Auth", rememberHost: true, storePassword: true }); + setAddHostForm({ name: "", folder: "", ip: "", user: "", password: "", sshKey: "", port: 22, authMethod: "Select Auth", rememberHost: true, storePassword: true, connectionType: "ssh", rdpDomain: "", rdpWindowsAuthentication: true, rdpConsole: false, vncScaling: "100%", vncQuality: "High" }); } const handleAuthSubmit = (form) => { - const updatedTerminals = terminals.map((terminal) => { - if (terminal.id === activeTab) { - return { - ...terminal, - hostConfig: { - ...terminal.hostConfig, - password: form.password, - sshKey: form.sshKey + try { + setIsNoAuthHidden(true); + + setTimeout(() => { + const updatedTerminals = terminals.map((terminal) => { + if (terminal.id === activeTab) { + return { + ...terminal, + hostConfig: { + ...terminal.hostConfig, + password: form.authMethod === 'password' ? form.password : undefined, + sshKey: form.authMethod === 'sshKey' ? form.sshKey : undefined + } + }; } - }; - } - return terminal; - }); - setTerminals(updatedTerminals); - setIsNoAuthHidden(true); + return terminal; + }); + + setTerminals(updatedTerminals); + + setNoAuthenticationForm({ + authMethod: 'Select Auth', + password: '', + sshKey: '', + keyType: '', + }); + }, 100); + } catch (error) { + console.error("Authentication error:", error); + setErrorMessage("Failed to authenticate: " + (error.message || "Unknown error")); + setIsErrorHidden(false); + } }; const connectToHostWithConfig = (hostConfig) => { @@ -327,20 +369,30 @@ function App() { setIsLaunchpadOpen(false); } - const handleSaveHost = () => { - let hostConfig = { - name: addHostForm.name || addHostForm.ip, - folder: addHostForm.folder, - ip: addHostForm.ip, - user: addHostForm.user, - password: addHostForm.authMethod === 'password' ? addHostForm.password : undefined, - sshKey: addHostForm.authMethod === 'sshKey' ? addHostForm.sshKey : undefined, - port: String(addHostForm.port), - } - if (userRef.current) { - userRef.current.saveHost({ - hostConfig, - }); + const handleSaveHost = async () => { + try { + let hostConfig = { + name: addHostForm.name || addHostForm.ip, + folder: addHostForm.folder, + ip: addHostForm.ip, + user: addHostForm.user, + password: (addHostForm.authMethod === 'password' || addHostForm.connectionType === 'vnc' || addHostForm.connectionType === 'rdp') ? addHostForm.password : undefined, + sshKey: addHostForm.connectionType === 'ssh' && addHostForm.authMethod === 'sshKey' ? addHostForm.sshKey : undefined, + port: String(addHostForm.port), + connectionType: addHostForm.connectionType, + rdpDomain: addHostForm.connectionType === 'rdp' ? addHostForm.rdpDomain : undefined, + rdpWindowsAuthentication: addHostForm.connectionType === 'rdp' ? addHostForm.rdpWindowsAuthentication : undefined, + rdpConsole: addHostForm.connectionType === 'rdp' ? addHostForm.rdpConsole : undefined, + vncScaling: addHostForm.connectionType === 'vnc' ? addHostForm.vncScaling : undefined, + vncQuality: addHostForm.connectionType === 'vnc' ? addHostForm.vncQuality : undefined + } + if (userRef.current) { + await userRef.current.saveHost({ + hostConfig, + }); + } + } catch (error) { + throw error; } } @@ -455,9 +507,11 @@ function App() { }); await new Promise(resolve => setTimeout(resolve, 3000)); + setIsEditHostHidden(true); + } catch (error) { + throw error; } finally { setIsEditing(false); - setIsEditHostHidden(true); } return; } @@ -465,7 +519,7 @@ function App() { updateEditHostForm(oldConfig); } catch (error) { console.error('Edit failed:', error); - setErrorMessage(`Edit failed: ${error}`); + setErrorMessage(`Edit failed: ${error.message || error}`); setIsErrorHidden(false); setIsEditing(false); } diff --git a/src/apps/ssh/Terminal.jsx b/src/apps/ssh/Terminal.jsx index 41f0c2d9..cec0ba1e 100644 --- a/src/apps/ssh/Terminal.jsx +++ b/src/apps/ssh/Terminal.jsx @@ -94,20 +94,15 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde }); socket.on("connect", () => { - console.log("Socket connected, attempting SSH connection..."); - fitAddon.current.fit(); resizeTerminal(); const { cols, rows } = terminalInstance.current; - // Check for authentication details if (!hostConfig.password?.trim() && !hostConfig.sshKey?.trim()) { - console.log("No authentication provided, showing modal"); setIsNoAuthHidden(false); return; } - // Ensure we have proper SSH config with both key field names for backward compatibility const sshConfig = { ip: hostConfig.ip, user: hostConfig.user, @@ -121,9 +116,11 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde }); setTimeout(() => { - fitAddon.current.fit(); - resizeTerminal(); - terminalInstance.current.focus(); + if (terminalInstance.current) { + fitAddon.current.fit(); + resizeTerminal(); + terminalInstance.current.focus(); + } }, 50); socket.on("data", (data) => { @@ -133,124 +130,49 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde let isPasting = false; - terminalInstance.current.onData((data) => { - if (socketRef.current && socketRef.current.connected) { - socketRef.current.emit("data", data); - } - }); + if (terminalInstance.current) { + terminalInstance.current.onData((data) => { + if (socketRef.current && socketRef.current.connected) { + socketRef.current.emit("data", data); + } + }); - terminalInstance.current.attachCustomKeyEventHandler((event) => { - if ((event.ctrlKey || event.metaKey) && event.key === "v") { - if (isPasting) return false; - isPasting = true; - - event.preventDefault(); - - // Use a multi-layered approach for clipboard access - const pasteFromClipboard = async () => { - try { - // Try modern Clipboard API first - if (navigator.clipboard && navigator.clipboard.readText) { - try { - const text = await navigator.clipboard.readText(); - if (text && socketRef.current?.connected) { - const processedText = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n/g, "\r"); - socketRef.current.emit("data", processedText); - return true; - } - } catch (clipboardErr) { - console.warn("Clipboard API failed:", clipboardErr); - // Continue to fallbacks + terminalInstance.current.attachCustomKeyEventHandler((event) => { + if ((event.ctrlKey || event.metaKey) && event.key === "v") { + event.preventDefault(); + + navigator.clipboard.readText() + .then(text => { + if (text && socketRef.current?.connected) { + const processedText = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n/g, "\r"); + socketRef.current.emit("data", processedText); } - } - - // Try execCommand fallback - if (document.queryCommandSupported && document.queryCommandSupported('paste')) { - const textarea = document.createElement('textarea'); - textarea.style.position = 'fixed'; - textarea.style.opacity = '0'; - document.body.appendChild(textarea); - textarea.focus(); - - try { - const successful = document.execCommand('paste'); - if (successful) { - const text = textarea.value; - if (text && socketRef.current?.connected) { - const processedText = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n/g, "\r"); - socketRef.current.emit("data", processedText); - document.body.removeChild(textarea); - return true; - } - } - } catch (execErr) { - console.warn("execCommand paste failed:", execErr); + }) + .catch(() => { + if (terminalInstance.current) { + terminalInstance.current.write("\r\n*** Paste failed: Clipboard access denied. Please check browser permissions. ***\r\n"); } - document.body.removeChild(textarea); - } + }); - // Show permissions warning and instructions - terminalInstance.current.write("\r\n*** To paste: Right-click in terminal and select Paste from context menu ***\r\n"); - return false; - } finally { - setTimeout(() => { - isPasting = false; - }, 100); - } - }; + return false; + } + return true; + }); - pasteFromClipboard(); - return false; - } - - return true; - }); - - terminalInstance.current.onKey(({ domEvent }) => { - if (domEvent.key === "c" && (domEvent.ctrlKey || domEvent.metaKey)) { - const selection = terminalInstance.current.getSelection(); - if (selection) { - // Use a try-catch to handle clipboard failures - try { - if (navigator.clipboard && navigator.clipboard.writeText) { - navigator.clipboard.writeText(selection) - .catch(err => { - console.warn("Clipboard write failed:", err); - terminalInstance.current.write("\r\n*** Copy failed: Text copied to internal buffer ***\r\n"); - // Store selection in a variable as fallback - window.termixInternalClipboard = selection; - }); - } else { - // Fallback for browsers without clipboard API - const textarea = document.createElement('textarea'); - textarea.value = selection; - textarea.style.position = 'fixed'; - textarea.style.opacity = '0'; - document.body.appendChild(textarea); - textarea.select(); - - try { - const successful = document.execCommand('copy'); - if (!successful) { - terminalInstance.current.write("\r\n*** Copy failed: Text copied to internal buffer ***\r\n"); - window.termixInternalClipboard = selection; + terminalInstance.current.onKey(({ domEvent }) => { + if (domEvent.key === "c" && (domEvent.ctrlKey || domEvent.metaKey)) { + const selection = terminalInstance.current.getSelection(); + if (selection) { + navigator.clipboard.writeText(selection) + .catch(() => { + if (terminalInstance.current) { + terminalInstance.current.write("\r\n*** Copy failed: Clipboard access denied. Please check browser permissions. ***\r\n"); } - } catch (err) { - console.warn("execCommand copy failed:", err); - terminalInstance.current.write("\r\n*** Copy failed: Text copied to internal buffer ***\r\n"); - window.termixInternalClipboard = selection; - } - - document.body.removeChild(textarea); - } - } catch (err) { - console.error("Copy failed:", err); - terminalInstance.current.write("\r\n*** Copy failed: Text copied to internal buffer ***\r\n"); - window.termixInternalClipboard = selection; + }); } } - } - }); + }); + } let authModalShown = false; @@ -262,18 +184,22 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde }); socket.on("disconnect", (reason) => { - console.log("Socket disconnected:", reason); - terminalInstance.current.write(`\r\n*** Socket disconnected: ${reason} ***\r\n`); + if (terminalInstance.current) { + terminalInstance.current.write(`\r\n*** Socket disconnected: ${reason} ***\r\n`); + } }); socket.on("reconnect", (attemptNumber) => { - console.log("Socket reconnected after", attemptNumber, "attempts"); - terminalInstance.current.write(`\r\n*** Socket reconnected after ${attemptNumber} attempts ***\r\n`); + if (terminalInstance.current) { + terminalInstance.current.write(`\r\n*** Socket reconnected after ${attemptNumber} attempts ***\r\n`); + } }); socket.on("reconnect_error", (error) => { console.error("Socket reconnect error:", error); - terminalInstance.current.write(`\r\n*** Socket reconnect error: ${error.message} ***\r\n`); + if (terminalInstance.current) { + terminalInstance.current.write(`\r\n*** Socket reconnect error: ${error.message} ***\r\n`); + } }); const pingInterval = setInterval(() => { @@ -284,13 +210,11 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde socketRef.current.on("pong", () => {}); - // Add right-click context menu for paste - const element = terminalInstance.current.element; - if (element) { + if (terminalInstance.current && terminalInstance.current.element) { + const element = terminalInstance.current.element; element.addEventListener('contextmenu', (event) => { event.preventDefault(); - - // Create and show context menu + const contextMenu = document.createElement('div'); contextMenu.className = 'terminal-context-menu'; contextMenu.style.position = 'fixed'; @@ -302,8 +226,7 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde contextMenu.style.padding = '4px 0'; contextMenu.style.boxShadow = '0 2px 10px rgba(0,0,0,0.2)'; contextMenu.style.zIndex = '1000'; - - // Create copy option + const copyOption = document.createElement('div'); copyOption.innerText = 'Copy'; copyOption.className = 'terminal-context-menu-item'; @@ -317,29 +240,31 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde copyOption.onmouseout = () => { copyOption.style.backgroundColor = 'transparent'; }; - - // Handle copy action + copyOption.onclick = () => { - const selection = terminalInstance.current.getSelection(); - if (selection) { - // Try to copy using clipboard API - if (navigator.clipboard && navigator.clipboard.writeText) { - navigator.clipboard.writeText(selection) - .catch(err => { - console.warn("Clipboard write failed:", err); - window.termixInternalClipboard = selection; + if (terminalInstance.current) { + const selection = terminalInstance.current.getSelection(); + if (selection) { + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(selection) + .catch(err => { + console.warn("Clipboard write failed:", err); + window.termixInternalClipboard = selection; + if (terminalInstance.current) { + terminalInstance.current.write("\r\n*** Copied to internal clipboard ***\r\n"); + } + }); + } else { + window.termixInternalClipboard = selection; + if (terminalInstance.current) { terminalInstance.current.write("\r\n*** Copied to internal clipboard ***\r\n"); - }); - } else { - // Store in internal clipboard - window.termixInternalClipboard = selection; - terminalInstance.current.write("\r\n*** Copied to internal clipboard ***\r\n"); + } + } } } document.body.removeChild(contextMenu); }; - - // Create paste option + const pasteOption = document.createElement('div'); pasteOption.innerText = 'Paste'; pasteOption.className = 'terminal-context-menu-item'; @@ -353,11 +278,9 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde pasteOption.onmouseout = () => { pasteOption.style.backgroundColor = 'transparent'; }; - - // Handle paste action + pasteOption.onclick = async () => { try { - // Try clipboard API first if (navigator.clipboard && navigator.clipboard.readText) { try { const text = await navigator.clipboard.readText(); @@ -366,32 +289,28 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde socketRef.current.emit("data", processedText); } } catch (err) { - // Use fallback or internal clipboard if (window.termixInternalClipboard) { const processedText = window.termixInternalClipboard.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n/g, "\r"); socketRef.current.emit("data", processedText); - } else { + } else if (terminalInstance.current) { terminalInstance.current.write("\r\n*** Paste failed: No clipboard content available ***\r\n"); } } } else if (window.termixInternalClipboard) { - // Use internal clipboard if available const processedText = window.termixInternalClipboard.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n/g, "\r"); socketRef.current.emit("data", processedText); - } else { + } else if (terminalInstance.current) { terminalInstance.current.write("\r\n*** Paste failed: No clipboard content available ***\r\n"); } } finally { document.body.removeChild(contextMenu); } }; - - // Add options to menu + contextMenu.appendChild(copyOption); contextMenu.appendChild(pasteOption); document.body.appendChild(contextMenu); - - // Remove menu when clicking elsewhere + const removeMenu = (e) => { if (!contextMenu.contains(e.target)) { document.body.removeChild(contextMenu); diff --git a/src/apps/user/User.jsx b/src/apps/user/User.jsx index ab22ec4f..534ee227 100644 --- a/src/apps/user/User.jsx +++ b/src/apps/user/User.jsx @@ -149,6 +149,27 @@ export const User = forwardRef(({ onLoginSuccess, onCreateSuccess, onDeleteSucce if (!currentUser.current) return onFailure("Not authenticated"); try { + const existingHosts = await getAllHosts(); + + const duplicateNameHost = existingHosts.find(host => + host.config.name && + host.config.name.toLowerCase() === hostConfig.hostConfig.name.toLowerCase() + ); + + if (duplicateNameHost) { + return onFailure("A host with this name already exists. Please choose a different name."); + } + + if (!hostConfig.hostConfig.name) { + const duplicateIpHost = existingHosts.find(host => + host.config.ip.toLowerCase() === hostConfig.hostConfig.ip.toLowerCase() + ); + + if (duplicateIpHost) { + return onFailure("A host with this IP already exists. Please provide a unique name."); + } + } + const response = await new Promise((resolve) => { socketRef.current.emit("saveHostConfig", { userId: currentUser.current.id, @@ -222,6 +243,18 @@ export const User = forwardRef(({ onLoginSuccess, onCreateSuccess, onDeleteSucce if (!currentUser.current) return onFailure("Not authenticated"); try { + const existingHosts = await getAllHosts(); + + const duplicateNameHost = existingHosts.find(host => + host.config.name && + host.config.name.toLowerCase() === newHostConfig.name.toLowerCase() && + host.config.ip.toLowerCase() !== oldHostConfig.ip.toLowerCase() + ); + + if (duplicateNameHost) { + return onFailure("A host with this name already exists. Please choose a different name."); + } + const response = await new Promise((resolve) => { socketRef.current.emit("editHost", { userId: currentUser.current.id, diff --git a/src/backend/database.cjs b/src/backend/database.cjs index 6d99d81a..2e3e4bb4 100644 --- a/src/backend/database.cjs +++ b/src/backend/database.cjs @@ -190,14 +190,31 @@ io.of('/database.io').on('connection', (socket) => { const finalName = cleanConfig.name || cleanConfig.ip; - const existingHost = await Host.findOne({ - name: finalName, - createdBy: userId + // Check for hosts with the same name (case insensitive) + const existingHostByName = await Host.findOne({ + createdBy: userId, + name: { $regex: new RegExp('^' + finalName + '$', 'i') } }); - if (existingHost) { + if (existingHostByName) { logger.warn(`Host with name ${finalName} already exists for user: ${userId}`); - return callback({ error: 'Host with this name already exists' }); + return callback({ error: `Host with name "${finalName}" already exists. Please choose a different name.` }); + } + + // Prevent duplicate IPs if using IP as name + if (!cleanConfig.name) { + const existingHostByIp = await Host.findOne({ + createdBy: userId, + config: { $regex: new RegExp(cleanConfig.ip, 'i') } + }); + + if (existingHostByIp) { + const decryptedConfig = decryptData(existingHostByIp.config, userId, sessionToken); + if (decryptedConfig && decryptedConfig.ip.toLowerCase() === cleanConfig.ip.toLowerCase()) { + logger.warn(`Host with IP ${cleanConfig.ip} already exists for user: ${userId}`); + return callback({ error: `Host with IP "${cleanConfig.ip}" already exists. Please provide a unique name.` }); + } + } } const encryptedConfig = encryptData(cleanConfig, userId, sessionToken); @@ -397,6 +414,7 @@ io.of('/database.io').on('connection', (socket) => { return callback({ error: 'Invalid session' }); } + // Find the host to be edited const hosts = await Host.find({ createdBy: userId }); const host = hosts.find(h => { const decryptedConfig = decryptData(h.config, userId, sessionToken); @@ -408,6 +426,37 @@ io.of('/database.io').on('connection', (socket) => { return callback({ error: 'Host not found' }); } + const finalName = newHostConfig.name?.trim() || newHostConfig.ip.trim(); + + // If the name is being changed, check for duplicates using case-insensitive comparison + if (finalName.toLowerCase() !== host.name.toLowerCase()) { + // Check for duplicate name using regex for case-insensitive comparison + const duplicateNameHost = await Host.findOne({ + createdBy: userId, + _id: { $ne: host._id }, // Exclude the current host + name: { $regex: new RegExp('^' + finalName + '$', 'i') } + }); + + if (duplicateNameHost) { + logger.warn(`Host with name ${finalName} already exists for user: ${userId}`); + return callback({ error: `Host with name "${finalName}" already exists. Please choose a different name.` }); + } + } + + // If IP is changed and no custom name provided, check for duplicate IP + if (newHostConfig.ip !== oldHostConfig.ip && !newHostConfig.name) { + const duplicateIpHost = hosts.find(h => { + if (h._id.toString() === host._id.toString()) return false; + const decryptedConfig = decryptData(h.config, userId, sessionToken); + return decryptedConfig && decryptedConfig.ip.toLowerCase() === newHostConfig.ip.toLowerCase(); + }); + + if (duplicateIpHost) { + logger.warn(`Host with IP ${newHostConfig.ip} already exists for user: ${userId}`); + return callback({ error: `Host with IP "${newHostConfig.ip}" already exists. Please provide a unique name.` }); + } + } + const cleanConfig = { name: newHostConfig.name?.trim(), folder: newHostConfig.folder?.trim() || null, @@ -424,6 +473,7 @@ io.of('/database.io').on('connection', (socket) => { return callback({ error: 'Configuration encryption failed' }); } + host.name = finalName; host.config = encryptedConfig; host.folder = cleanConfig.folder; await host.save(); @@ -432,7 +482,7 @@ io.of('/database.io').on('connection', (socket) => { callback({ success: true }); } catch (error) { logger.error('Host edit error:', error); - callback({ error: 'Failed to edit host' }); + callback({ error: `Failed to edit host: ${error.message}` }); } }); diff --git a/src/modals/AddHostModal.jsx b/src/modals/AddHostModal.jsx index d8bdf8c7..b65cc54f 100644 --- a/src/modals/AddHostModal.jsx +++ b/src/modals/AddHostModal.jsx @@ -25,6 +25,8 @@ import VisibilityOff from '@mui/icons-material/VisibilityOff'; const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidden }) => { const [showPassword, setShowPassword] = useState(false); const [activeTab, setActiveTab] = useState(0); + const [errorMessage, setErrorMessage] = useState(""); + const [showError, setShowError] = useState(false); const handleFileChange = (e) => { const file = e.target.files[0]; @@ -102,12 +104,30 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd const handleSubmit = (event) => { event.preventDefault(); - if (!form.ip?.trim() || !form.user?.trim() || !form.port) { - alert("Please fill out all required fields (IP, User, Port)."); + + setErrorMessage(""); + setShowError(false); + + if (!form.ip?.trim()) { + setErrorMessage("Please provide an IP address."); + setShowError(true); return; } - handleAddHost(); - setActiveTab(0); + + if (form.connectionType === 'ssh' && !form.user?.trim()) { + setErrorMessage("Please provide a username for SSH connection."); + setShowError(true); + return; + } + + try { + handleAddHost(); + setActiveTab(0); + } catch (error) { + console.error("Add host error:", error); + setErrorMessage(error.message || "Failed to add host. The host name or IP may already exist."); + setShowError(true); + } }; return ( @@ -138,6 +158,18 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd mx: 2, }} > + {showError && ( +
+ {errorMessage} +
+ )} setActiveTab(val)} diff --git a/src/modals/EditHostModal.jsx b/src/modals/EditHostModal.jsx index 25810fe7..02aafb40 100644 --- a/src/modals/EditHostModal.jsx +++ b/src/modals/EditHostModal.jsx @@ -24,21 +24,23 @@ import VisibilityOff from '@mui/icons-material/VisibilityOff'; const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHost }) => { const [form, setForm] = useState({ - name: hostConfig?.name || '', - folder: hostConfig?.folder || '', - ip: hostConfig?.ip || '', - user: hostConfig?.user || '', - port: hostConfig?.port || '', + name: '', + folder: '', + ip: '', + user: '', + port: '', password: '', - sshKey: hostConfig?.sshKey || '', - keyType: hostConfig?.keyType || '', - authMethod: hostConfig?.authMethod || 'Select Auth', + sshKey: '', + keyType: '', + authMethod: 'Select Auth', storePassword: true, rememberHost: true }); const [showPassword, setShowPassword] = useState(false); const [activeTab, setActiveTab] = useState(0); const [isLoading, setIsLoading] = useState(false); + const [errorMessage, setErrorMessage] = useState(""); + const [showError, setShowError] = useState(false); useEffect(() => { if (!isHidden && hostConfig) { @@ -106,17 +108,10 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo const handleAuthChange = (newMethod) => { setForm((prev) => ({ ...prev, - authMethod: newMethod - })); - }; - - const handleStorePasswordChange = (checked) => { - setForm((prev) => ({ - ...prev, - storePassword: Boolean(checked), - password: checked ? prev.password : "", - sshKey: checked ? prev.sshKey : "", - authMethod: checked ? prev.authMethod : "Select Auth" + authMethod: newMethod, + password: "", + sshKey: "", + keyType: "", })); }; @@ -131,7 +126,7 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo if (form.storePassword) { if (authMethod === 'Select Auth') return false; if (authMethod === 'password' && !password?.trim()) return false; - if (authMethod === 'sshKey' && !sshKey?.trim()) return false; + if (authMethod === 'key' && !sshKey?.trim()) return false; } return true; @@ -143,6 +138,23 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo setIsLoading(true); try { + setErrorMessage(""); + setShowError(false); + + if (!form.ip || !form.user) { + setErrorMessage("IP and Username are required fields"); + setShowError(true); + setIsLoading(false); + return; + } + + if (!form.port) { + setErrorMessage("Port is required"); + setShowError(true); + setIsLoading(false); + return; + } + const newConfig = { name: form.name || form.ip, folder: form.folder, @@ -161,6 +173,11 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo } await handleEditHost(hostConfig, newConfig); + setActiveTab(0); + } catch (error) { + console.error("Edit host error:", error); + setErrorMessage(error.message || "Failed to edit host. The host name may already exist."); + setShowError(true); } finally { setIsLoading(false); } @@ -196,10 +213,22 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo mx: 2, }} > - + {errorMessage} + + )} + setActiveTab(val)} - sx={{ + sx={{ width: '100%', mb: 0, backgroundColor: theme.palette.general.tertiary, @@ -241,22 +270,21 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo Host Name setForm((prev) => ({ ...prev, name: e.target.value }))} + onChange={(e) => setForm({ ...form, name: e.target.value })} sx={{ backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary + color: theme.palette.text.primary, }} /> - Folder setForm((prev) => ({ ...prev, folder: e.target.value }))} + value={form.folder || ''} + onChange={(e) => setForm({ ...form, folder: e.target.value })} sx={{ backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary + color: theme.palette.text.primary, }} /> @@ -269,35 +297,38 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo Host IP setForm((prev) => ({ ...prev, ip: e.target.value }))} + onChange={(e) => setForm({ ...form, ip: e.target.value })} + required sx={{ backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary + color: theme.palette.text.primary, + }} + /> + + + Host User + setForm({ ...form, user: e.target.value })} + required + sx={{ + backgroundColor: theme.palette.general.primary, + color: theme.palette.text.primary, }} /> - 65535}> Host Port setForm((prev) => ({ ...prev, port: e.target.value }))} + onChange={(e) => setForm({ ...form, port: e.target.value })} + min={1} + max={65535} + required sx={{ backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary - }} - /> - - - - Host User - setForm((prev) => ({ ...prev, user: e.target.value }))} - sx={{ - backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary + color: theme.palette.text.primary, }} /> @@ -306,23 +337,9 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo - - Store Password - handleStorePasswordChange(e.target.checked)} - sx={{ - color: theme.palette.text.primary, - '&.Mui-checked': { - color: theme.palette.text.primary, - }, - }} - /> - - {form.storePassword && ( <> - + Authentication Method setForm(prev => ({ ...prev, password: e.target.value }))} + onChange={(e) => setForm({ ...form, password: e.target.value })} sx={{ backgroundColor: theme.palette.general.primary, color: theme.palette.text.primary, @@ -367,7 +384,7 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo {form.authMethod === 'key' && ( - + SSH Key