Dev 0.2.1 (#30)

Change Log:
- Renamed all release versions for consistency
- Show version number in profile menu
- Support for low end devices (switched MongoDB to version 4)
- Better SSH  key support (RSA, PEM, Key, DSA, ECDSA, ED25519)
- Improve UI for logging in, creating hosts, and viewing hosts

Bug Fixes:
- SSH would disconnect if left opened for too long without activity
- Pasting permission and formatting issues
- No longer allow hosts to have the same name
This commit was merged in pull request #30.
This commit is contained in:
Karmaa
2025-03-23 22:17:56 -05:00
committed by GitHub
parent 10bc491a9f
commit 6940f4e9bb
20 changed files with 1962 additions and 1351 deletions

View File

@@ -4,6 +4,9 @@ on:
push:
branches:
- development
paths-ignore:
- '**.md'
- '.gitignore'
workflow_dispatch:
inputs:
tag_name:
@@ -16,27 +19,44 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
uses: actions/checkout@v4
with:
node-version: '18'
fetch-depth: 1
- name: Install Dependencies and Build Frontend
run: |
cd src
npm ci
npm run build
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: arm64
- name: Setup QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
platforms: linux/amd64,linux/arm64
driver-opts: |
image=moby/buildkit:master
network=host
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Cache npm dependencies
uses: actions/cache@v3
with:
path: |
~/.npm
node_modules
*/*/node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Cache Docker layers
uses: actions/cache@v3
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Login to Docker Registry
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -53,21 +73,36 @@ jobs:
echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
- name: Build and Push Multi-Arch Docker Image
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
context: .
file: ./docker/Dockerfile
push: true
platforms: linux/amd64,linux/arm64
tags: ghcr.io/${{ env.REPO_OWNER }}/termix:${{ env.IMAGE_TAG }}
labels: org.opencontainers.image.source=https://github.com/${{ github.repository }}
labels: |
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
build-args: |
BUILDKIT_INLINE_CACHE=1
BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
outputs: type=registry,compression=zstd,compression-level=19
- name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
- name: Notify via ntfy
if: success()
run: |
curl -d "Docker image build and push completed successfully for tag: ${{ env.IMAGE_TAG }}" \
https://ntfy.karmaa.site/termix-build
- name: Delete all untagged image versions
if: success()
uses: quartx-analytics/ghcr-cleaner@v1
with:
owner-type: user
@@ -76,6 +111,7 @@ jobs:
delete-untagged: true
- name: Cleanup Docker Images Locally
if: always()
run: |
docker image prune -af
docker system prune -af --volumes

View File

@@ -1,61 +1,107 @@
# Stage 1: Build frontend
FROM --platform=$BUILDPLATFORM node:18 AS frontend-builder
FROM node:18-alpine AS frontend-builder
WORKDIR /app
COPY package*.json ./
RUN npm install
RUN npm ci --force && \
npm cache clean --force
COPY . .
RUN npm run build
# Stage 2: Build backend
FROM --platform=$BUILDPLATFORM node:18 AS backend-builder
FROM node:18-alpine AS backend-builder
WORKDIR /app
COPY package*.json ./
RUN npm install
RUN npm ci --only=production --force && \
npm cache clean --force
COPY src/backend/ ./src/backend/
# Stage 3: Final production image
FROM mongo:5
# 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 Ubuntu
FROM ubuntu:focal AS bcrypt-builder
ENV DEBIAN_FRONTEND=noninteractive \
NODE_VERSION=18.x
WORKDIR /app
COPY package*.json ./
RUN rm -f /var/lib/apt/lists/lock /var/cache/apt/archives/lock /var/lib/dpkg/lock* && \
apt-get clean && \
apt-get update && \
apt-get install -y --no-install-recommends \
python3 \
make \
g++ \
wget \
ca-certificates \
gnupg && \
wget -qO- https://deb.nodesource.com/setup_${NODE_VERSION} | bash - && \
apt-get update && \
apt-get install -y nodejs && \
npm ci --only=production bcrypt --force && \
npm cache clean --force && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* /var/cache/apt/* /tmp/*
# Configure nginx
# Final stage
FROM ubuntu:focal
ENV DEBIAN_FRONTEND=noninteractive \
NODE_VERSION=18.x \
MONGO_VERSION=4.4.24 \
MONGO_URL=mongodb://localhost:27017/termix \
MONGODB_DATA_DIR=/data/db \
MONGODB_LOG_DIR=/var/log/mongodb \
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
# Create users first
RUN groupadd -r mongodb && useradd -r -g mongodb mongodb \
&& groupadd -r node && useradd -r -g node -m node
# Install all dependencies in one layer
RUN rm -f /var/lib/apt/lists/lock /var/cache/apt/archives/lock /var/lib/dpkg/lock* && \
apt-get clean && \
apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates \
gnupg \
gosu \
nginx-light \
wget && \
# Add MongoDB 4.4 repository
wget -qO - https://www.mongodb.org/static/pgp/server-4.4.asc | apt-key add - && \
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/4.4 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-4.4.list && \
# Add MongoDB 5.0 repository
wget -qO - https://www.mongodb.org/static/pgp/server-5.0.asc | apt-key add - && \
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/5.0 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-5.0.list && \
# Add NodeJS repository
wget -qO- https://deb.nodesource.com/setup_${NODE_VERSION} | bash - && \
apt-get update && \
# Install MongoDB 4.4 and 5.0 packages
apt-get install -y --no-install-recommends \
nodejs \
mongodb-org-server=${MONGO_VERSION} \
mongodb-org-shell=${MONGO_VERSION} \
mongodb-org-server=5.0.21 && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* /var/cache/apt/* /tmp/* && \
mkdir -p /data/db /var/log/mongodb /var/run/mongodb && \
chown -R mongodb:mongodb /data/db /var/log/mongodb /var/run/mongodb && \
chmod 755 /data/db /var/log/mongodb /var/run/mongodb
# Setup nginx and frontend
COPY docker/nginx.conf /etc/nginx/nginx.conf
COPY --from=frontend-builder /app/dist /usr/share/nginx/html
RUN chown -R www-data:www-data /usr/share/nginx/html
# Setup backend
WORKDIR /app
COPY package*.json ./
RUN npm install --omit=dev
RUN npm ci --only=production --ignore-scripts --force && \
npm cache clean --force && \
rm -rf /tmp/*
COPY --from=bcrypt-builder /app/node_modules/bcrypt /app/node_modules/bcrypt
COPY --from=backend-builder /app/src/backend ./src/backend
RUN chown -R node:node /app
# 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
# Set environment variables
ENV MONGO_URL=mongodb://localhost:27017/termix \
MONGODB_DATA_DIR=/data/db \
MONGODB_LOG_DIR=/var/log/mongodb
# Create volume for MongoDB data
VOLUME ["/data/db"]
# Expose ports
EXPOSE 8080 8081 8082 27017
# Use a entrypoint script to run all services
COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
CMD ["/entrypoint.sh"]

View File

@@ -1,32 +1,124 @@
#!/bin/bash
set -e
# Start MongoDB
echo "Starting MongoDB..."
mongod --fork --dbpath $MONGODB_DATA_DIR --logpath $MONGODB_LOG_DIR/mongodb.log
# Create required directories and set permissions
mkdir -p /data/db /var/log/mongodb /var/run/mongodb
chown -R mongodb:mongodb /data/db /var/log/mongodb /var/run/mongodb
chmod 755 /data/db /var/log/mongodb /var/run/mongodb
# Function to check MongoDB version
check_mongo_version() {
echo "Checking MongoDB version..."
if [ -f "/data/db/diagnostic.data/metrics.2" ] || [ -f "/data/db/WiredTiger.wt" ]; then
echo "Existing MongoDB data detected, attempting migration..."
# Clear any existing mongod lock file
rm -f /tmp/mongodb-27017.sock
rm -f /data/db/mongod.lock
# First, start MongoDB 5.0 to set compatibility version
echo "Starting MongoDB 5.0 to set compatibility version..."
gosu mongodb /usr/bin/mongod --dbpath $MONGODB_DATA_DIR --port 27017 --bind_ip 127.0.0.1 --config /etc/mongod.conf &
MONGO_PID=$!
# Wait for MongoDB 5.0 to start
echo "Waiting for MongoDB 5.0 to start..."
MAX_TRIES=30
COUNT=0
while ! gosu mongodb mongo --quiet --eval "db.version()" > /dev/null 2>&1; do
sleep 2
COUNT=$((COUNT + 1))
if [ $COUNT -ge $MAX_TRIES ]; then
echo "Failed to start MongoDB 5.0 after $MAX_TRIES attempts"
kill -9 $MONGO_PID 2>/dev/null || true
return 1
fi
done
# Set compatibility version to 4.4
echo "Setting feature compatibility version to 4.4..."
if ! gosu mongodb mongo --quiet --eval 'db.adminCommand({setFeatureCompatibilityVersion: "4.4"})'; then
echo "Failed to set feature compatibility version"
kill -9 $MONGO_PID 2>/dev/null || true
return 1
fi
# Shutdown MongoDB 5.0 cleanly
echo "Shutting down MongoDB 5.0..."
gosu mongodb mongo --quiet --eval "db.adminCommand({shutdown: 1})" || kill $MONGO_PID
# Wait for process to end
while kill -0 $MONGO_PID 2>/dev/null; do
sleep 1
done
# Run repair with MongoDB 4.4
echo "Running repair with MongoDB 4.4..."
gosu mongodb /usr/bin/mongod --dbpath $MONGODB_DATA_DIR --repair
return 0
fi
return 0
}
# Try migration up to 3 times
MAX_MIGRATION_ATTEMPTS=3
MIGRATION_ATTEMPT=1
while [ $MIGRATION_ATTEMPT -le $MAX_MIGRATION_ATTEMPTS ]; do
echo "Migration attempt $MIGRATION_ATTEMPT of $MAX_MIGRATION_ATTEMPTS"
if check_mongo_version; then
break
fi
MIGRATION_ATTEMPT=$((MIGRATION_ATTEMPT + 1))
if [ $MIGRATION_ATTEMPT -le $MAX_MIGRATION_ATTEMPTS ]; then
echo "Migration failed, waiting before retry..."
sleep 5
fi
done
if [ $MIGRATION_ATTEMPT -gt $MAX_MIGRATION_ATTEMPTS ]; then
echo "Migration failed after $MAX_MIGRATION_ATTEMPTS attempts"
exit 1
fi
# Start MongoDB 4.4 normally
echo "Starting MongoDB 4.4..."
gosu mongodb /usr/bin/mongod --dbpath $MONGODB_DATA_DIR --logpath $MONGODB_LOG_DIR/mongodb.log --bind_ip 0.0.0.0 &
MONGO_PID=$!
# Wait for MongoDB to be ready
echo "Waiting for MongoDB to start..."
until mongosh --eval "print(\"waited for connection\")" > /dev/null 2>&1; do
sleep 0.5
MAX_TRIES=30
COUNT=0
while ! gosu mongodb mongo --quiet --eval "db.version()" > /dev/null 2>&1; do
sleep 2
COUNT=$((COUNT + 1))
if [ $COUNT -ge $MAX_TRIES ]; then
echo "Failed to start MongoDB. Checking logs:"
cat $MONGODB_LOG_DIR/mongodb.log
exit 1
fi
echo "Waiting for MongoDB... (attempt $COUNT/$MAX_TRIES)"
done
echo "MongoDB has started"
echo "MongoDB started successfully"
# Start nginx
echo "Starting nginx..."
nginx
# Change to app directory
# Start backend services
echo "Starting backend services..."
cd /app
export NODE_ENV=production
# Start the SSH service
echo "Starting SSH service..."
node src/backend/ssh.cjs &
# Start SSH service
su -s /bin/bash node -c "node src/backend/ssh.cjs" &
# Start the database service
echo "Starting database service..."
node src/backend/database.cjs &
# Start database service
su -s /bin/bash node -c "node src/backend/database.cjs" &
# Keep the container running and show MongoDB logs
echo "All services started. Tailing MongoDB logs..."
echo "All services started"
# Keep container running and show logs
tail -f $MONGODB_LOG_DIR/mongodb.log

241
package-lock.json generated
View File

@@ -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",
@@ -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",
@@ -41,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",
@@ -1225,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",
@@ -2011,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"
],
@@ -2060,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"
],
@@ -2076,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"
],
@@ -2092,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"
],
@@ -2108,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"
],
@@ -2124,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"
],
@@ -2140,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"
],
@@ -2156,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"
],
@@ -2172,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"
],
@@ -2188,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"
],
@@ -2204,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"
],
@@ -2220,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"
@@ -3932,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": {
@@ -5880,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"
@@ -5895,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"
],
@@ -5928,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"
],
@@ -5948,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"
],
@@ -5968,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"
],
@@ -5988,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"
],
@@ -6008,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"
],
@@ -6028,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"
],
@@ -6048,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"
],
@@ -6068,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"
],
@@ -6088,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"
],
@@ -6341,6 +6330,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",
@@ -8218,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": {

View File

@@ -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",
@@ -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",
@@ -43,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",

View File

@@ -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,16 +12,15 @@ 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";
import NoAuthenticationModal from "./modals/NoAuthenticationModal.jsx";
import eventBus from "./other/eventBus.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('');
@@ -35,10 +34,17 @@ function App() {
ip: "",
user: "",
password: "",
sshKey: "",
port: 22,
authMethod: "Select Auth",
rememberHost: false,
rememberHost: true,
storePassword: true,
connectionType: "ssh",
rdpDomain: "",
rdpWindowsAuthentication: true,
rdpConsole: false,
vncScaling: "100%",
vncQuality: "High"
});
const [editHostForm, setEditHostForm] = useState({
name: "",
@@ -46,6 +52,7 @@ function App() {
ip: "",
user: "",
password: "",
sshKey: "",
port: 22,
authMethod: "Select Auth",
rememberHost: true,
@@ -53,23 +60,23 @@ 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: "",
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);
const [currentHostConfig, setCurrentHostConfig] = useState(null);
const [isLoggingIn, setIsLoggingIn] = useState(true);
const [isEditing, setIsEditing] = useState(false);
const [isHostViewerMenuOpen, setIsHostViewerMenuOpen] = useState(null);
useEffect(() => {
const handleKeyDown = (e) => {
@@ -133,13 +140,13 @@ function App() {
if (userRef.current?.getUser()) {
setIsLoggingIn(false);
setIsLoginUserHidden(true);
setIsAuthModalHidden(true);
return;
}
if (!sessionToken) {
setIsLoggingIn(false);
setIsLoginUserHidden(false);
setIsAuthModalHidden(false);
return;
}
@@ -147,13 +154,13 @@ function App() {
let loginAttempts = 0;
const maxAttempts = 50;
let attemptLoginInterval;
const loginTimeout = setTimeout(() => {
if (isComponentMounted) {
clearInterval(attemptLoginInterval);
if (!userRef.current?.getUser()) {
localStorage.removeItem('sessionToken');
setIsLoginUserHidden(false);
setIsAuthModalHidden(false);
setIsLoggingIn(false);
setErrorMessage('Login timed out. Please try again.');
setIsErrorHidden(false);
@@ -163,14 +170,14 @@ 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');
setIsLoginUserHidden(false);
setIsAuthModalHidden(false);
setIsLoggingIn(false);
setErrorMessage('Login timed out. Please try again.');
setIsErrorHidden(false);
@@ -186,7 +193,7 @@ function App() {
if (isComponentMounted) {
clearTimeout(loginTimeout);
clearInterval(attemptLoginInterval);
setIsLoginUserHidden(true);
setIsAuthModalHidden(true);
setIsLoggingIn(false);
setIsErrorHidden(true);
}
@@ -200,7 +207,7 @@ function App() {
localStorage.removeItem('sessionToken');
setErrorMessage(`Auto-login failed: ${error}`);
setIsErrorHidden(false);
setIsLoginUserHidden(false);
setIsAuthModalHidden(false);
setIsLoggingIn(false);
}
}
@@ -222,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 === 'rsaKey' && !addHostForm.rsaKey) {
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);
}
};
@@ -261,7 +287,7 @@ function App() {
user: addHostForm.user,
port: String(addHostForm.port),
password: addHostForm.rememberHost && addHostForm.authMethod === 'password' ? addHostForm.password : undefined,
rsaKey: addHostForm.rememberHost && addHostForm.authMethod === 'rsaKey' ? addHostForm.rsaKey : undefined,
sshKey: addHostForm.rememberHost && addHostForm.authMethod === 'sshKey' ? addHostForm.sshKey : undefined,
};
const newTerminal = {
@@ -274,25 +300,42 @@ function App() {
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: "", 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,
rsaKey: form.rsaKey
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) => {
@@ -311,7 +354,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 = {
@@ -326,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,
rsaKey: addHostForm.authMethod === 'rsaKey' ? addHostForm.rsaKey : 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;
}
}
@@ -349,13 +402,13 @@ function App() {
userRef.current.loginUser({
sessionToken,
onSuccess: () => {
setIsLoginUserHidden(true);
setIsAuthModalHidden(true);
setIsLoggingIn(false);
if (onSuccess) onSuccess();
},
onFailure: (error) => {
localStorage.removeItem('sessionToken');
setIsLoginUserHidden(false);
setIsAuthModalHidden(false);
setIsLoggingIn(false);
if (onFailure) onFailure(error);
},
@@ -365,12 +418,12 @@ function App() {
username,
password,
onSuccess: () => {
setIsLoginUserHidden(true);
setIsAuthModalHidden(true);
setIsLoggingIn(false);
if (onSuccess) onSuccess();
},
onFailure: (error) => {
setIsLoginUserHidden(false);
setIsAuthModalHidden(false);
setIsLoggingIn(false);
if (onFailure) onFailure(error);
},
@@ -446,7 +499,7 @@ function App() {
if (newConfig) {
if (isEditing) return;
setIsEditing(true);
try {
await userRef.current.editHost({
oldHostConfig: oldConfig,
@@ -454,9 +507,11 @@ function App() {
});
await new Promise(resolve => setTimeout(resolve, 3000));
setIsEditHostHidden(true);
} catch (error) {
throw error;
} finally {
setIsEditing(false);
setIsEditHostHidden(true);
}
return;
}
@@ -464,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);
}
@@ -585,7 +640,7 @@ function App() {
{/* Profile Button */}
<Button
disabled={isLoggingIn}
onClick={() => userRef.current?.getUser() ? setIsProfileHidden(false) : setIsLoginUserHidden(false)}
onClick={() => userRef.current?.getUser() ? setIsProfileHidden(false) : setIsAuthModalHidden(false)}
sx={{
backgroundColor: theme.palette.general.tertiary,
"&:hover": { backgroundColor: theme.palette.general.secondary },
@@ -633,6 +688,8 @@ function App() {
hostConfig={terminal.hostConfig}
isVisible={activeTab === terminal.id || splitTabIds.includes(terminal.id)}
setIsNoAuthHidden={setIsNoAuthHidden}
setErrorMessage={setErrorMessage}
setIsErrorHidden={setIsErrorHidden}
ref={(ref) => {
terminal.terminalRef = ref;
}}
@@ -649,8 +706,8 @@ function App() {
)}
<NoAuthenticationModal
isHidden={isNoAuthHidden}
form={authForm}
setForm={setAuthForm}
form={noAuthenticationForm}
setForm={setNoAuthenticationForm}
setIsNoAuthHidden={setIsNoAuthHidden}
handleAuthSubmit={handleAuthSubmit}
/>
@@ -694,6 +751,8 @@ function App() {
editHost={handleEditHost}
shareHost={(hostId, username) => userRef.current?.shareHost(hostId, username)}
userRef={userRef}
isHostViewerMenuOpen={isHostViewerMenuOpen}
setIsHostViewerMenuOpen={setIsHostViewerMenuOpen}
/>
)}
</>
@@ -705,40 +764,31 @@ function App() {
setIsErrorHidden={setIsErrorHidden}
/>
<LoginUserModal
isHidden={isLoginUserHidden}
form={loginUserForm}
setForm={setLoginUserForm}
<AuthModal
isHidden={isAuthModalHidden}
form={authForm}
setForm={setAuthForm}
handleLoginUser={handleLoginUser}
handleGuestLogin={handleGuestLogin}
setIsLoginUserHidden={setIsLoginUserHidden}
setIsCreateUserHidden={setIsCreateUserHidden}
/>
<CreateUserModal
isHidden={isCreateUserHidden}
form={createUserForm}
setForm={setCreateUserForm}
handleCreateUser={handleCreateUser}
setIsCreateUserHidden={setIsCreateUserHidden}
setIsLoginUserHidden={setIsLoginUserHidden}
handleGuestLogin={handleGuestLogin}
setIsAuthModalHidden={setIsAuthModalHidden}
/>
{/* User component */}
<User
ref={userRef}
onLoginSuccess={() => {
setIsLoginUserHidden(true);
setIsAuthModalHidden(true);
setIsLoggingIn(false);
setIsErrorHidden(true);
}}
onCreateSuccess={() => {
setIsCreateUserHidden(true);
handleLoginUser({
username: createUserForm.username,
password: createUserForm.password,
setIsAuthModalHidden(true);
handleLoginUser({
username: authForm.username,
password: authForm.password,
onSuccess: () => {
setIsLoginUserHidden(true);
setIsAuthModalHidden(true);
setIsLoggingIn(false);
setIsErrorHidden(true);
},
@@ -756,6 +806,7 @@ function App() {
setErrorMessage(`Action failed: ${error}`);
setIsErrorHidden(false);
setIsLoggingIn(false);
eventBus.emit('failedLoginUser');
}}
/>
</div>

View File

@@ -18,6 +18,8 @@ function Launchpad({
editHost,
shareHost,
userRef,
isHostViewerMenuOpen,
setIsHostViewerMenuOpen,
}) {
const launchpadRef = useRef(null);
const [sidebarOpen, setSidebarOpen] = useState(false);
@@ -32,6 +34,7 @@ function Launchpad({
isAddHostHidden &&
isEditHostHidden &&
isErrorHidden &&
!isHostViewerMenuOpen &&
!isAnyModalOpen
) {
onClose();
@@ -43,7 +46,7 @@ function Launchpad({
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [onClose, isAddHostHidden, isEditHostHidden, isErrorHidden, isAnyModalOpen]);
}, [onClose, isAddHostHidden, isEditHostHidden, isErrorHidden, isHostViewerMenuOpen, isAnyModalOpen]);
const handleModalOpen = () => {
setIsAnyModalOpen(true);
@@ -190,6 +193,8 @@ function Launchpad({
onModalOpen={handleModalOpen}
onModalClose={handleModalClose}
userRef={userRef}
isMenuOpen={isHostViewerMenuOpen || false}
setIsMenuOpen={setIsHostViewerMenuOpen}
/>
)}
</div>
@@ -211,6 +216,8 @@ Launchpad.propTypes = {
editHost: PropTypes.func.isRequired,
shareHost: PropTypes.func.isRequired,
userRef: PropTypes.object.isRequired,
isHostViewerMenuOpen: PropTypes.bool,
setIsHostViewerMenuOpen: PropTypes.func.isRequired,
};
export default Launchpad;

View File

@@ -1,9 +1,22 @@
import PropTypes from "prop-types";
import { useState, useEffect, useRef } from "react";
import { Button, Input } from "@mui/joy";
import { Button, Input, Menu, MenuItem, IconButton } from "@mui/joy";
import ShareHostModal from "../../modals/ShareHostModal";
function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, editHost, openEditPanel, shareHost, onModalOpen, onModalClose, userRef }) {
function HostViewer({
getHosts,
connectToHost,
setIsAddHostHidden,
deleteHost,
editHost,
openEditPanel,
shareHost,
onModalOpen,
onModalClose,
userRef,
isMenuOpen,
setIsMenuOpen,
}) {
const [hosts, setHosts] = useState([]);
const [filteredHosts, setFilteredHosts] = useState([]);
const [isLoading, setIsLoading] = useState(true);
@@ -15,6 +28,24 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
const [isDeleting, setIsDeleting] = useState(false);
const [isShareModalHidden, setIsShareModalHidden] = useState(true);
const [selectedHostForShare, setSelectedHostForShare] = useState(null);
const [selectedHost, setSelectedHost] = useState(null);
const anchorEl = useRef(null);
const menuRef = useRef(null);
useEffect(() => {
const handleClickOutside = (event) => {
if (menuRef.current && !menuRef.current.contains(event.target) && anchorEl.current && !anchorEl.current.contains(event.target)) {
setIsMenuOpen(false);
setSelectedHost(null);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
const fetchHosts = async () => {
try {
@@ -51,9 +82,9 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
useEffect(() => {
const filtered = hosts.filter((hostWrapper) => {
const hostConfig = hostWrapper.config || {};
return hostConfig.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
hostConfig.ip?.toLowerCase().includes(searchTerm.toLowerCase()) ||
hostConfig.folder?.toLowerCase().includes(searchTerm.toLowerCase());
return hostConfig.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
hostConfig.ip?.toLowerCase().includes(searchTerm.toLowerCase()) ||
hostConfig.folder?.toLowerCase().includes(searchTerm.toLowerCase());
});
setFilteredHosts(filtered);
}, [searchTerm, hosts]);
@@ -168,7 +199,7 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
const handleDelete = async (e, hostWrapper) => {
e.stopPropagation();
if (isDeleting) return;
setIsDeleting(true);
try {
const isOwner = hostWrapper.createdBy?._id === userRef.current?.getUser()?.id;
@@ -229,7 +260,8 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
</div>
<div className="flex gap-2">
<Button
className="text-black"
variant="outlined"
className="text-white"
onClick={(e) => {
e.stopPropagation();
if (!hostWrapper.config || !hostWrapper.config.ip || !hostWrapper.config.user) {
@@ -242,76 +274,36 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
backgroundColor: "#6e6e6e",
"&:hover": { backgroundColor: "#0f0f0f" },
opacity: isDeleting ? 0.5 : 1,
cursor: isDeleting ? "not-allowed" : "pointer"
cursor: isDeleting ? "not-allowed" : "pointer",
borderColor: "#3d3d3d",
borderWidth: "2px",
color: "#fff",
}}
>
Connect
</Button>
{isOwner && (
<>
<Button
className="text-black"
onClick={(e) => {
e.stopPropagation();
setSelectedHostForShare(hostWrapper);
setIsShareModalHidden(false);
}}
disabled={isDeleting}
sx={{
backgroundColor: "#6e6e6e",
"&:hover": { backgroundColor: "#0f0f0f" },
opacity: isDeleting ? 0.5 : 1,
cursor: isDeleting ? "not-allowed" : "pointer"
}}
>
Share
</Button>
<Button
className="text-black"
onClick={(e) => handleDelete(e, hostWrapper)}
disabled={isDeleting}
sx={{
backgroundColor: "#6e6e6e",
"&:hover": { backgroundColor: "#0f0f0f" },
opacity: isDeleting ? 0.5 : 1,
cursor: isDeleting ? "not-allowed" : "pointer"
}}
>
{isDeleting ? "Deleting..." : "Delete"}
</Button>
<Button
className="text-black"
onClick={(e) => {
e.stopPropagation();
openEditPanel(hostConfig);
}}
disabled={isDeleting}
sx={{
backgroundColor: "#6e6e6e",
"&:hover": { backgroundColor: "#0f0f0f" },
opacity: isDeleting ? 0.5 : 1,
cursor: isDeleting ? "not-allowed" : "pointer"
}}
>
Edit
</Button>
</>
)}
{!isOwner && (
<Button
className="text-black"
onClick={(e) => handleDelete(e, hostWrapper)}
disabled={isDeleting}
sx={{
backgroundColor: "#6e6e6e",
"&:hover": { backgroundColor: "#0f0f0f" },
opacity: isDeleting ? 0.5 : 1,
cursor: isDeleting ? "not-allowed" : "pointer"
}}
>
{isDeleting ? "Removing..." : "Remove Share"}
</Button>
)}
<IconButton
variant="outlined"
className="text-white"
onClick={(e) => {
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",
}}
>
</IconButton>
</div>
</div>
);
@@ -352,7 +344,6 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
return (
<>
{/* Render hosts without folders first */}
<div
className={`flex flex-col gap-2 p-2 rounded-lg transition-colors ${isDraggingOver === 'no-folder' ? 'bg-neutral-700' : ''}`}
onDragOver={(e) => handleDragOver(e, 'no-folder')}
@@ -362,7 +353,6 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
{noFolder.map((host) => renderHostItem(host))}
</div>
{/* Render folders and their hosts */}
{sortedFolders.map((folderName) => (
<div key={folderName} className="mb-2">
<div
@@ -403,6 +393,68 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
handleShare={handleShare}
hostConfig={selectedHostForShare}
/>
<Menu
ref={menuRef}
anchorEl={anchorEl.current}
open={isMenuOpen}
onClose={() => {
setIsMenuOpen(false);
setSelectedHost(null);
}}
sx={{
"& .MuiMenu-list": {
backgroundColor: "#6e6e6e",
color: "white"
}
}}
>
{selectedHost && (
selectedHost.createdBy?._id === userRef.current?.getUser()?.id ? (
<>
<MenuItem
onClick={(e) => {
e.stopPropagation();
setSelectedHostForShare(selectedHost);
setIsShareModalHidden(false);
setIsMenuOpen(false);
}}
>
Share
</MenuItem>
<MenuItem
onClick={(e) => {
e.stopPropagation();
openEditPanel(selectedHost.config);
setIsMenuOpen(false);
}}
>
Edit
</MenuItem>
<MenuItem
onClick={(e) => {
e.stopPropagation();
handleDelete(e, selectedHost);
setIsMenuOpen(false);
}}
disabled={isDeleting}
>
{isDeleting ? "Deleting..." : "Delete"}
</MenuItem>
</>
) : (
<MenuItem
onClick={(e) => {
e.stopPropagation();
handleDelete(e, selectedHost);
setIsMenuOpen(false);
}}
disabled={isDeleting}
>
{isDeleting ? "Removing..." : "Remove Share"}
</MenuItem>
)
)}
</Menu>
</div>
);
}
@@ -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;

View File

@@ -6,7 +6,7 @@ import io from "socket.io-client";
import PropTypes from "prop-types";
import theme from "../../theme.js";
export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidden }, ref) => {
export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidden, setErrorMessage, setIsErrorHidden }, ref) => {
const terminalRef = useRef(null);
const socketRef = useRef(null);
const fitAddon = useRef(new FitAddon());
@@ -18,8 +18,8 @@ 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`;
@@ -50,6 +50,9 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde
fontSize: 14,
scrollback: 1000,
ignoreBracketedPasteMode: true,
letterSpacing: 0,
lineHeight: 1,
padding: 2,
});
terminalInstance.current.loadAddon(fitAddon.current);
@@ -62,21 +65,28 @@ 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.rsaKey?.trim() && !authModalShown) {
if (isAuthError && !hostConfig.password?.trim() && !hostConfig.sshKey?.trim() && !authModalShown) {
authModalShown = true;
setIsNoAuthHidden(false);
}
@@ -88,7 +98,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,16 +108,19 @@ 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(),
rsaKey: hostConfig.sshKey?.trim() || hostConfig.rsaKey?.trim(),
};
socket.emit("connectToHost", cols, rows, sshConfig);
});
setTimeout(() => {
fitAddon.current.fit();
resizeTerminal();
terminalInstance.current.focus();
if (terminalInstance.current) {
fitAddon.current.fit();
resizeTerminal();
terminalInstance.current.focus();
}
}, 50);
socket.on("data", (data) => {
@@ -117,68 +130,85 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde
let isPasting = false;
terminalInstance.current.onData((data) => {
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();
navigator.clipboard.readText().then((text) => {
text = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
const lines = text.split("\n");
if (socketRef.current) {
let index = 0;
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) => {
console.error("Failed to read clipboard contents:", err);
isPasting = false;
});
return false;
}
return true;
});
terminalInstance.current.onKey(({ domEvent }) => {
if (domEvent.key === "c" && (domEvent.ctrlKey || domEvent.metaKey)) {
const selection = terminalInstance.current.getSelection();
if (selection) {
navigator.clipboard.writeText(selection);
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();
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);
}
}).catch(() => {
setErrorMessage("Paste failed: Clipboard access denied. Instead, use Control Shift V.");
setIsErrorHidden(false);
}).finally(() => {
setTimeout(() => {
isPasting = false;
}, 300);
});
return false;
}
return true;
});
terminalInstance.current.onKey(({ domEvent }) => {
if (domEvent.key === "c" && (domEvent.ctrlKey || domEvent.metaKey)) {
const selection = terminalInstance.current.getSelection();
if (selection) {
navigator.clipboard.writeText(selection);
}
}
});
}
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);
}
});
socket.on("disconnect", (reason) => {
if (terminalInstance.current) {
terminalInstance.current.write(`\r\n*** Socket disconnected: ${reason} ***\r\n`);
}
});
socket.on("reconnect", (attemptNumber) => {
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);
if (terminalInstance.current) {
terminalInstance.current.write(`\r\n*** Socket reconnect error: ${error.message} ***\r\n`);
}
});
const pingInterval = setInterval(() => {
if (socketRef.current && socketRef.current.connected) {
socketRef.current.emit("ping");
}
}, 5000);
socketRef.current.on("pong", () => {});
return () => {
clearInterval(pingInterval);
if (terminalInstance.current) {
terminalInstance.current.dispose();
terminalInstance.current = null;
@@ -202,14 +232,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);
};
}, []);
@@ -222,7 +259,7 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde
position: 'absolute',
width: '100%',
height: '100%',
transform: 'translateY(5px) translateX(5px)',
transform: 'translateY(2px) translateX(3px)',
}}
/>
);
@@ -235,9 +272,12 @@ NewTerminal.propTypes = {
ip: PropTypes.string.isRequired,
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,
setIsNoAuthHidden: PropTypes.func.isRequired,
setErrorMessage: PropTypes.func.isRequired,
setIsErrorHidden: PropTypes.func.isRequired,
};

View File

@@ -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,
@@ -186,7 +207,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 {
@@ -222,7 +243,18 @@ export const User = forwardRef(({ onLoginSuccess, onCreateSuccess, onDeleteSucce
if (!currentUser.current) return onFailure("Not authenticated");
try {
console.log('Editing host with configs:', { oldHostConfig, newHostConfig });
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,

View File

@@ -185,19 +185,36 @@ 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;
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,
@@ -415,7 +464,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);
@@ -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}` });
}
});

View File

@@ -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 = {
@@ -32,7 +35,7 @@ io.on("connection", (socket) => {
return;
}
if (!hostConfig.password && !hostConfig.rsaKey) {
if (!hostConfig.password && !hostConfig.sshKey) {
logger.error("No authentication provided");
socket.emit("error", "Authentication required");
return;
@@ -42,18 +45,18 @@ io.on("connection", (socket) => {
ip: hostConfig.ip,
port: hostConfig.port,
user: hostConfig.user,
authType: hostConfig.password ? 'password' : 'public key',
authType: hostConfig.password ? 'password' : 'key',
};
logger.info("Connecting with config:", safeHostConfig);
const { ip, port, user, password, rsaKey } = hostConfig;
const { ip, port, user, password, sshKey, } = hostConfig;
const conn = new SSHClient();
conn
.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,12 +96,22 @@ 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,
username: user,
password: password,
privateKey: rsaKey ? Buffer.from(rsaKey) : undefined,
password: password || undefined,
privateKey: sshKey ? Buffer.from(sshKey) : undefined,
algorithms: {
kex: ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'ecdh-sha2-nistp256'],
serverHostKey: ['ssh-ed25519', 'ecdsa-sha2-nistp256']
},
keepaliveInterval: 10000,
keepaliveCountMax: 5,
readyTimeout: 5000,
});
});

View File

@@ -7,8 +7,6 @@ import {
FormLabel,
Input,
Stack,
DialogTitle,
DialogContent,
ModalDialog,
Select,
Option,
@@ -27,19 +25,51 @@ 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];
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';
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,
sshKey: keyContent,
keyType: keyType,
authMethod: 'sshKey'
}));
};
reader.readAsText(file);
} else {
alert('Please upload a valid SSH key file (RSA, ED25519, ECDSA, DSA, PEM, or PPK format).');
}
};
@@ -48,19 +78,25 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
...prev,
authMethod: newMethod,
password: "",
rsaKey: ""
sshKey: "",
keyType: "",
}));
};
const isFormValid = () => {
if (!form.ip || !form.user || !form.port) return false;
const portNum = Number(form.port);
const { ip, user, port, authMethod, password, sshKey } = form;
if (!ip?.trim() || !user?.trim() || !port) return false;
const portNum = Number(port);
if (isNaN(portNum) || portNum < 1 || portNum > 65535) return false;
if (!form.rememberHost) return true;
if (form.rememberHost) {
if (form.authMethod === 'Select Auth') return false;
if (form.authMethod === 'rsaKey' && !form.rsaKey) 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 === 'sshKey' && !sshKey?.trim()) return false;
}
return true;
@@ -68,26 +104,29 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
const handleSubmit = (event) => {
event.preventDefault();
if (isFormValid()) {
if (!form.rememberHost) {
handleAddHost();
} else {
handleAddHost();
}
setErrorMessage("");
setShowError(false);
setForm({
name: '',
folder: '',
ip: '',
user: '',
password: '',
rsaKey: '',
port: 22,
authMethod: 'Select Auth',
rememberHost: false,
storePassword: true,
});
setIsAddHostHidden(true);
if (!form.ip?.trim()) {
setErrorMessage("Please provide an IP address.");
setShowError(true);
return;
}
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);
}
};
@@ -98,15 +137,18 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
backdropFilter: 'blur(5px)',
backgroundColor: 'rgba(0, 0, 0, 0.2)',
}}
>
<ModalDialog
layout="center"
variant="outlined"
sx={{
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%',
@@ -116,135 +158,225 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
mx: 2,
}}
>
<DialogTitle sx={{ mb: 2 }}>Add Host</DialogTitle>
<DialogContent>
<form onSubmit={handleSubmit}>
<Tabs
value={activeTab}
onChange={(e, val) => setActiveTab(val)}
sx={{
backgroundColor: theme.palette.general.disabled,
borderRadius: '8px',
padding: '8px',
marginBottom: '16px',
width: '100%',
}}
>
<TabList
sx={{
width: '100%',
gap: 0,
mb: 2,
'& button': {
flex: 1,
bgcolor: 'transparent',
color: theme.palette.text.secondary,
'&:hover': {
bgcolor: 'rgba(255, 255, 255, 0.1)',
},
'&.Mui-selected': {
bgcolor: theme.palette.general.primary,
color: theme.palette.text.primary,
'&:hover': {
bgcolor: theme.palette.general.primary,
},
},
{showError && (
<div style={{
backgroundColor: "#c53030",
color: "white",
padding: "10px",
textAlign: "center",
borderTopLeftRadius: "10px",
borderTopRightRadius: "10px"
}}>
{errorMessage}
</div>
)}
<Tabs
value={activeTab}
onChange={(e, val) => setActiveTab(val)}
sx={{
width: '100%',
mb: 0,
backgroundColor: theme.palette.general.tertiary,
}}
>
<TabList
sx={{
width: '100%',
gap: 0,
borderTopLeftRadius: 10,
borderTopRightRadius: 10,
backgroundColor: theme.palette.general.primary,
'& button': {
flex: 1,
bgcolor: 'transparent',
color: theme.palette.text.secondary,
'&:hover': {
bgcolor: theme.palette.general.disabled,
},
'&.Mui-selected': {
bgcolor: theme.palette.general.tertiary,
color: theme.palette.text.primary,
'&:hover': {
bgcolor: theme.palette.general.tertiary,
},
}}
>
<Tab>Basic Info</Tab>
<Tab>Connection</Tab>
<Tab>Authentication</Tab>
</TabList>
},
},
}}
>
<Tab sx={{ flex: 1 }}>Basic Info</Tab>
<Tab sx={{ flex: 1 }}>Connection</Tab>
<Tab sx={{ flex: 1 }}>Authentication</Tab>
</TabList>
<TabPanel value={0}>
<Stack spacing={2}>
<FormControl>
<FormLabel>Host Name</FormLabel>
<Input
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
sx={{
backgroundColor: theme.palette.general.primary,
<div style={{ padding: '24px', backgroundColor: theme.palette.general.tertiary }}>
<TabPanel value={0}>
<Stack spacing={2}>
<FormControl>
<FormLabel>Host Name</FormLabel>
<Input
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
/>
</FormControl>
<FormControl>
<FormLabel>Folder</FormLabel>
<Input
value={form.folder || ''}
onChange={(e) => setForm({ ...form, folder: e.target.value })}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
/>
</FormControl>
<FormControl>
<FormLabel>Remember Host</FormLabel>
<Checkbox
checked={Boolean(form.rememberHost)}
onChange={(e) => setForm({
...form,
rememberHost: e.target.checked,
})}
sx={{
color: theme.palette.text.primary,
'&.Mui-checked': {
color: theme.palette.text.primary,
}}
/>
</FormControl>
<FormControl>
<FormLabel>Folder</FormLabel>
<Input
value={form.folder || ''}
onChange={(e) => setForm({ ...form, folder: e.target.value })}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
/>
</FormControl>
</Stack>
</TabPanel>
},
}}
/>
</FormControl>
</Stack>
</TabPanel>
<TabPanel value={1}>
<Stack spacing={2}>
<FormControl error={!form.ip}>
<FormLabel>Host IP</FormLabel>
<Input
value={form.ip}
onChange={(e) => setForm({ ...form, ip: e.target.value })}
required
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
/>
</FormControl>
<FormControl error={!form.user}>
<FormLabel>Host User</FormLabel>
<Input
value={form.user}
onChange={(e) => setForm({ ...form, user: e.target.value })}
required
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
/>
</FormControl>
<FormControl error={form.port < 1 || form.port > 65535}>
<FormLabel>Host Port</FormLabel>
<Input
type="number"
value={form.port}
onChange={(e) => setForm({ ...form, port: e.target.value })}
min={1}
max={65535}
required
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
/>
</FormControl>
</Stack>
</TabPanel>
<TabPanel value={1}>
<Stack spacing={2}>
<FormControl error={!form.ip}>
<FormLabel>Host IP</FormLabel>
<Input
value={form.ip}
onChange={(e) => setForm({ ...form, ip: e.target.value })}
required
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
/>
</FormControl>
<FormControl error={!form.user}>
<FormLabel>Host User</FormLabel>
<Input
value={form.user}
onChange={(e) => setForm({ ...form, user: e.target.value })}
required
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
/>
</FormControl>
<FormControl error={form.port < 1 || form.port > 65535}>
<FormLabel>Host Port</FormLabel>
<Input
type="number"
value={form.port}
onChange={(e) => setForm({ ...form, port: e.target.value })}
min={1}
max={65535}
required
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
/>
</FormControl>
</Stack>
</TabPanel>
<TabPanel value={2}>
<Stack spacing={2}>
<TabPanel value={2}>
<Stack spacing={2}>
<FormControl error={!form.authMethod || form.authMethod === 'Select Auth'}>
<FormLabel>Authentication Method</FormLabel>
<Select
value={form.authMethod}
onChange={(e, val) => handleAuthChange(val)}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
>
<Option value="Select Auth" disabled>Select Auth</Option>
<Option value="password">Password</Option>
<Option value="sshKey">SSH Key</Option>
</Select>
</FormControl>
{form.authMethod === 'password' && (
<FormControl error={!form.password}>
<FormLabel>Password</FormLabel>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Input
type={showPassword ? 'text' : 'password'}
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
flex: 1
}}
/>
<IconButton
onClick={() => setShowPassword(!showPassword)}
sx={{
color: theme.palette.text.primary,
marginLeft: 1
}}
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</div>
</FormControl>
)}
{form.authMethod === 'sshKey' && (
<Stack spacing={2}>
<FormControl error={!form.sshKey}>
<FormLabel>SSH Key</FormLabel>
<Button
component="label"
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '40px',
'&:hover': {
backgroundColor: theme.palette.general.disabled,
},
}}
>
{form.sshKey ? `Change ${form.keyType || 'SSH'} Key File` : 'Upload SSH Key File'}
<Input
type="file"
onChange={handleFileChange}
sx={{ display: 'none' }}
/>
</Button>
</FormControl>
</Stack>
)}
{form.rememberHost && (
<FormControl>
<FormLabel>Remember Host</FormLabel>
<FormLabel>Store Password</FormLabel>
<Checkbox
checked={form.rememberHost}
onChange={(e) => setForm({
...form,
rememberHost: e.target.checked,
...((!e.target.checked) && {
authMethod: 'Select Auth',
password: '',
rsaKey: '',
storePassword: true
})
})}
checked={Boolean(form.storePassword)}
onChange={(e) => setForm({ ...form, storePassword: e.target.checked })}
sx={{
color: theme.palette.text.primary,
'&.Mui-checked': {
@@ -253,119 +385,32 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
}}
/>
</FormControl>
{form.rememberHost && (
<>
<FormControl>
<FormLabel>Store Password</FormLabel>
<Checkbox
checked={form.storePassword}
onChange={(e) => setForm({ ...form, storePassword: e.target.checked })}
sx={{
color: theme.palette.text.primary,
'&.Mui-checked': {
color: theme.palette.text.primary,
},
}}
/>
</FormControl>
<FormControl error={!form.authMethod || form.authMethod === 'Select Auth'}>
<FormLabel>Authentication Method</FormLabel>
<Select
value={form.authMethod}
onChange={(e, val) => handleAuthChange(val)}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
>
<Option value="Select Auth" disabled>Select Auth</Option>
<Option value="password">Password</Option>
<Option value="rsaKey">Public Key</Option>
</Select>
</FormControl>
)}
</Stack>
</TabPanel>
</div>
{form.authMethod === 'password' && (
<FormControl error={!form.password}>
<FormLabel>Password</FormLabel>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Input
type={showPassword ? 'text' : 'password'}
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
flex: 1
}}
/>
<IconButton
onClick={() => setShowPassword(!showPassword)}
sx={{
color: theme.palette.text.primary,
marginLeft: 1
}}
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</div>
</FormControl>
)}
{form.authMethod === 'rsaKey' && (
<FormControl error={!form.rsaKey}>
<FormLabel>Public Key</FormLabel>
<Button
component="label"
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '40px',
'&:hover': {
backgroundColor: theme.palette.general.disabled,
},
}}
>
{form.rsaKey ? 'Change Public Key File' : 'Upload Public Key File'}
<Input
type="file"
onChange={handleFileChange}
sx={{ display: 'none' }}
/>
</Button>
</FormControl>
)}
</>
)}
</Stack>
</TabPanel>
</Tabs>
<Button
type="submit"
disabled={!isFormValid()}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
'&:hover': {
backgroundColor: theme.palette.general.disabled,
},
'&:disabled': {
backgroundColor: 'rgba(255, 255, 255, 0.1)',
color: 'rgba(255, 255, 255, 0.3)',
},
marginTop: 3,
width: '100%',
height: '40px',
}}
>
Add Host
</Button>
</form>
</DialogContent>
<Button
onClick={handleSubmit}
disabled={!isFormValid()}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
'&:hover': {
backgroundColor: theme.palette.general.disabled,
},
'&:disabled': {
backgroundColor: 'rgba(255, 255, 255, 0.1)',
color: 'rgba(255, 255, 255, 0.3)',
},
marginTop: 1,
width: '100%',
height: '40px',
}}
>
Add Host
</Button>
</Tabs>
</ModalDialog>
</Modal>
</CssVarsProvider>
@@ -374,18 +419,7 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
AddHostModal.propTypes = {
isHidden: PropTypes.bool.isRequired,
form: PropTypes.shape({
name: PropTypes.string,
folder: PropTypes.string,
ip: PropTypes.string.isRequired,
user: PropTypes.string.isRequired,
password: PropTypes.string,
rsaKey: PropTypes.string,
port: PropTypes.number.isRequired,
authMethod: PropTypes.string.isRequired,
rememberHost: PropTypes.bool,
storePassword: PropTypes.bool,
}).isRequired,
form: PropTypes.object.isRequired,
setForm: PropTypes.func.isRequired,
handleAddHost: PropTypes.func.isRequired,
setIsAddHostHidden: PropTypes.func.isRequired,

306
src/modals/AuthModal.jsx Normal file
View File

@@ -0,0 +1,306 @@
import PropTypes from 'prop-types';
import { CssVarsProvider } from '@mui/joy/styles';
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,
handleCreateUser,
handleGuestLogin,
setIsAuthModalHidden
}) => {
const [activeTab, setActiveTab] = useState(0);
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
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 handleLogin = async () => {
setIsLoading(true);
try {
await handleLoginUser({
...form,
onSuccess: () => {
setIsLoading(false);
setIsAuthModalHidden(true);
},
onFailure: () => setIsLoading(false),
});
} catch (error) {
setIsLoading(false);
}
};
const handleCreate = async () => {
setIsLoading(true);
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) resetForm();
}, [isHidden]);
const isLoginValid = !!form.username && !!form.password;
const isCreateValid = isLoginValid && form.password === form.confirmPassword;
return (
<CssVarsProvider theme={theme}>
<Modal open={!isHidden} onClose={() => setIsAuthModalHidden(true)}>
<ModalDialog
layout="center"
variant="outlined"
sx={{
backgroundColor: theme.palette.general.tertiary,
borderColor: theme.palette.general.secondary,
color: theme.palette.text.primary,
padding: 0,
borderRadius: 10,
maxWidth: '400px',
width: '100%',
overflow: 'hidden',
}}
>
<Tabs
value={activeTab}
onChange={(e, val) => setActiveTab(val)}
sx={{
width: '100%',
backgroundColor: theme.palette.general.tertiary,
}}
>
<TabList
sx={{
gap: 0,
borderTopLeftRadius: 10,
borderTopRightRadius: 10,
backgroundColor: theme.palette.general.primary,
'& button': {
flex: 1,
bgcolor: 'transparent',
color: theme.palette.text.secondary,
'&:hover': {
bgcolor: theme.palette.general.disabled,
},
'&.Mui-selected': {
bgcolor: theme.palette.general.tertiary,
color: theme.palette.text.primary,
'&:hover': {
bgcolor: theme.palette.general.tertiary,
},
},
},
}}
>
<Tab sx={{ flex: 1 }}>Login</Tab>
<Tab sx={{ flex: 1 }}>Create</Tab>
</TabList>
<DialogContent sx={{ padding: 3, backgroundColor: theme.palette.general.tertiary }}>
<TabPanel value={0} sx={{ p: 0 }}>
<Stack spacing={2} component="form" onSubmit={(e) => { e.preventDefault(); handleLogin(); }}>
<FormControl>
<FormLabel>Username</FormLabel>
<Input
disabled={isLoading}
value={form.username}
onChange={(e) => setForm({ ...form, username: e.target.value })}
sx={inputStyle}
/>
</FormControl>
<FormControl>
<FormLabel>Password</FormLabel>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Input
disabled={isLoading}
type={showPassword ? 'text' : 'password'}
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
sx={{ ...inputStyle, flex: 1 }}
/>
<IconButton
disabled={isLoading}
onClick={() => setShowPassword(!showPassword)}
sx={iconButtonStyle}
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</div>
</FormControl>
<Button
type="submit"
disabled={!isLoginValid || isLoading}
sx={buttonStyle}
>
{isLoading ? "Logging in..." : "Login"}
</Button>
<Button
disabled={isLoading}
onClick={handleGuest}
sx={buttonStyle}
>
{isLoading ? "Logging in..." : "Continue as Guest"}
</Button>
</Stack>
</TabPanel>
<TabPanel value={1} sx={{ p: 0 }}>
<Stack spacing={2} component="form" onSubmit={(e) => { e.preventDefault(); handleCreate(); }}>
<FormControl>
<FormLabel>Username</FormLabel>
<Input
disabled={isLoading}
value={form.username}
onChange={(e) => setForm({ ...form, username: e.target.value })}
sx={inputStyle}
/>
</FormControl>
<FormControl>
<FormLabel>Password</FormLabel>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Input
disabled={isLoading}
type={showPassword ? 'text' : 'password'}
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
sx={{ ...inputStyle, flex: 1 }}
/>
<IconButton
disabled={isLoading}
onClick={() => setShowPassword(!showPassword)}
sx={iconButtonStyle}
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</div>
</FormControl>
<FormControl>
<FormLabel>Confirm Password</FormLabel>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Input
disabled={isLoading}
type={showConfirmPassword ? 'text' : 'password'}
value={form.confirmPassword || ''}
onChange={(e) => setForm({ ...form, confirmPassword: e.target.value })}
sx={{ ...inputStyle, flex: 1 }}
/>
<IconButton
disabled={isLoading}
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
sx={iconButtonStyle}
>
{showConfirmPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</div>
</FormControl>
<Button
type="submit"
disabled={!isCreateValid || isLoading}
sx={buttonStyle}
>
{isLoading ? "Creating..." : "Create Account"}
</Button>
</Stack>
</TabPanel>
</DialogContent>
</Tabs>
</ModalDialog>
</Modal>
</CssVarsProvider>
);
};
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,
handleCreateUser: PropTypes.func.isRequired,
handleGuestLogin: PropTypes.func.isRequired,
setIsAuthModalHidden: PropTypes.func.isRequired,
};
export default AuthModal;

View File

@@ -1,166 +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 isFormValid = () => {
if (!form.username || !form.password || form.password !== confirmPassword) return false;
return true;
};
const handleCreate = () => {
handleCreateUser({
...form
});
};
useEffect(() => {
if (isHidden) {
setForm({ username: '', password: '' });
setConfirmPassword('');
}
}, [isHidden]);
return (
<CssVarsProvider theme={theme}>
<Modal open={!isHidden} onClose={() => {}}>
<ModalDialog
layout="center"
sx={{
backgroundColor: theme.palette.general.tertiary,
borderColor: theme.palette.general.secondary,
color: theme.palette.text.primary,
padding: 3,
borderRadius: 10,
width: "auto",
maxWidth: "90vw",
minWidth: "fit-content",
overflow: "hidden",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<DialogTitle>Create</DialogTitle>
<DialogContent>
<form
onSubmit={(event) => {
event.preventDefault();
if (isFormValid()) handleCreate();
}}
>
<Stack spacing={2} sx={{ width: "100%", maxWidth: "100%", overflow: "hidden" }}>
<FormControl>
<FormLabel>Username</FormLabel>
<Input
value={form.username}
onChange={(event) => setForm({ ...form, username: event.target.value })}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
/>
</FormControl>
<FormControl>
<FormLabel>Password</FormLabel>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Input
type={showPassword ? 'text' : 'password'}
value={form.password}
onChange={(event) => setForm({ ...form, password: event.target.value })}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
flex: 1,
}}
/>
<IconButton
onClick={() => setShowPassword(!showPassword)}
sx={{
color: theme.palette.text.primary,
marginLeft: 1,
}}
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</div>
</FormControl>
<FormControl>
<FormLabel>Confirm Password</FormLabel>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Input
type={showConfirmPassword ? 'text' : 'password'}
value={confirmPassword}
onChange={(event) => setConfirmPassword(event.target.value)}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
flex: 1,
}}
/>
<IconButton
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
sx={{
color: theme.palette.text.primary,
marginLeft: 1,
}}
>
{showConfirmPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</div>
</FormControl>
<Button
type="submit"
disabled={!isFormValid()}
sx={{
backgroundColor: theme.palette.general.primary,
'&:hover': {
backgroundColor: theme.palette.general.disabled,
},
}}
>
Create
</Button>
<Button
onClick={() => {
setForm({ username: '', password: '' });
setConfirmPassword('');
setIsCreateUserHidden(true);
setIsLoginUserHidden(false);
}}
sx={{
backgroundColor: theme.palette.general.primary,
'&:hover': {
backgroundColor: theme.palette.general.disabled,
},
}}
>
Back
</Button>
</Stack>
</form>
</DialogContent>
</ModalDialog>
</Modal>
</CssVarsProvider>
);
};
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;

View File

@@ -8,8 +8,6 @@ import {
FormLabel,
Input,
Stack,
DialogTitle,
DialogContent,
ModalDialog,
Select,
Option,
@@ -24,10 +22,25 @@ import theme from '/src/theme';
import Visibility from '@mui/icons-material/Visibility';
import VisibilityOff from '@mui/icons-material/VisibilityOff';
const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostHidden, hostConfig }) => {
const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHost }) => {
const [form, setForm] = useState({
name: '',
folder: '',
ip: '',
user: '',
port: '',
password: '',
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) {
@@ -37,54 +50,84 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
ip: hostConfig.ip || '',
user: hostConfig.user || '',
password: hostConfig.password || '',
rsaKey: hostConfig.rsaKey || '',
sshKey: hostConfig.sshKey || '',
keyType: hostConfig.keyType || '',
port: hostConfig.port || 22,
authMethod: hostConfig.password ? 'password' : hostConfig.rsaKey ? 'rsaKey' : 'Select Auth',
authMethod: hostConfig.password ? 'password' : hostConfig.sshKey ? 'key' : 'Select Auth',
rememberHost: true,
storePassword: !!(hostConfig.password || hostConfig.rsaKey),
storePassword: !!(hostConfig.password || hostConfig.sshKey),
});
}
}, [isHidden, hostConfig]);
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';
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,
sshKey: 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).');
}
};
const handleAuthChange = (newMethod) => {
setForm((prev) => ({
...prev,
authMethod: newMethod
}));
};
const handleStorePasswordChange = (checked) => {
setForm((prev) => ({
...prev,
storePassword: Boolean(checked),
password: checked ? prev.password : "",
rsaKey: checked ? prev.rsaKey : "",
authMethod: checked ? prev.authMethod : "Select Auth"
authMethod: newMethod,
password: "",
sshKey: "",
keyType: "",
}));
};
const isFormValid = () => {
const { ip, user, port, authMethod, password, rsaKey, storePassword } = form;
const { ip, user, port, authMethod, password, sshKey } = form;
if (!ip?.trim() || !user?.trim() || !port) return false;
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;
if (form.storePassword) {
if (authMethod === 'Select Auth') return false;
if (authMethod === 'password' && !password?.trim()) return false;
if (authMethod === 'key' && !sshKey?.trim()) return false;
}
return true;
};
@@ -92,18 +135,49 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
const handleSubmit = async (event) => {
event.preventDefault();
if (isLoading) return;
setIsLoading(true);
try {
await handleEditHost(hostConfig, {
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,
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.sshKey = form.sshKey;
newConfig.keyType = form.keyType;
}
}
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);
}
@@ -115,11 +189,9 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
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)',
}}
@@ -131,7 +203,7 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
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%',
@@ -141,134 +213,133 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
mx: 2,
}}
>
<DialogTitle sx={{ mb: 2 }}>Edit Host</DialogTitle>
<DialogContent>
<form onSubmit={handleSubmit}>
<Tabs
value={activeTab}
onChange={(e, val) => setActiveTab(val)}
sx={{
backgroundColor: theme.palette.general.disabled,
borderRadius: '8px',
padding: '8px',
marginBottom: '16px',
width: '100%',
}}
>
<TabList
sx={{
width: '100%',
gap: 0,
mb: 2,
'& button': {
flex: 1,
bgcolor: 'transparent',
color: theme.palette.text.secondary,
'&:hover': {
bgcolor: 'rgba(255, 255, 255, 0.1)',
},
'&.Mui-selected': {
bgcolor: theme.palette.general.primary,
color: theme.palette.text.primary,
'&:hover': {
bgcolor: theme.palette.general.primary,
},
},
{showError && (
<div style={{
backgroundColor: "#c53030",
color: "white",
padding: "10px",
textAlign: "center",
borderTopLeftRadius: "10px",
borderTopRightRadius: "10px"
}}>
{errorMessage}
</div>
)}
<Tabs
value={activeTab}
onChange={(e, val) => setActiveTab(val)}
sx={{
width: '100%',
mb: 0,
backgroundColor: theme.palette.general.tertiary,
}}
>
<TabList
sx={{
width: '100%',
gap: 0,
borderTopLeftRadius: 10,
borderTopRightRadius: 10,
backgroundColor: theme.palette.general.primary,
'& button': {
flex: 1,
bgcolor: 'transparent',
color: theme.palette.text.secondary,
'&:hover': {
bgcolor: theme.palette.general.disabled,
},
'&.Mui-selected': {
bgcolor: theme.palette.general.tertiary,
color: theme.palette.text.primary,
'&:hover': {
bgcolor: theme.palette.general.tertiary,
},
}}
>
<Tab>Basic Info</Tab>
<Tab>Connection</Tab>
<Tab>Authentication</Tab>
</TabList>
},
},
}}
>
<Tab sx={{ flex: 1 }}>Basic Info</Tab>
<Tab sx={{ flex: 1 }}>Connection</Tab>
<Tab sx={{ flex: 1 }}>Authentication</Tab>
</TabList>
<TabPanel value={0}>
<Stack spacing={2}>
<FormControl>
<FormLabel>Host Name</FormLabel>
<Input
value={form.name}
onChange={(e) => setForm((prev) => ({ ...prev, name: e.target.value }))}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary
}}
/>
</FormControl>
<div style={{ padding: '24px', backgroundColor: theme.palette.general.tertiary }}>
<TabPanel value={0}>
<Stack spacing={2}>
<FormControl>
<FormLabel>Host Name</FormLabel>
<Input
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
/>
</FormControl>
<FormControl>
<FormLabel>Folder</FormLabel>
<Input
value={form.folder || ''}
onChange={(e) => setForm({ ...form, folder: e.target.value })}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
/>
</FormControl>
</Stack>
</TabPanel>
<FormControl>
<FormLabel>Folder</FormLabel>
<Input
value={form.folder}
onChange={(e) => setForm((prev) => ({ ...prev, folder: e.target.value }))}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary
}}
/>
</FormControl>
</Stack>
</TabPanel>
<TabPanel value={1}>
<Stack spacing={2}>
<FormControl error={!form.ip}>
<FormLabel>Host IP</FormLabel>
<Input
value={form.ip}
onChange={(e) => setForm({ ...form, ip: e.target.value })}
required
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
/>
</FormControl>
<FormControl error={!form.user}>
<FormLabel>Host User</FormLabel>
<Input
value={form.user}
onChange={(e) => setForm({ ...form, user: e.target.value })}
required
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
/>
</FormControl>
<FormControl error={form.port < 1 || form.port > 65535}>
<FormLabel>Host Port</FormLabel>
<Input
type="number"
value={form.port}
onChange={(e) => setForm({ ...form, port: e.target.value })}
min={1}
max={65535}
required
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
/>
</FormControl>
</Stack>
</TabPanel>
<TabPanel value={1}>
<Stack spacing={2}>
<FormControl error={!form.ip}>
<FormLabel>Host IP</FormLabel>
<Input
value={form.ip}
onChange={(e) => setForm((prev) => ({ ...prev, ip: e.target.value }))}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary
}}
/>
</FormControl>
<FormControl error={form.port < 1 || form.port > 65535}>
<FormLabel>Host Port</FormLabel>
<Input
type="number"
value={form.port}
onChange={(e) => setForm((prev) => ({ ...prev, port: e.target.value }))}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary
}}
/>
</FormControl>
<FormControl error={!form.user}>
<FormLabel>Host User</FormLabel>
<Input
value={form.user}
onChange={(e) => setForm((prev) => ({ ...prev, user: e.target.value }))}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary
}}
/>
</FormControl>
</Stack>
</TabPanel>
<TabPanel value={2}>
<Stack spacing={2}>
<FormControl>
<FormLabel>Store Password</FormLabel>
<Checkbox
checked={form.storePassword}
onChange={(e) => handleStorePasswordChange(e.target.checked)}
sx={{
color: theme.palette.text.primary,
'&.Mui-checked': {
color: theme.palette.text.primary
}
}}
/>
</FormControl>
{form.storePassword && (
<FormControl error={form.authMethod === 'Select Auth'}>
<TabPanel value={2}>
<Stack spacing={2}>
{form.storePassword && (
<>
<FormControl error={!form.authMethod || form.authMethod === 'Select Auth'}>
<FormLabel>Authentication Method</FormLabel>
<Select
value={form.authMethod}
@@ -280,104 +351,126 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
>
<Option value="Select Auth" disabled>Select Auth</Option>
<Option value="password">Password</Option>
<Option value="rsaKey">Public Key</Option>
<Option value="key">SSH Key</Option>
</Select>
</FormControl>
)}
{form.authMethod === 'password' && form.storePassword && (
<FormControl error={!form.password}>
<FormLabel>Password</FormLabel>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Input
type={showPassword ? 'text' : 'password'}
value={form.password}
onChange={(e) => setForm((prev) => ({ ...prev, password: e.target.value }))}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
flex: 1
}}
/>
<IconButton
onClick={() => setShowPassword(!showPassword)}
sx={{
color: theme.palette.text.primary,
marginLeft: 1
}}
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</div>
</FormControl>
)}
{form.authMethod === 'password' && (
<FormControl error={!form.password}>
<FormLabel>Password</FormLabel>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Input
type={showPassword ? 'text' : 'password'}
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
flex: 1
}}
/>
<IconButton
onClick={() => setShowPassword(!showPassword)}
sx={{
color: theme.palette.text.primary,
marginLeft: 1
}}
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</div>
</FormControl>
)}
{form.authMethod === 'rsaKey' && form.storePassword && (
<FormControl error={!form.rsaKey && !hostConfig?.rsaKey}>
<FormLabel>Public Key</FormLabel>
<Button
component="label"
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '40px',
'&:hover': {
backgroundColor: theme.palette.general.disabled,
},
}}
>
{form.rsaKey ? 'Change Public Key File' : 'Upload Public Key File'}
<Input
type="file"
onChange={handleFileChange}
sx={{ display: 'none' }}
/>
</Button>
{hostConfig?.rsaKey && !form.rsaKey && (
<FormLabel
sx={{
color: theme.palette.text.secondary,
fontSize: '0.875rem',
mt: 1,
display: 'block',
textAlign: 'center'
}}
>
Existing key detected. Upload to replace.
</FormLabel>
)}
</FormControl>
)}
</Stack>
</TabPanel>
</Tabs>
{form.authMethod === 'key' && (
<Stack spacing={2}>
<FormControl error={!form.sshKey}>
<FormLabel>SSH Key</FormLabel>
<Button
component="label"
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '40px',
'&:hover': {
backgroundColor: theme.palette.general.disabled,
},
}}
>
{form.sshKey ? `Change ${form.keyType || 'SSH'} Key File` : 'Upload SSH Key File'}
<Input
type="file"
onChange={handleFileChange}
sx={{ display: 'none' }}
/>
</Button>
{hostConfig?.sshKey && !form.sshKey && (
<FormLabel
sx={{
color: theme.palette.text.secondary,
fontSize: '0.875rem',
mt: 1,
display: 'block',
textAlign: 'center'
}}
>
Existing {hostConfig.keyType || 'SSH'} key detected. Upload to replace.
</FormLabel>
)}
</FormControl>
</Stack>
)}
</>
)}
<Button
type="submit"
disabled={!isFormValid() || isLoading}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
'&:hover': {
backgroundColor: theme.palette.general.disabled
},
'&:disabled': {
backgroundColor: 'rgba(255, 255, 255, 0.1)',
color: 'rgba(255, 255, 255, 0.3)',
},
marginTop: 3,
width: '100%',
height: '40px',
}}
>
{isLoading ? "Saving..." : "Save Changes"}
</Button>
</form>
</DialogContent>
<FormControl>
<FormLabel>Store Password</FormLabel>
<Checkbox
checked={Boolean(form.storePassword)}
onChange={(e) => setForm({
...form,
storePassword: e.target.checked,
password: e.target.checked ? form.password : "",
sshKey: e.target.checked ? form.sshKey : "",
authMethod: e.target.checked ? form.authMethod : "Select Auth"
})}
sx={{
color: theme.palette.text.primary,
'&.Mui-checked': {
color: theme.palette.text.primary,
},
}}
/>
</FormControl>
</Stack>
</TabPanel>
</div>
<Button
onClick={handleSubmit}
disabled={isLoading || !isFormValid()}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
'&:hover': {
backgroundColor: theme.palette.general.disabled,
},
'&:disabled': {
backgroundColor: 'rgba(255, 255, 255, 0.1)',
color: 'rgba(255, 255, 255, 0.3)',
},
marginTop: 1,
width: '100%',
height: '40px',
}}
>
{isLoading ? "Saving changes..." : "Save changes"}
</Button>
</Tabs>
</ModalDialog>
</Modal>
</CssVarsProvider>
@@ -386,11 +479,9 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
EditHostModal.propTypes = {
isHidden: PropTypes.bool.isRequired,
form: PropTypes.object.isRequired,
setForm: PropTypes.func.isRequired,
handleEditHost: PropTypes.func.isRequired,
hostConfig: PropTypes.object,
setIsEditHostHidden: PropTypes.func.isRequired,
hostConfig: PropTypes.object
handleEditHost: PropTypes.func.isRequired
};
export default EditHostModal;

View File

@@ -1,150 +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 LoginUserModal = ({ isHidden, form, setForm, handleLoginUser, handleGuestLogin, setIsLoginUserHidden, setIsCreateUserHidden }) => {
const [showPassword, setShowPassword] = useState(false);
const isFormValid = () => {
if (!form.username || !form.password) return false;
return true;
};
const handleLogin = () => {
handleLoginUser({
...form,
});
};
useEffect(() => {
if (isHidden) {
setForm({ username: '', password: '' });
}
}, [isHidden]);
return (
<CssVarsProvider theme={theme}>
<Modal open={!isHidden} onClose={() => {}}>
<ModalDialog
layout="center"
sx={{
backgroundColor: theme.palette.general.tertiary,
borderColor: theme.palette.general.secondary,
color: theme.palette.text.primary,
padding: 3,
borderRadius: 10,
width: "auto",
maxWidth: "90vw",
minWidth: "fit-content",
overflow: "hidden",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<DialogTitle>Login</DialogTitle>
<DialogContent>
<form
onSubmit={(event) => {
event.preventDefault();
if (isFormValid()) handleLogin();
}}
>
<Stack spacing={2} sx={{ width: "100%", maxWidth: "100%", overflow: "hidden" }}>
<FormControl>
<FormLabel>Username</FormLabel>
<Input
value={form.username}
onChange={(event) => setForm({ ...form, username: event.target.value })}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
/>
</FormControl>
<FormControl>
<FormLabel>Password</FormLabel>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Input
type={showPassword ? 'text' : 'password'}
value={form.password}
onChange={(event) => setForm({ ...form, password: event.target.value })}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
flex: 1,
}}
/>
<IconButton
onClick={() => setShowPassword(!showPassword)}
sx={{
color: theme.palette.text.primary,
marginLeft: 1,
}}
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</div>
</FormControl>
<Button
type="submit"
disabled={!isFormValid()}
sx={{
backgroundColor: theme.palette.general.primary,
'&:hover': {
backgroundColor: theme.palette.general.disabled,
},
}}
>
Login
</Button>
<Button
onClick={() => {
setForm({ username: '', password: '' });
setIsCreateUserHidden(false);
setIsLoginUserHidden(true);
}}
sx={{
backgroundColor: theme.palette.general.primary,
'&:hover': {
backgroundColor: theme.palette.general.disabled,
},
}}
>
Create User
</Button>
<Button
onClick={handleGuestLogin}
sx={{
backgroundColor: theme.palette.general.primary,
'&:hover': {
backgroundColor: theme.palette.general.disabled,
},
}}
>
Login as Guest
</Button>
</Stack>
</form>
</DialogContent>
</ModalDialog>
</Modal>
</CssVarsProvider>
);
};
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;

View File

@@ -15,7 +15,7 @@ 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';
@@ -26,23 +26,91 @@ const NoAuthenticationModal = ({ isHidden, form, setForm, setIsNoAuthHidden, han
if (!form.authMethod) {
setForm(prev => ({
...prev,
authMethod: 'Select Auth'
authMethod: 'Select Auth',
password: '',
sshKey: '',
keyType: '',
}));
}
}, []);
const isFormValid = () => {
if (!form.authMethod || form.authMethod === 'Select Auth') return false;
if (form.authMethod === 'rsaKey' && !form.rsaKey) return false;
if (form.authMethod === 'sshKey' && !form.sshKey) return false;
if (form.authMethod === 'password' && !form.password) return false;
return true;
};
const handleSubmit = (event) => {
event.preventDefault();
if (isFormValid()) {
handleAuthSubmit(form);
setForm({ authMethod: 'Select Auth', password: '', rsaKey: '' });
const handleSubmit = (e) => {
e.preventDefault();
e.stopPropagation();
try {
if(isFormValid()) {
const formData = {
authMethod: form.authMethod,
password: form.authMethod === 'password' ? form.password : '',
sshKey: form.authMethod === 'sshKey' ? form.sshKey : '',
keyType: form.authMethod === 'sshKey' ? form.keyType : '',
};
handleAuthSubmit(formData);
setForm(prev => ({
...prev,
authMethod: 'Select Auth',
password: '',
sshKey: '',
keyType: '',
}));
}
} catch (error) {
console.error("Authentication form error:", error);
}
};
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';
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,
sshKey: keyContent,
keyType: keyType,
authMethod: 'sshKey'
});
};
reader.readAsText(file);
} else {
alert('Please upload a valid SSH key file (RSA, ED25519, ECDSA, DSA, PEM, or PPK format).');
}
};
@@ -85,7 +153,13 @@ const NoAuthenticationModal = ({ isHidden, form, setForm, setIsNoAuthHidden, han
<FormLabel>Authentication Method</FormLabel>
<Select
value={form.authMethod || 'Select Auth'}
onChange={(e, val) => setForm(prev => ({ ...prev, authMethod: val, password: '', rsaKey: '' }))}
onChange={(e, val) => setForm(prev => ({
...prev,
authMethod: val,
password: '',
sshKey: '',
keyType: '',
}))}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
@@ -93,7 +167,7 @@ const NoAuthenticationModal = ({ isHidden, form, setForm, setIsNoAuthHidden, han
>
<Option value="Select Auth" disabled>Select Auth</Option>
<Option value="password">Password</Option>
<Option value="rsaKey">Public Key</Option>
<Option value="sshKey">SSH Key</Option >
</Select>
</FormControl>
@@ -102,9 +176,9 @@ const NoAuthenticationModal = ({ isHidden, form, setForm, setIsNoAuthHidden, han
<FormLabel>Password</FormLabel>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Input
type={showPassword ? 'text' : 'password'}
type={showPassword ? "text" : "password"}
value={form.password || ''}
onChange={(e) => 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,
@@ -115,7 +189,10 @@ const NoAuthenticationModal = ({ isHidden, form, setForm, setIsNoAuthHidden, han
onClick={() => setShowPassword(!showPassword)}
sx={{
color: theme.palette.text.primary,
marginLeft: 1
marginLeft: 1,
'&:disabled': {
opacity: 0.5,
},
}}
>
{showPassword ? <VisibilityOff /> : <Visibility />}
@@ -124,41 +201,34 @@ const NoAuthenticationModal = ({ isHidden, form, setForm, setIsNoAuthHidden, han
</FormControl>
)}
{form.authMethod === 'rsaKey' && (
<FormControl error={!form.rsaKey}>
<FormLabel>Public Key</FormLabel>
<Button
component="label"
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '40px',
'&:hover': {
backgroundColor: theme.palette.general.disabled,
},
}}
>
{form.rsaKey ? 'Change Public Key File' : 'Upload Public Key File'}
<Input
type="file"
onChange={(e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
setForm({ ...form, rsaKey: event.target.result });
};
reader.readAsText(file);
}
{form.authMethod === 'sshKey' && (
<Stack spacing={2}>
<FormControl error={!form.sshKey}>
<FormLabel>SSH Key</FormLabel>
<Button
component="label"
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '40px',
'&:hover': {
backgroundColor: theme.palette.general.disabled,
},
}}
sx={{ display: 'none' }}
/>
</Button>
</FormControl>
>
{form.sshKey ? `Change ${form.keyType || 'SSH'} Key File` : 'Upload SSH Key File'}
<Input
type="file"
onChange={handleFileChange}
sx={{ display: 'none' }}
/>
</Button>
</FormControl>
</Stack>
)}
<Button

View File

@@ -71,6 +71,10 @@ export default function ProfileModal({
>
Delete Account
</Button>
<div className="text-center text-xs text-gray-400">
v0.2.1
</div>
</div>
</div>
</Modal>

5
src/other/eventBus.jsx Normal file
View File

@@ -0,0 +1,5 @@
import mitt from "mitt";
const eventBus = mitt();
export default eventBus;