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:
70
.github/workflows/docker-image.yml
vendored
70
.github/workflows/docker-image.yml
vendored
@@ -4,6 +4,9 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- development
|
- development
|
||||||
|
paths-ignore:
|
||||||
|
- '**.md'
|
||||||
|
- '.gitignore'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
tag_name:
|
tag_name:
|
||||||
@@ -16,27 +19,44 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v2
|
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Install Dependencies and Build Frontend
|
- name: Set up QEMU
|
||||||
run: |
|
uses: docker/setup-qemu-action@v3
|
||||||
cd src
|
with:
|
||||||
npm ci
|
platforms: arm64
|
||||||
npm run build
|
|
||||||
|
|
||||||
- name: Setup QEMU
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
with:
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
driver-opts: |
|
||||||
|
image=moby/buildkit:master
|
||||||
|
network=host
|
||||||
|
|
||||||
- name: Setup Docker Buildx
|
- name: Cache npm dependencies
|
||||||
uses: docker/setup-buildx-action@v2
|
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
|
- name: Login to Docker Registry
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -53,21 +73,36 @@ jobs:
|
|||||||
echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
|
echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Build and Push Multi-Arch Docker Image
|
- name: Build and Push Multi-Arch Docker Image
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./docker/Dockerfile
|
file: ./docker/Dockerfile
|
||||||
push: true
|
push: true
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
tags: ghcr.io/${{ env.REPO_OWNER }}/termix:${{ env.IMAGE_TAG }}
|
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
|
- name: Notify via ntfy
|
||||||
|
if: success()
|
||||||
run: |
|
run: |
|
||||||
curl -d "Docker image build and push completed successfully for tag: ${{ env.IMAGE_TAG }}" \
|
curl -d "Docker image build and push completed successfully for tag: ${{ env.IMAGE_TAG }}" \
|
||||||
https://ntfy.karmaa.site/termix-build
|
https://ntfy.karmaa.site/termix-build
|
||||||
|
|
||||||
- name: Delete all untagged image versions
|
- name: Delete all untagged image versions
|
||||||
|
if: success()
|
||||||
uses: quartx-analytics/ghcr-cleaner@v1
|
uses: quartx-analytics/ghcr-cleaner@v1
|
||||||
with:
|
with:
|
||||||
owner-type: user
|
owner-type: user
|
||||||
@@ -76,6 +111,7 @@ jobs:
|
|||||||
delete-untagged: true
|
delete-untagged: true
|
||||||
|
|
||||||
- name: Cleanup Docker Images Locally
|
- name: Cleanup Docker Images Locally
|
||||||
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
docker image prune -af
|
docker image prune -af
|
||||||
docker system prune -af --volumes
|
docker system prune -af --volumes
|
||||||
@@ -1,61 +1,107 @@
|
|||||||
# Stage 1: Build frontend
|
# Stage 1: Build frontend
|
||||||
FROM --platform=$BUILDPLATFORM node:18 AS frontend-builder
|
FROM node:18-alpine AS frontend-builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm install
|
RUN npm ci --force && \
|
||||||
|
npm cache clean --force
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Stage 2: Build backend
|
# Stage 2: Build backend
|
||||||
FROM --platform=$BUILDPLATFORM node:18 AS backend-builder
|
FROM node:18-alpine AS backend-builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm install
|
RUN npm ci --only=production --force && \
|
||||||
|
npm cache clean --force
|
||||||
COPY src/backend/ ./src/backend/
|
COPY src/backend/ ./src/backend/
|
||||||
|
|
||||||
# Stage 3: Final production image
|
# Stage 3: Build bcrypt for Ubuntu
|
||||||
FROM mongo:5
|
FROM ubuntu:focal AS bcrypt-builder
|
||||||
# Install Node.js
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
RUN apt-get update && apt-get install -y \
|
NODE_VERSION=18.x
|
||||||
curl \
|
WORKDIR /app
|
||||||
nginx \
|
COPY package*.json ./
|
||||||
python3 \
|
RUN rm -f /var/lib/apt/lists/lock /var/cache/apt/archives/lock /var/lib/dpkg/lock* && \
|
||||||
build-essential \
|
apt-get clean && \
|
||||||
&& curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
apt-get update && \
|
||||||
&& apt-get install -y nodejs \
|
apt-get install -y --no-install-recommends \
|
||||||
&& apt-get clean \
|
python3 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
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 docker/nginx.conf /etc/nginx/nginx.conf
|
||||||
COPY --from=frontend-builder /app/dist /usr/share/nginx/html
|
COPY --from=frontend-builder /app/dist /usr/share/nginx/html
|
||||||
|
RUN chown -R www-data:www-data /usr/share/nginx/html
|
||||||
|
|
||||||
# Setup backend
|
# Setup backend
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package*.json ./
|
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
|
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"]
|
VOLUME ["/data/db"]
|
||||||
|
|
||||||
# Expose ports
|
|
||||||
EXPOSE 8080 8081 8082 27017
|
EXPOSE 8080 8081 8082 27017
|
||||||
|
|
||||||
# Use a entrypoint script to run all services
|
|
||||||
COPY docker/entrypoint.sh /entrypoint.sh
|
COPY docker/entrypoint.sh /entrypoint.sh
|
||||||
RUN chmod +x /entrypoint.sh
|
RUN chmod +x /entrypoint.sh
|
||||||
CMD ["/entrypoint.sh"]
|
CMD ["/entrypoint.sh"]
|
||||||
@@ -1,32 +1,124 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Start MongoDB
|
# Create required directories and set permissions
|
||||||
echo "Starting MongoDB..."
|
mkdir -p /data/db /var/log/mongodb /var/run/mongodb
|
||||||
mongod --fork --dbpath $MONGODB_DATA_DIR --logpath $MONGODB_LOG_DIR/mongodb.log
|
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
|
# Wait for MongoDB to be ready
|
||||||
echo "Waiting for MongoDB to start..."
|
echo "Waiting for MongoDB to start..."
|
||||||
until mongosh --eval "print(\"waited for connection\")" > /dev/null 2>&1; do
|
MAX_TRIES=30
|
||||||
sleep 0.5
|
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
|
done
|
||||||
echo "MongoDB has started"
|
echo "MongoDB started successfully"
|
||||||
|
|
||||||
# Start nginx
|
# Start nginx
|
||||||
echo "Starting nginx..."
|
echo "Starting nginx..."
|
||||||
nginx
|
nginx
|
||||||
|
|
||||||
# Change to app directory
|
# Start backend services
|
||||||
|
echo "Starting backend services..."
|
||||||
cd /app
|
cd /app
|
||||||
|
export NODE_ENV=production
|
||||||
|
|
||||||
# Start the SSH service
|
# Start SSH service
|
||||||
echo "Starting SSH service..."
|
su -s /bin/bash node -c "node src/backend/ssh.cjs" &
|
||||||
node src/backend/ssh.cjs &
|
|
||||||
|
|
||||||
# Start the database service
|
# Start database service
|
||||||
echo "Starting database service..."
|
su -s /bin/bash node -c "node src/backend/database.cjs" &
|
||||||
node src/backend/database.cjs &
|
|
||||||
|
|
||||||
# Keep the container running and show MongoDB logs
|
echo "All services started"
|
||||||
echo "All services started. Tailing MongoDB logs..."
|
|
||||||
|
# Keep container running and show logs
|
||||||
tail -f $MONGODB_LOG_DIR/mongodb.log
|
tail -f $MONGODB_LOG_DIR/mongodb.log
|
||||||
241
package-lock.json
generated
241
package-lock.json
generated
@@ -13,7 +13,7 @@
|
|||||||
"@fontsource/inter": "^5.1.1",
|
"@fontsource/inter": "^5.1.1",
|
||||||
"@mui/icons-material": "^6.4.7",
|
"@mui/icons-material": "^6.4.7",
|
||||||
"@mui/joy": "^5.0.0-beta.51",
|
"@mui/joy": "^5.0.0-beta.51",
|
||||||
"@tailwindcss/vite": "^4.0.8",
|
"@tailwindcss/vite": "^4.0.15",
|
||||||
"@tiptap/extension-link": "^2.11.5",
|
"@tiptap/extension-link": "^2.11.5",
|
||||||
"@tiptap/pm": "^2.11.5",
|
"@tiptap/pm": "^2.11.5",
|
||||||
"@tiptap/react": "^2.11.5",
|
"@tiptap/react": "^2.11.5",
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"is-stream": "^4.0.1",
|
"is-stream": "^4.0.1",
|
||||||
"make-dir": "^5.0.0",
|
"make-dir": "^5.0.0",
|
||||||
|
"mitt": "^3.0.1",
|
||||||
"mongoose": "^8.12.1",
|
"mongoose": "^8.12.1",
|
||||||
"node-ssh": "^13.2.0",
|
"node-ssh": "^13.2.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
@@ -41,7 +42,7 @@
|
|||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"ssh2": "^1.16.0",
|
"ssh2": "^1.16.0",
|
||||||
"tailwindcss": "^4.0.8"
|
"tailwindcss": "^4.0.15"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.17.0",
|
"@eslint/js": "^9.17.0",
|
||||||
@@ -1225,15 +1226,6 @@
|
|||||||
"node-pre-gyp": "bin/node-pre-gyp"
|
"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": {
|
"node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||||
@@ -2011,42 +2003,42 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/node": {
|
"node_modules/@tailwindcss/node": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.15.tgz",
|
||||||
"integrity": "sha512-FKArQpbrbwv08TNT0k7ejYXpF+R8knZFAatNc0acOxbgeqLzwb86r+P3LGOjIeI3Idqe9CVkZrh4GlsJLJKkkw==",
|
"integrity": "sha512-IODaJjNmiasfZX3IoS+4Em3iu0fD2HS0/tgrnkYfW4hyUor01Smnr5eY3jc4rRgaTDrJlDmBTHbFO0ETTDaxWA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"enhanced-resolve": "^5.18.1",
|
"enhanced-resolve": "^5.18.1",
|
||||||
"jiti": "^2.4.2",
|
"jiti": "^2.4.2",
|
||||||
"tailwindcss": "4.0.8"
|
"tailwindcss": "4.0.15"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide": {
|
"node_modules/@tailwindcss/oxide": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.0.15.tgz",
|
||||||
"integrity": "sha512-KfMcuAu/Iw+DcV1e8twrFyr2yN8/ZDC/odIGta4wuuJOGkrkHZbvJvRNIbQNhGh7erZTYV6Ie0IeD6WC9Y8Hcw==",
|
"integrity": "sha512-e0uHrKfPu7JJGMfjwVNyt5M0u+OP8kUmhACwIRlM+JNBuReDVQ63yAD1NWe5DwJtdaHjugNBil76j+ks3zlk6g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@tailwindcss/oxide-android-arm64": "4.0.8",
|
"@tailwindcss/oxide-android-arm64": "4.0.15",
|
||||||
"@tailwindcss/oxide-darwin-arm64": "4.0.8",
|
"@tailwindcss/oxide-darwin-arm64": "4.0.15",
|
||||||
"@tailwindcss/oxide-darwin-x64": "4.0.8",
|
"@tailwindcss/oxide-darwin-x64": "4.0.15",
|
||||||
"@tailwindcss/oxide-freebsd-x64": "4.0.8",
|
"@tailwindcss/oxide-freebsd-x64": "4.0.15",
|
||||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.8",
|
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.15",
|
||||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.0.8",
|
"@tailwindcss/oxide-linux-arm64-gnu": "4.0.15",
|
||||||
"@tailwindcss/oxide-linux-arm64-musl": "4.0.8",
|
"@tailwindcss/oxide-linux-arm64-musl": "4.0.15",
|
||||||
"@tailwindcss/oxide-linux-x64-gnu": "4.0.8",
|
"@tailwindcss/oxide-linux-x64-gnu": "4.0.15",
|
||||||
"@tailwindcss/oxide-linux-x64-musl": "4.0.8",
|
"@tailwindcss/oxide-linux-x64-musl": "4.0.15",
|
||||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.0.8",
|
"@tailwindcss/oxide-win32-arm64-msvc": "4.0.15",
|
||||||
"@tailwindcss/oxide-win32-x64-msvc": "4.0.8"
|
"@tailwindcss/oxide-win32-x64-msvc": "4.0.15"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-android-arm64": {
|
"node_modules/@tailwindcss/oxide-android-arm64": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.15.tgz",
|
||||||
"integrity": "sha512-We7K79+Sm4mwJHk26Yzu/GAj7C7myemm7PeXvpgMxyxO70SSFSL3uCcqFbz9JA5M5UPkrl7N9fkBe/Y0iazqpA==",
|
"integrity": "sha512-EBuyfSKkom7N+CB3A+7c0m4+qzKuiN0WCvzPvj5ZoRu4NlQadg/mthc1tl5k9b5ffRGsbDvP4k21azU4VwVk3Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2060,9 +2052,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.15.tgz",
|
||||||
"integrity": "sha512-Lv9Isi2EwkCTG1sRHNDi0uRNN1UGFdEThUAGFrydRmQZnraGLMjN8gahzg2FFnOizDl7LB2TykLUuiw833DSNg==",
|
"integrity": "sha512-ObVAnEpLepMhV9VoO0JSit66jiN5C4YCqW3TflsE9boo2Z7FIjV80RFbgeL2opBhtxbaNEDa6D0/hq/EP03kgQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2076,9 +2068,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.15.tgz",
|
||||||
"integrity": "sha512-fWfywfYIlSWtKoqWTjukTHLWV3ARaBRjXCC2Eo0l6KVpaqGY4c2y8snUjp1xpxUtpqwMvCvFWFaleMoz1Vhzlw==",
|
"integrity": "sha512-IElwoFhUinOr9MyKmGTPNi1Rwdh68JReFgYWibPWTGuevkHkLWKEflZc2jtI5lWZ5U9JjUnUfnY43I4fEXrc4g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2092,9 +2084,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.15.tgz",
|
||||||
"integrity": "sha512-SO+dyvjJV9G94bnmq2288Ke0BIdvrbSbvtPLaQdqjqHR83v5L2fWADyFO+1oecHo9Owsk8MxcXh1agGVPIKIqw==",
|
"integrity": "sha512-6BLLqyx7SIYRBOnTZ8wgfXANLJV5TQd3PevRJZp0vn42eO58A2LykRKdvL1qyPfdpmEVtF+uVOEZ4QTMqDRAWA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2108,9 +2100,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.15.tgz",
|
||||||
"integrity": "sha512-ZSHggWiEblQNV69V0qUK5vuAtHP+I+S2eGrKGJ5lPgwgJeAd6GjLsVBN+Mqn2SPVfYM3BOpS9jX/zVg9RWQVDQ==",
|
"integrity": "sha512-Zy63EVqO9241Pfg6G0IlRIWyY5vNcWrL5dd2WAKVJZRQVeolXEf1KfjkyeAAlErDj72cnyXObEZjMoPEKHpdNw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -2124,9 +2116,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.15.tgz",
|
||||||
"integrity": "sha512-xWpr6M0OZLDNsr7+bQz+3X7zcnDJZJ1N9gtBWCtfhkEtDjjxYEp+Lr5L5nc/yXlL4MyCHnn0uonGVXy3fhxaVA==",
|
"integrity": "sha512-2NemGQeaTbtIp1Z2wyerbVEJZTkAWhMDOhhR5z/zJ75yMNf8yLnE+sAlyf6yGDNr+1RqvWrRhhCFt7i0CIxe4Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2140,9 +2132,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.15.tgz",
|
||||||
"integrity": "sha512-5tz2IL7LN58ssGEq7h/staD7pu/izF/KeMWdlJ86WDe2Ah46LF3ET6ZGKTr5eZMrnEA0M9cVFuSPprKRHNgjeg==",
|
"integrity": "sha512-342GVnhH/6PkVgKtEzvNVuQ4D+Q7B7qplvuH20Cfz9qEtydG6IQczTZ5IT4JPlh931MG1NUCVxg+CIorr1WJyw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2156,9 +2148,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.15.tgz",
|
||||||
"integrity": "sha512-KSzMkhyrxAQyY2o194NKVKU9j/c+NFSoMvnHWFaNHKi3P1lb+Vq1UC19tLHrmxSkKapcMMu69D7+G1+FVGNDXQ==",
|
"integrity": "sha512-g76GxlKH124RuGqacCEFc2nbzRl7bBrlC8qDQMiUABkiifDRHOIUjgKbLNG4RuR9hQAD/MKsqZ7A8L08zsoBrw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2172,9 +2164,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.15.tgz",
|
||||||
"integrity": "sha512-yFYKG5UtHTRimjtqxUWXBgI4Tc6NJe3USjRIVdlTczpLRxq/SFwgzGl5JbatCxgSRDPBFwRrNPxq+ukfQFGdrw==",
|
"integrity": "sha512-Gg/Y1XrKEvKpq6WeNt2h8rMIKOBj/W3mNa5NMvkQgMC7iO0+UNLrYmt6zgZufht66HozNpn+tJMbbkZ5a3LczA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2188,9 +2180,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.15.tgz",
|
||||||
"integrity": "sha512-tndGujmCSba85cRCnQzXgpA2jx5gXimyspsUYae5jlPyLRG0RjXbDshFKOheVXU4TLflo7FSG8EHCBJ0EHTKdQ==",
|
"integrity": "sha512-7QtSSJwYZ7ZK1phVgcNZpuf7c7gaCj8Wb0xjliligT5qCGCp79OV2n3SJummVZdw4fbTNKUOYMO7m1GinppZyA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2204,9 +2196,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.15.tgz",
|
||||||
"integrity": "sha512-T77jroAc0p4EHVVgTUiNeFn6Nj3jtD3IeNId2X+0k+N1XxfNipy81BEkYErpKLiOkNhpNFjPee8/ZVas29b2OQ==",
|
"integrity": "sha512-JQ5H+5MLhOjpgNp6KomouE0ZuKmk3hO5h7/ClMNAQ8gZI2zkli3IH8ZqLbd2DVfXDbdxN2xvooIEeIlkIoSCqw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2220,15 +2212,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/vite": {
|
"node_modules/@tailwindcss/vite": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.0.15.tgz",
|
||||||
"integrity": "sha512-+SAq44yLzYlzyrb7QTcFCdU8Xa7FOA0jp+Xby7fPMUie+MY9HhJysM7Vp+vL8qIp8ceQJfLD+FjgJuJ4lL6nyg==",
|
"integrity": "sha512-JRexava80NijI8cTcLXNM3nQL5A0ptTHI8oJLLe8z1MpNB6p5J4WCdJJP8RoyHu8/eB1JzEdbpH86eGfbuaezQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/node": "4.0.8",
|
"@tailwindcss/node": "4.0.15",
|
||||||
"@tailwindcss/oxide": "4.0.8",
|
"@tailwindcss/oxide": "4.0.15",
|
||||||
"lightningcss": "^1.29.1",
|
"lightningcss": "1.29.2",
|
||||||
"tailwindcss": "4.0.8"
|
"tailwindcss": "4.0.15"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"vite": "^5.2.0 || ^6"
|
"vite": "^5.2.0 || ^6"
|
||||||
@@ -3932,15 +3924,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "1.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
|
||||||
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
|
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
|
||||||
"detect-libc": "bin/detect-libc.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/doctrine": {
|
"node_modules/doctrine": {
|
||||||
@@ -5880,12 +5869,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss": {
|
"node_modules/lightningcss": {
|
||||||
"version": "1.29.1",
|
"version": "1.29.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz",
|
||||||
"integrity": "sha512-FmGoeD4S05ewj+AkhTY+D+myDvXI6eL27FjHIjoyUkO/uw7WZD1fBVs0QxeYWa7E17CUHJaYX/RUGISCtcrG4Q==",
|
"integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"detect-libc": "^1.0.3"
|
"detect-libc": "^2.0.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
@@ -5895,22 +5884,22 @@
|
|||||||
"url": "https://opencollective.com/parcel"
|
"url": "https://opencollective.com/parcel"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"lightningcss-darwin-arm64": "1.29.1",
|
"lightningcss-darwin-arm64": "1.29.2",
|
||||||
"lightningcss-darwin-x64": "1.29.1",
|
"lightningcss-darwin-x64": "1.29.2",
|
||||||
"lightningcss-freebsd-x64": "1.29.1",
|
"lightningcss-freebsd-x64": "1.29.2",
|
||||||
"lightningcss-linux-arm-gnueabihf": "1.29.1",
|
"lightningcss-linux-arm-gnueabihf": "1.29.2",
|
||||||
"lightningcss-linux-arm64-gnu": "1.29.1",
|
"lightningcss-linux-arm64-gnu": "1.29.2",
|
||||||
"lightningcss-linux-arm64-musl": "1.29.1",
|
"lightningcss-linux-arm64-musl": "1.29.2",
|
||||||
"lightningcss-linux-x64-gnu": "1.29.1",
|
"lightningcss-linux-x64-gnu": "1.29.2",
|
||||||
"lightningcss-linux-x64-musl": "1.29.1",
|
"lightningcss-linux-x64-musl": "1.29.2",
|
||||||
"lightningcss-win32-arm64-msvc": "1.29.1",
|
"lightningcss-win32-arm64-msvc": "1.29.2",
|
||||||
"lightningcss-win32-x64-msvc": "1.29.1"
|
"lightningcss-win32-x64-msvc": "1.29.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-darwin-arm64": {
|
"node_modules/lightningcss-darwin-arm64": {
|
||||||
"version": "1.29.1",
|
"version": "1.29.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz",
|
||||||
"integrity": "sha512-HtR5XJ5A0lvCqYAoSv2QdZZyoHNttBpa5EP9aNuzBQeKGfbyH5+UipLWvVzpP4Uml5ej4BYs5I9Lco9u1fECqw==",
|
"integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -5928,9 +5917,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-darwin-x64": {
|
"node_modules/lightningcss-darwin-x64": {
|
||||||
"version": "1.29.1",
|
"version": "1.29.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz",
|
||||||
"integrity": "sha512-k33G9IzKUpHy/J/3+9MCO4e+PzaFblsgBjSGlpAaFikeBFm8B/CkO3cKU9oI4g+fjS2KlkLM/Bza9K/aw8wsNA==",
|
"integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -5948,9 +5937,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-freebsd-x64": {
|
"node_modules/lightningcss-freebsd-x64": {
|
||||||
"version": "1.29.1",
|
"version": "1.29.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz",
|
||||||
"integrity": "sha512-0SUW22fv/8kln2LnIdOCmSuXnxgxVC276W5KLTwoehiO0hxkacBxjHOL5EtHD8BAXg2BvuhsJPmVMasvby3LiQ==",
|
"integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -5968,9 +5957,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||||
"version": "1.29.1",
|
"version": "1.29.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz",
|
||||||
"integrity": "sha512-sD32pFvlR0kDlqsOZmYqH/68SqUMPNj+0pucGxToXZi4XZgZmqeX/NkxNKCPsswAXU3UeYgDSpGhu05eAufjDg==",
|
"integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -5988,9 +5977,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-arm64-gnu": {
|
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||||
"version": "1.29.1",
|
"version": "1.29.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz",
|
||||||
"integrity": "sha512-0+vClRIZ6mmJl/dxGuRsE197o1HDEeeRk6nzycSy2GofC2JsY4ifCRnvUWf/CUBQmlrvMzt6SMQNMSEu22csWQ==",
|
"integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -6008,9 +5997,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-arm64-musl": {
|
"node_modules/lightningcss-linux-arm64-musl": {
|
||||||
"version": "1.29.1",
|
"version": "1.29.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz",
|
||||||
"integrity": "sha512-UKMFrG4rL/uHNgelBsDwJcBqVpzNJbzsKkbI3Ja5fg00sgQnHw/VrzUTEc4jhZ+AN2BvQYz/tkHu4vt1kLuJyw==",
|
"integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -6028,9 +6017,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-x64-gnu": {
|
"node_modules/lightningcss-linux-x64-gnu": {
|
||||||
"version": "1.29.1",
|
"version": "1.29.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz",
|
||||||
"integrity": "sha512-u1S+xdODy/eEtjADqirA774y3jLcm8RPtYztwReEXoZKdzgsHYPl0s5V52Tst+GKzqjebkULT86XMSxejzfISw==",
|
"integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -6048,9 +6037,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-x64-musl": {
|
"node_modules/lightningcss-linux-x64-musl": {
|
||||||
"version": "1.29.1",
|
"version": "1.29.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz",
|
||||||
"integrity": "sha512-L0Tx0DtaNUTzXv0lbGCLB/c/qEADanHbu4QdcNOXLIe1i8i22rZRpbT3gpWYsCh9aSL9zFujY/WmEXIatWvXbw==",
|
"integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -6068,9 +6057,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-win32-arm64-msvc": {
|
"node_modules/lightningcss-win32-arm64-msvc": {
|
||||||
"version": "1.29.1",
|
"version": "1.29.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz",
|
||||||
"integrity": "sha512-QoOVnkIEFfbW4xPi+dpdft/zAKmgLgsRHfJalEPYuJDOWf7cLQzYg0DEh8/sn737FaeMJxHZRc1oBreiwZCjog==",
|
"integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -6088,9 +6077,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-win32-x64-msvc": {
|
"node_modules/lightningcss-win32-x64-msvc": {
|
||||||
"version": "1.29.1",
|
"version": "1.29.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.2.tgz",
|
||||||
"integrity": "sha512-NygcbThNBe4JElP+olyTI/doBNGJvLs3bFCRPdvuCcxZCcCZ71B858IHpdm7L1btZex0FvCmM17FK98Y9MRy1Q==",
|
"integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -6341,6 +6330,12 @@
|
|||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/mkdirp": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||||
@@ -8218,9 +8213,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.15.tgz",
|
||||||
"integrity": "sha512-Me7N5CKR+D2A1xdWA5t5+kjjT7bwnxZOE6/yDI/ixJdJokszsn2n++mdU5yJwrsTpqFX2B9ZNMBJDwcqk9C9lw==",
|
"integrity": "sha512-6ZMg+hHdMJpjpeCCFasX7K+U615U9D+7k5/cDK/iRwl6GptF24+I/AbKgOnXhVKePzrEyIXutLv36n4cRsq3Sg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/tapable": {
|
"node_modules/tapable": {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
"@fontsource/inter": "^5.1.1",
|
"@fontsource/inter": "^5.1.1",
|
||||||
"@mui/icons-material": "^6.4.7",
|
"@mui/icons-material": "^6.4.7",
|
||||||
"@mui/joy": "^5.0.0-beta.51",
|
"@mui/joy": "^5.0.0-beta.51",
|
||||||
"@tailwindcss/vite": "^4.0.8",
|
"@tailwindcss/vite": "^4.0.15",
|
||||||
"@tiptap/extension-link": "^2.11.5",
|
"@tiptap/extension-link": "^2.11.5",
|
||||||
"@tiptap/pm": "^2.11.5",
|
"@tiptap/pm": "^2.11.5",
|
||||||
"@tiptap/react": "^2.11.5",
|
"@tiptap/react": "^2.11.5",
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"is-stream": "^4.0.1",
|
"is-stream": "^4.0.1",
|
||||||
"make-dir": "^5.0.0",
|
"make-dir": "^5.0.0",
|
||||||
|
"mitt": "^3.0.1",
|
||||||
"mongoose": "^8.12.1",
|
"mongoose": "^8.12.1",
|
||||||
"node-ssh": "^13.2.0",
|
"node-ssh": "^13.2.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
@@ -43,7 +44,7 @@
|
|||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"ssh2": "^1.16.0",
|
"ssh2": "^1.16.0",
|
||||||
"tailwindcss": "^4.0.8"
|
"tailwindcss": "^4.0.15"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.17.0",
|
"@eslint/js": "^9.17.0",
|
||||||
|
|||||||
253
src/App.jsx
253
src/App.jsx
@@ -2,7 +2,7 @@ import { useState, useEffect, useRef } from "react";
|
|||||||
import { NewTerminal } from "./apps/ssh/Terminal.jsx";
|
import { NewTerminal } from "./apps/ssh/Terminal.jsx";
|
||||||
import { User } from "./apps/user/User.jsx";
|
import { User } from "./apps/user/User.jsx";
|
||||||
import AddHostModal from "./modals/AddHostModal.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 { Button } from "@mui/joy";
|
||||||
import { CssVarsProvider } from "@mui/joy";
|
import { CssVarsProvider } from "@mui/joy";
|
||||||
import theme from "./theme";
|
import theme from "./theme";
|
||||||
@@ -12,16 +12,15 @@ import { Debounce } from './other/Utils.jsx';
|
|||||||
import TermixIcon from "./images/termix_icon.png";
|
import TermixIcon from "./images/termix_icon.png";
|
||||||
import RocketIcon from './images/launchpad_rocket.png';
|
import RocketIcon from './images/launchpad_rocket.png';
|
||||||
import ProfileIcon from './images/profile_icon.png';
|
import ProfileIcon from './images/profile_icon.png';
|
||||||
import CreateUserModal from "./modals/CreateUserModal.jsx";
|
|
||||||
import ProfileModal from "./modals/ProfileModal.jsx";
|
import ProfileModal from "./modals/ProfileModal.jsx";
|
||||||
import ErrorModal from "./modals/ErrorModal.jsx";
|
import ErrorModal from "./modals/ErrorModal.jsx";
|
||||||
import EditHostModal from "./modals/EditHostModal.jsx";
|
import EditHostModal from "./modals/EditHostModal.jsx";
|
||||||
import NoAuthenticationModal from "./modals/NoAuthenticationModal.jsx";
|
import NoAuthenticationModal from "./modals/NoAuthenticationModal.jsx";
|
||||||
|
import eventBus from "./other/eventBus.jsx";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [isAddHostHidden, setIsAddHostHidden] = useState(true);
|
const [isAddHostHidden, setIsAddHostHidden] = useState(true);
|
||||||
const [isLoginUserHidden, setIsLoginUserHidden] = useState(true);
|
const [isAuthModalHidden, setIsAuthModalHidden] = useState(true);
|
||||||
const [isCreateUserHidden, setIsCreateUserHidden] = useState(true);
|
|
||||||
const [isProfileHidden, setIsProfileHidden] = useState(true);
|
const [isProfileHidden, setIsProfileHidden] = useState(true);
|
||||||
const [isErrorHidden, setIsErrorHidden] = useState(true);
|
const [isErrorHidden, setIsErrorHidden] = useState(true);
|
||||||
const [errorMessage, setErrorMessage] = useState('');
|
const [errorMessage, setErrorMessage] = useState('');
|
||||||
@@ -35,10 +34,17 @@ function App() {
|
|||||||
ip: "",
|
ip: "",
|
||||||
user: "",
|
user: "",
|
||||||
password: "",
|
password: "",
|
||||||
|
sshKey: "",
|
||||||
port: 22,
|
port: 22,
|
||||||
authMethod: "Select Auth",
|
authMethod: "Select Auth",
|
||||||
rememberHost: false,
|
rememberHost: true,
|
||||||
storePassword: true,
|
storePassword: true,
|
||||||
|
connectionType: "ssh",
|
||||||
|
rdpDomain: "",
|
||||||
|
rdpWindowsAuthentication: true,
|
||||||
|
rdpConsole: false,
|
||||||
|
vncScaling: "100%",
|
||||||
|
vncQuality: "High"
|
||||||
});
|
});
|
||||||
const [editHostForm, setEditHostForm] = useState({
|
const [editHostForm, setEditHostForm] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
@@ -46,6 +52,7 @@ function App() {
|
|||||||
ip: "",
|
ip: "",
|
||||||
user: "",
|
user: "",
|
||||||
password: "",
|
password: "",
|
||||||
|
sshKey: "",
|
||||||
port: 22,
|
port: 22,
|
||||||
authMethod: "Select Auth",
|
authMethod: "Select Auth",
|
||||||
rememberHost: true,
|
rememberHost: true,
|
||||||
@@ -53,23 +60,23 @@ function App() {
|
|||||||
});
|
});
|
||||||
const [isNoAuthHidden, setIsNoAuthHidden] = useState(true);
|
const [isNoAuthHidden, setIsNoAuthHidden] = useState(true);
|
||||||
const [authForm, setAuthForm] = useState({
|
const [authForm, setAuthForm] = useState({
|
||||||
password: "",
|
username: '',
|
||||||
rsaKey: "",
|
password: '',
|
||||||
});
|
confirmPassword: ''
|
||||||
const [loginUserForm, setLoginUserForm] = useState({
|
|
||||||
username: "",
|
|
||||||
password: "",
|
|
||||||
});
|
|
||||||
const [createUserForm, setCreateUserForm] = useState({
|
|
||||||
username: "",
|
|
||||||
password: "",
|
|
||||||
});
|
});
|
||||||
|
const [noAuthenticationForm, setNoAuthenticationForm] = useState({
|
||||||
|
authMethod: 'Select Auth',
|
||||||
|
password: '',
|
||||||
|
sshKey: '',
|
||||||
|
keyType: '',
|
||||||
|
})
|
||||||
const [isLaunchpadOpen, setIsLaunchpadOpen] = useState(false);
|
const [isLaunchpadOpen, setIsLaunchpadOpen] = useState(false);
|
||||||
const [splitTabIds, setSplitTabIds] = useState([]);
|
const [splitTabIds, setSplitTabIds] = useState([]);
|
||||||
const [isEditHostHidden, setIsEditHostHidden] = useState(true);
|
const [isEditHostHidden, setIsEditHostHidden] = useState(true);
|
||||||
const [currentHostConfig, setCurrentHostConfig] = useState(null);
|
const [currentHostConfig, setCurrentHostConfig] = useState(null);
|
||||||
const [isLoggingIn, setIsLoggingIn] = useState(true);
|
const [isLoggingIn, setIsLoggingIn] = useState(true);
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [isHostViewerMenuOpen, setIsHostViewerMenuOpen] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e) => {
|
const handleKeyDown = (e) => {
|
||||||
@@ -133,13 +140,13 @@ function App() {
|
|||||||
|
|
||||||
if (userRef.current?.getUser()) {
|
if (userRef.current?.getUser()) {
|
||||||
setIsLoggingIn(false);
|
setIsLoggingIn(false);
|
||||||
setIsLoginUserHidden(true);
|
setIsAuthModalHidden(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sessionToken) {
|
if (!sessionToken) {
|
||||||
setIsLoggingIn(false);
|
setIsLoggingIn(false);
|
||||||
setIsLoginUserHidden(false);
|
setIsAuthModalHidden(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,13 +154,13 @@ function App() {
|
|||||||
let loginAttempts = 0;
|
let loginAttempts = 0;
|
||||||
const maxAttempts = 50;
|
const maxAttempts = 50;
|
||||||
let attemptLoginInterval;
|
let attemptLoginInterval;
|
||||||
|
|
||||||
const loginTimeout = setTimeout(() => {
|
const loginTimeout = setTimeout(() => {
|
||||||
if (isComponentMounted) {
|
if (isComponentMounted) {
|
||||||
clearInterval(attemptLoginInterval);
|
clearInterval(attemptLoginInterval);
|
||||||
if (!userRef.current?.getUser()) {
|
if (!userRef.current?.getUser()) {
|
||||||
localStorage.removeItem('sessionToken');
|
localStorage.removeItem('sessionToken');
|
||||||
setIsLoginUserHidden(false);
|
setIsAuthModalHidden(false);
|
||||||
setIsLoggingIn(false);
|
setIsLoggingIn(false);
|
||||||
setErrorMessage('Login timed out. Please try again.');
|
setErrorMessage('Login timed out. Please try again.');
|
||||||
setIsErrorHidden(false);
|
setIsErrorHidden(false);
|
||||||
@@ -163,14 +170,14 @@ function App() {
|
|||||||
|
|
||||||
const attemptLogin = () => {
|
const attemptLogin = () => {
|
||||||
if (!isComponentMounted || isLoginInProgress) return;
|
if (!isComponentMounted || isLoginInProgress) return;
|
||||||
|
|
||||||
if (loginAttempts >= maxAttempts || userRef.current?.getUser()) {
|
if (loginAttempts >= maxAttempts || userRef.current?.getUser()) {
|
||||||
clearTimeout(loginTimeout);
|
clearTimeout(loginTimeout);
|
||||||
clearInterval(attemptLoginInterval);
|
clearInterval(attemptLoginInterval);
|
||||||
|
|
||||||
if (!userRef.current?.getUser()) {
|
if (!userRef.current?.getUser()) {
|
||||||
localStorage.removeItem('sessionToken');
|
localStorage.removeItem('sessionToken');
|
||||||
setIsLoginUserHidden(false);
|
setIsAuthModalHidden(false);
|
||||||
setIsLoggingIn(false);
|
setIsLoggingIn(false);
|
||||||
setErrorMessage('Login timed out. Please try again.');
|
setErrorMessage('Login timed out. Please try again.');
|
||||||
setIsErrorHidden(false);
|
setIsErrorHidden(false);
|
||||||
@@ -186,7 +193,7 @@ function App() {
|
|||||||
if (isComponentMounted) {
|
if (isComponentMounted) {
|
||||||
clearTimeout(loginTimeout);
|
clearTimeout(loginTimeout);
|
||||||
clearInterval(attemptLoginInterval);
|
clearInterval(attemptLoginInterval);
|
||||||
setIsLoginUserHidden(true);
|
setIsAuthModalHidden(true);
|
||||||
setIsLoggingIn(false);
|
setIsLoggingIn(false);
|
||||||
setIsErrorHidden(true);
|
setIsErrorHidden(true);
|
||||||
}
|
}
|
||||||
@@ -200,7 +207,7 @@ function App() {
|
|||||||
localStorage.removeItem('sessionToken');
|
localStorage.removeItem('sessionToken');
|
||||||
setErrorMessage(`Auto-login failed: ${error}`);
|
setErrorMessage(`Auto-login failed: ${error}`);
|
||||||
setIsErrorHidden(false);
|
setIsErrorHidden(false);
|
||||||
setIsLoginUserHidden(false);
|
setIsAuthModalHidden(false);
|
||||||
setIsLoggingIn(false);
|
setIsLoggingIn(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,34 +229,53 @@ function App() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleAddHost = () => {
|
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) {
|
if (!addHostForm.rememberHost) {
|
||||||
connectToHost();
|
connectToHost();
|
||||||
setIsAddHostHidden(true);
|
setIsAddHostHidden(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (addHostForm.authMethod === 'Select Auth') {
|
if (addHostForm.connectionType === 'ssh') {
|
||||||
alert("Please select an authentication method.");
|
if (addHostForm.authMethod === 'Select Auth') {
|
||||||
return;
|
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) {
|
else if (!addHostForm.password) {
|
||||||
setIsNoAuthHidden(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (addHostForm.authMethod === 'rsaKey' && !addHostForm.rsaKey) {
|
|
||||||
setIsNoAuthHidden(false);
|
setIsNoAuthHidden(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
connectToHost();
|
try {
|
||||||
if (!addHostForm.storePassword) {
|
connectToHost();
|
||||||
addHostForm.password = '';
|
if (!addHostForm.storePassword) {
|
||||||
|
addHostForm.password = '';
|
||||||
|
}
|
||||||
|
handleSaveHost();
|
||||||
|
setIsAddHostHidden(true);
|
||||||
|
} catch (error) {
|
||||||
|
setErrorMessage(error.message || "Failed to add host");
|
||||||
|
setIsErrorHidden(false);
|
||||||
}
|
}
|
||||||
handleSaveHost();
|
|
||||||
setIsAddHostHidden(true);
|
|
||||||
} else {
|
} 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,
|
user: addHostForm.user,
|
||||||
port: String(addHostForm.port),
|
port: String(addHostForm.port),
|
||||||
password: addHostForm.rememberHost && addHostForm.authMethod === 'password' ? addHostForm.password : undefined,
|
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 = {
|
const newTerminal = {
|
||||||
@@ -274,25 +300,42 @@ function App() {
|
|||||||
setActiveTab(nextId);
|
setActiveTab(nextId);
|
||||||
setNextId(nextId + 1);
|
setNextId(nextId + 1);
|
||||||
setIsAddHostHidden(true);
|
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 handleAuthSubmit = (form) => {
|
||||||
const updatedTerminals = terminals.map((terminal) => {
|
try {
|
||||||
if (terminal.id === activeTab) {
|
setIsNoAuthHidden(true);
|
||||||
return {
|
|
||||||
...terminal,
|
setTimeout(() => {
|
||||||
hostConfig: {
|
const updatedTerminals = terminals.map((terminal) => {
|
||||||
...terminal.hostConfig,
|
if (terminal.id === activeTab) {
|
||||||
password: form.password,
|
return {
|
||||||
rsaKey: form.rsaKey
|
...terminal,
|
||||||
|
hostConfig: {
|
||||||
|
...terminal.hostConfig,
|
||||||
|
password: form.authMethod === 'password' ? form.password : undefined,
|
||||||
|
sshKey: form.authMethod === 'sshKey' ? form.sshKey : undefined
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
return terminal;
|
||||||
}
|
});
|
||||||
return terminal;
|
|
||||||
});
|
setTerminals(updatedTerminals);
|
||||||
setTerminals(updatedTerminals);
|
|
||||||
setIsNoAuthHidden(true);
|
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) => {
|
const connectToHostWithConfig = (hostConfig) => {
|
||||||
@@ -311,7 +354,7 @@ function App() {
|
|||||||
user: hostConfig.user.trim(),
|
user: hostConfig.user.trim(),
|
||||||
port: hostConfig.port || '22',
|
port: hostConfig.port || '22',
|
||||||
password: hostConfig.password?.trim(),
|
password: hostConfig.password?.trim(),
|
||||||
rsaKey: hostConfig.rsaKey?.trim(),
|
sshKey: hostConfig.sshKey?.trim(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const newTerminal = {
|
const newTerminal = {
|
||||||
@@ -326,20 +369,30 @@ function App() {
|
|||||||
setIsLaunchpadOpen(false);
|
setIsLaunchpadOpen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSaveHost = () => {
|
const handleSaveHost = async () => {
|
||||||
let hostConfig = {
|
try {
|
||||||
name: addHostForm.name || addHostForm.ip,
|
let hostConfig = {
|
||||||
folder: addHostForm.folder,
|
name: addHostForm.name || addHostForm.ip,
|
||||||
ip: addHostForm.ip,
|
folder: addHostForm.folder,
|
||||||
user: addHostForm.user,
|
ip: addHostForm.ip,
|
||||||
password: addHostForm.authMethod === 'password' ? addHostForm.password : undefined,
|
user: addHostForm.user,
|
||||||
rsaKey: addHostForm.authMethod === 'rsaKey' ? addHostForm.rsaKey : undefined,
|
password: (addHostForm.authMethod === 'password' || addHostForm.connectionType === 'vnc' || addHostForm.connectionType === 'rdp') ? addHostForm.password : undefined,
|
||||||
port: String(addHostForm.port),
|
sshKey: addHostForm.connectionType === 'ssh' && addHostForm.authMethod === 'sshKey' ? addHostForm.sshKey : undefined,
|
||||||
}
|
port: String(addHostForm.port),
|
||||||
if (userRef.current) {
|
connectionType: addHostForm.connectionType,
|
||||||
userRef.current.saveHost({
|
rdpDomain: addHostForm.connectionType === 'rdp' ? addHostForm.rdpDomain : undefined,
|
||||||
hostConfig,
|
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({
|
userRef.current.loginUser({
|
||||||
sessionToken,
|
sessionToken,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setIsLoginUserHidden(true);
|
setIsAuthModalHidden(true);
|
||||||
setIsLoggingIn(false);
|
setIsLoggingIn(false);
|
||||||
if (onSuccess) onSuccess();
|
if (onSuccess) onSuccess();
|
||||||
},
|
},
|
||||||
onFailure: (error) => {
|
onFailure: (error) => {
|
||||||
localStorage.removeItem('sessionToken');
|
localStorage.removeItem('sessionToken');
|
||||||
setIsLoginUserHidden(false);
|
setIsAuthModalHidden(false);
|
||||||
setIsLoggingIn(false);
|
setIsLoggingIn(false);
|
||||||
if (onFailure) onFailure(error);
|
if (onFailure) onFailure(error);
|
||||||
},
|
},
|
||||||
@@ -365,12 +418,12 @@ function App() {
|
|||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setIsLoginUserHidden(true);
|
setIsAuthModalHidden(true);
|
||||||
setIsLoggingIn(false);
|
setIsLoggingIn(false);
|
||||||
if (onSuccess) onSuccess();
|
if (onSuccess) onSuccess();
|
||||||
},
|
},
|
||||||
onFailure: (error) => {
|
onFailure: (error) => {
|
||||||
setIsLoginUserHidden(false);
|
setIsAuthModalHidden(false);
|
||||||
setIsLoggingIn(false);
|
setIsLoggingIn(false);
|
||||||
if (onFailure) onFailure(error);
|
if (onFailure) onFailure(error);
|
||||||
},
|
},
|
||||||
@@ -446,7 +499,7 @@ function App() {
|
|||||||
if (newConfig) {
|
if (newConfig) {
|
||||||
if (isEditing) return;
|
if (isEditing) return;
|
||||||
setIsEditing(true);
|
setIsEditing(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await userRef.current.editHost({
|
await userRef.current.editHost({
|
||||||
oldHostConfig: oldConfig,
|
oldHostConfig: oldConfig,
|
||||||
@@ -454,9 +507,11 @@ function App() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||||
|
setIsEditHostHidden(true);
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
setIsEditHostHidden(true);
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -464,7 +519,7 @@ function App() {
|
|||||||
updateEditHostForm(oldConfig);
|
updateEditHostForm(oldConfig);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Edit failed:', error);
|
console.error('Edit failed:', error);
|
||||||
setErrorMessage(`Edit failed: ${error}`);
|
setErrorMessage(`Edit failed: ${error.message || error}`);
|
||||||
setIsErrorHidden(false);
|
setIsErrorHidden(false);
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
}
|
}
|
||||||
@@ -585,7 +640,7 @@ function App() {
|
|||||||
{/* Profile Button */}
|
{/* Profile Button */}
|
||||||
<Button
|
<Button
|
||||||
disabled={isLoggingIn}
|
disabled={isLoggingIn}
|
||||||
onClick={() => userRef.current?.getUser() ? setIsProfileHidden(false) : setIsLoginUserHidden(false)}
|
onClick={() => userRef.current?.getUser() ? setIsProfileHidden(false) : setIsAuthModalHidden(false)}
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: theme.palette.general.tertiary,
|
backgroundColor: theme.palette.general.tertiary,
|
||||||
"&:hover": { backgroundColor: theme.palette.general.secondary },
|
"&:hover": { backgroundColor: theme.palette.general.secondary },
|
||||||
@@ -633,6 +688,8 @@ function App() {
|
|||||||
hostConfig={terminal.hostConfig}
|
hostConfig={terminal.hostConfig}
|
||||||
isVisible={activeTab === terminal.id || splitTabIds.includes(terminal.id)}
|
isVisible={activeTab === terminal.id || splitTabIds.includes(terminal.id)}
|
||||||
setIsNoAuthHidden={setIsNoAuthHidden}
|
setIsNoAuthHidden={setIsNoAuthHidden}
|
||||||
|
setErrorMessage={setErrorMessage}
|
||||||
|
setIsErrorHidden={setIsErrorHidden}
|
||||||
ref={(ref) => {
|
ref={(ref) => {
|
||||||
terminal.terminalRef = ref;
|
terminal.terminalRef = ref;
|
||||||
}}
|
}}
|
||||||
@@ -649,8 +706,8 @@ function App() {
|
|||||||
)}
|
)}
|
||||||
<NoAuthenticationModal
|
<NoAuthenticationModal
|
||||||
isHidden={isNoAuthHidden}
|
isHidden={isNoAuthHidden}
|
||||||
form={authForm}
|
form={noAuthenticationForm}
|
||||||
setForm={setAuthForm}
|
setForm={setNoAuthenticationForm}
|
||||||
setIsNoAuthHidden={setIsNoAuthHidden}
|
setIsNoAuthHidden={setIsNoAuthHidden}
|
||||||
handleAuthSubmit={handleAuthSubmit}
|
handleAuthSubmit={handleAuthSubmit}
|
||||||
/>
|
/>
|
||||||
@@ -694,6 +751,8 @@ function App() {
|
|||||||
editHost={handleEditHost}
|
editHost={handleEditHost}
|
||||||
shareHost={(hostId, username) => userRef.current?.shareHost(hostId, username)}
|
shareHost={(hostId, username) => userRef.current?.shareHost(hostId, username)}
|
||||||
userRef={userRef}
|
userRef={userRef}
|
||||||
|
isHostViewerMenuOpen={isHostViewerMenuOpen}
|
||||||
|
setIsHostViewerMenuOpen={setIsHostViewerMenuOpen}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -705,40 +764,31 @@ function App() {
|
|||||||
setIsErrorHidden={setIsErrorHidden}
|
setIsErrorHidden={setIsErrorHidden}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LoginUserModal
|
<AuthModal
|
||||||
isHidden={isLoginUserHidden}
|
isHidden={isAuthModalHidden}
|
||||||
form={loginUserForm}
|
form={authForm}
|
||||||
setForm={setLoginUserForm}
|
setForm={setAuthForm}
|
||||||
handleLoginUser={handleLoginUser}
|
handleLoginUser={handleLoginUser}
|
||||||
handleGuestLogin={handleGuestLogin}
|
|
||||||
setIsLoginUserHidden={setIsLoginUserHidden}
|
|
||||||
setIsCreateUserHidden={setIsCreateUserHidden}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CreateUserModal
|
|
||||||
isHidden={isCreateUserHidden}
|
|
||||||
form={createUserForm}
|
|
||||||
setForm={setCreateUserForm}
|
|
||||||
handleCreateUser={handleCreateUser}
|
handleCreateUser={handleCreateUser}
|
||||||
setIsCreateUserHidden={setIsCreateUserHidden}
|
handleGuestLogin={handleGuestLogin}
|
||||||
setIsLoginUserHidden={setIsLoginUserHidden}
|
setIsAuthModalHidden={setIsAuthModalHidden}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* User component */}
|
{/* User component */}
|
||||||
<User
|
<User
|
||||||
ref={userRef}
|
ref={userRef}
|
||||||
onLoginSuccess={() => {
|
onLoginSuccess={() => {
|
||||||
setIsLoginUserHidden(true);
|
setIsAuthModalHidden(true);
|
||||||
setIsLoggingIn(false);
|
setIsLoggingIn(false);
|
||||||
setIsErrorHidden(true);
|
setIsErrorHidden(true);
|
||||||
}}
|
}}
|
||||||
onCreateSuccess={() => {
|
onCreateSuccess={() => {
|
||||||
setIsCreateUserHidden(true);
|
setIsAuthModalHidden(true);
|
||||||
handleLoginUser({
|
handleLoginUser({
|
||||||
username: createUserForm.username,
|
username: authForm.username,
|
||||||
password: createUserForm.password,
|
password: authForm.password,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setIsLoginUserHidden(true);
|
setIsAuthModalHidden(true);
|
||||||
setIsLoggingIn(false);
|
setIsLoggingIn(false);
|
||||||
setIsErrorHidden(true);
|
setIsErrorHidden(true);
|
||||||
},
|
},
|
||||||
@@ -756,6 +806,7 @@ function App() {
|
|||||||
setErrorMessage(`Action failed: ${error}`);
|
setErrorMessage(`Action failed: ${error}`);
|
||||||
setIsErrorHidden(false);
|
setIsErrorHidden(false);
|
||||||
setIsLoggingIn(false);
|
setIsLoggingIn(false);
|
||||||
|
eventBus.emit('failedLoginUser');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ function Launchpad({
|
|||||||
editHost,
|
editHost,
|
||||||
shareHost,
|
shareHost,
|
||||||
userRef,
|
userRef,
|
||||||
|
isHostViewerMenuOpen,
|
||||||
|
setIsHostViewerMenuOpen,
|
||||||
}) {
|
}) {
|
||||||
const launchpadRef = useRef(null);
|
const launchpadRef = useRef(null);
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
@@ -32,6 +34,7 @@ function Launchpad({
|
|||||||
isAddHostHidden &&
|
isAddHostHidden &&
|
||||||
isEditHostHidden &&
|
isEditHostHidden &&
|
||||||
isErrorHidden &&
|
isErrorHidden &&
|
||||||
|
!isHostViewerMenuOpen &&
|
||||||
!isAnyModalOpen
|
!isAnyModalOpen
|
||||||
) {
|
) {
|
||||||
onClose();
|
onClose();
|
||||||
@@ -43,7 +46,7 @@ function Launchpad({
|
|||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("mousedown", handleClickOutside);
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
};
|
};
|
||||||
}, [onClose, isAddHostHidden, isEditHostHidden, isErrorHidden, isAnyModalOpen]);
|
}, [onClose, isAddHostHidden, isEditHostHidden, isErrorHidden, isHostViewerMenuOpen, isAnyModalOpen]);
|
||||||
|
|
||||||
const handleModalOpen = () => {
|
const handleModalOpen = () => {
|
||||||
setIsAnyModalOpen(true);
|
setIsAnyModalOpen(true);
|
||||||
@@ -190,6 +193,8 @@ function Launchpad({
|
|||||||
onModalOpen={handleModalOpen}
|
onModalOpen={handleModalOpen}
|
||||||
onModalClose={handleModalClose}
|
onModalClose={handleModalClose}
|
||||||
userRef={userRef}
|
userRef={userRef}
|
||||||
|
isMenuOpen={isHostViewerMenuOpen || false}
|
||||||
|
setIsMenuOpen={setIsHostViewerMenuOpen}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -211,6 +216,8 @@ Launchpad.propTypes = {
|
|||||||
editHost: PropTypes.func.isRequired,
|
editHost: PropTypes.func.isRequired,
|
||||||
shareHost: PropTypes.func.isRequired,
|
shareHost: PropTypes.func.isRequired,
|
||||||
userRef: PropTypes.object.isRequired,
|
userRef: PropTypes.object.isRequired,
|
||||||
|
isHostViewerMenuOpen: PropTypes.bool,
|
||||||
|
setIsHostViewerMenuOpen: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Launchpad;
|
export default Launchpad;
|
||||||
@@ -1,9 +1,22 @@
|
|||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { Button, Input } from "@mui/joy";
|
import { Button, Input, Menu, MenuItem, IconButton } from "@mui/joy";
|
||||||
import ShareHostModal from "../../modals/ShareHostModal";
|
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 [hosts, setHosts] = useState([]);
|
||||||
const [filteredHosts, setFilteredHosts] = useState([]);
|
const [filteredHosts, setFilteredHosts] = useState([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
@@ -15,6 +28,24 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
|
|||||||
const [isDeleting, setIsDeleting] = useState(false);
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
const [isShareModalHidden, setIsShareModalHidden] = useState(true);
|
const [isShareModalHidden, setIsShareModalHidden] = useState(true);
|
||||||
const [selectedHostForShare, setSelectedHostForShare] = useState(null);
|
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 () => {
|
const fetchHosts = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -51,9 +82,9 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const filtered = hosts.filter((hostWrapper) => {
|
const filtered = hosts.filter((hostWrapper) => {
|
||||||
const hostConfig = hostWrapper.config || {};
|
const hostConfig = hostWrapper.config || {};
|
||||||
return hostConfig.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
return hostConfig.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
hostConfig.ip?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
hostConfig.ip?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
hostConfig.folder?.toLowerCase().includes(searchTerm.toLowerCase());
|
hostConfig.folder?.toLowerCase().includes(searchTerm.toLowerCase());
|
||||||
});
|
});
|
||||||
setFilteredHosts(filtered);
|
setFilteredHosts(filtered);
|
||||||
}, [searchTerm, hosts]);
|
}, [searchTerm, hosts]);
|
||||||
@@ -168,7 +199,7 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
|
|||||||
const handleDelete = async (e, hostWrapper) => {
|
const handleDelete = async (e, hostWrapper) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (isDeleting) return;
|
if (isDeleting) return;
|
||||||
|
|
||||||
setIsDeleting(true);
|
setIsDeleting(true);
|
||||||
try {
|
try {
|
||||||
const isOwner = hostWrapper.createdBy?._id === userRef.current?.getUser()?.id;
|
const isOwner = hostWrapper.createdBy?._id === userRef.current?.getUser()?.id;
|
||||||
@@ -229,7 +260,8 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
className="text-black"
|
variant="outlined"
|
||||||
|
className="text-white"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (!hostWrapper.config || !hostWrapper.config.ip || !hostWrapper.config.user) {
|
if (!hostWrapper.config || !hostWrapper.config.ip || !hostWrapper.config.user) {
|
||||||
@@ -242,76 +274,36 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
|
|||||||
backgroundColor: "#6e6e6e",
|
backgroundColor: "#6e6e6e",
|
||||||
"&:hover": { backgroundColor: "#0f0f0f" },
|
"&:hover": { backgroundColor: "#0f0f0f" },
|
||||||
opacity: isDeleting ? 0.5 : 1,
|
opacity: isDeleting ? 0.5 : 1,
|
||||||
cursor: isDeleting ? "not-allowed" : "pointer"
|
cursor: isDeleting ? "not-allowed" : "pointer",
|
||||||
|
borderColor: "#3d3d3d",
|
||||||
|
borderWidth: "2px",
|
||||||
|
color: "#fff",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Connect
|
Connect
|
||||||
</Button>
|
</Button>
|
||||||
{isOwner && (
|
<IconButton
|
||||||
<>
|
variant="outlined"
|
||||||
<Button
|
className="text-white"
|
||||||
className="text-black"
|
onClick={(e) => {
|
||||||
onClick={(e) => {
|
e.stopPropagation();
|
||||||
e.stopPropagation();
|
setSelectedHost(hostWrapper);
|
||||||
setSelectedHostForShare(hostWrapper);
|
setIsMenuOpen(!isMenuOpen);
|
||||||
setIsShareModalHidden(false);
|
anchorEl.current = e.currentTarget;
|
||||||
}}
|
}}
|
||||||
disabled={isDeleting}
|
disabled={isDeleting}
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: "#6e6e6e",
|
backgroundColor: "#6e6e6e",
|
||||||
"&:hover": { backgroundColor: "#0f0f0f" },
|
"&:hover": { backgroundColor: "#0f0f0f" },
|
||||||
opacity: isDeleting ? 0.5 : 1,
|
opacity: isDeleting ? 0.5 : 1,
|
||||||
cursor: isDeleting ? "not-allowed" : "pointer"
|
cursor: isDeleting ? "not-allowed" : "pointer",
|
||||||
}}
|
borderColor: "#3d3d3d",
|
||||||
>
|
borderWidth: "2px",
|
||||||
Share
|
color: "#fff",
|
||||||
</Button>
|
}}
|
||||||
<Button
|
>
|
||||||
className="text-black"
|
⋮
|
||||||
onClick={(e) => handleDelete(e, hostWrapper)}
|
</IconButton>
|
||||||
disabled={isDeleting}
|
|
||||||
sx={{
|
|
||||||
backgroundColor: "#6e6e6e",
|
|
||||||
"&:hover": { backgroundColor: "#0f0f0f" },
|
|
||||||
opacity: isDeleting ? 0.5 : 1,
|
|
||||||
cursor: isDeleting ? "not-allowed" : "pointer"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isDeleting ? "Deleting..." : "Delete"}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
className="text-black"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
openEditPanel(hostConfig);
|
|
||||||
}}
|
|
||||||
disabled={isDeleting}
|
|
||||||
sx={{
|
|
||||||
backgroundColor: "#6e6e6e",
|
|
||||||
"&:hover": { backgroundColor: "#0f0f0f" },
|
|
||||||
opacity: isDeleting ? 0.5 : 1,
|
|
||||||
cursor: isDeleting ? "not-allowed" : "pointer"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!isOwner && (
|
|
||||||
<Button
|
|
||||||
className="text-black"
|
|
||||||
onClick={(e) => handleDelete(e, hostWrapper)}
|
|
||||||
disabled={isDeleting}
|
|
||||||
sx={{
|
|
||||||
backgroundColor: "#6e6e6e",
|
|
||||||
"&:hover": { backgroundColor: "#0f0f0f" },
|
|
||||||
opacity: isDeleting ? 0.5 : 1,
|
|
||||||
cursor: isDeleting ? "not-allowed" : "pointer"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isDeleting ? "Removing..." : "Remove Share"}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -352,7 +344,6 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Render hosts without folders first */}
|
|
||||||
<div
|
<div
|
||||||
className={`flex flex-col gap-2 p-2 rounded-lg transition-colors ${isDraggingOver === 'no-folder' ? 'bg-neutral-700' : ''}`}
|
className={`flex flex-col gap-2 p-2 rounded-lg transition-colors ${isDraggingOver === 'no-folder' ? 'bg-neutral-700' : ''}`}
|
||||||
onDragOver={(e) => handleDragOver(e, 'no-folder')}
|
onDragOver={(e) => handleDragOver(e, 'no-folder')}
|
||||||
@@ -362,7 +353,6 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
|
|||||||
{noFolder.map((host) => renderHostItem(host))}
|
{noFolder.map((host) => renderHostItem(host))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Render folders and their hosts */}
|
|
||||||
{sortedFolders.map((folderName) => (
|
{sortedFolders.map((folderName) => (
|
||||||
<div key={folderName} className="mb-2">
|
<div key={folderName} className="mb-2">
|
||||||
<div
|
<div
|
||||||
@@ -403,6 +393,68 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
|
|||||||
handleShare={handleShare}
|
handleShare={handleShare}
|
||||||
hostConfig={selectedHostForShare}
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -418,6 +470,8 @@ HostViewer.propTypes = {
|
|||||||
onModalOpen: PropTypes.func.isRequired,
|
onModalOpen: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired,
|
onModalClose: PropTypes.func.isRequired,
|
||||||
userRef: PropTypes.object.isRequired,
|
userRef: PropTypes.object.isRequired,
|
||||||
|
isMenuOpen: PropTypes.bool.isRequired,
|
||||||
|
setIsMenuOpen: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default HostViewer;
|
export default HostViewer;
|
||||||
@@ -6,7 +6,7 @@ import io from "socket.io-client";
|
|||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import theme from "../../theme.js";
|
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 terminalRef = useRef(null);
|
||||||
const socketRef = useRef(null);
|
const socketRef = useRef(null);
|
||||||
const fitAddon = useRef(new FitAddon());
|
const fitAddon = useRef(new FitAddon());
|
||||||
@@ -18,8 +18,8 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde
|
|||||||
|
|
||||||
if (!parentContainer || parentContainer.clientWidth === 0) return;
|
if (!parentContainer || parentContainer.clientWidth === 0) return;
|
||||||
|
|
||||||
const parentWidth = parentContainer.clientWidth - 10;
|
const parentWidth = parentContainer.clientWidth - 8;
|
||||||
const parentHeight = parentContainer.clientHeight - 10;
|
const parentHeight = parentContainer.clientHeight - 12;
|
||||||
|
|
||||||
terminalContainer.style.width = `${parentWidth}px`;
|
terminalContainer.style.width = `${parentWidth}px`;
|
||||||
terminalContainer.style.height = `${parentHeight}px`;
|
terminalContainer.style.height = `${parentHeight}px`;
|
||||||
@@ -50,6 +50,9 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde
|
|||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
scrollback: 1000,
|
scrollback: 1000,
|
||||||
ignoreBracketedPasteMode: true,
|
ignoreBracketedPasteMode: true,
|
||||||
|
letterSpacing: 0,
|
||||||
|
lineHeight: 1,
|
||||||
|
padding: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
terminalInstance.current.loadAddon(fitAddon.current);
|
terminalInstance.current.loadAddon(fitAddon.current);
|
||||||
@@ -62,21 +65,28 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde
|
|||||||
{
|
{
|
||||||
path: "/ssh.io/socket.io",
|
path: "/ssh.io/socket.io",
|
||||||
transports: ["websocket", "polling"],
|
transports: ["websocket", "polling"],
|
||||||
|
reconnection: true,
|
||||||
|
reconnectionAttempts: 5,
|
||||||
|
reconnectionDelay: 1000,
|
||||||
|
timeout: 20000,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
socketRef.current = socket;
|
socketRef.current = socket;
|
||||||
|
|
||||||
socket.on("connect_error", (error) => {
|
socket.on("connect_error", (error) => {
|
||||||
terminalInstance.current.write(`\r\n*** Socket connection error: ${error.message} ***\r\n`);
|
terminalInstance.current.write(`\r\n*** Socket connection error: ${error.message} ***\r\n`);
|
||||||
|
console.error("Socket connection error:", error);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("connect_timeout", () => {
|
socket.on("connect_timeout", () => {
|
||||||
terminalInstance.current.write(`\r\n*** Socket connection timeout ***\r\n`);
|
terminalInstance.current.write(`\r\n*** Socket connection timeout ***\r\n`);
|
||||||
|
console.error("Socket connection timeout");
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("error", (err) => {
|
socket.on("error", (err) => {
|
||||||
|
console.error("SSH connection error:", err);
|
||||||
const isAuthError = err.toLowerCase().includes("authentication") || err.toLowerCase().includes("auth");
|
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;
|
authModalShown = true;
|
||||||
setIsNoAuthHidden(false);
|
setIsNoAuthHidden(false);
|
||||||
}
|
}
|
||||||
@@ -88,7 +98,7 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde
|
|||||||
resizeTerminal();
|
resizeTerminal();
|
||||||
const { cols, rows } = terminalInstance.current;
|
const { cols, rows } = terminalInstance.current;
|
||||||
|
|
||||||
if (!hostConfig.password?.trim() && !hostConfig.rsaKey?.trim()) {
|
if (!hostConfig.password?.trim() && !hostConfig.sshKey?.trim()) {
|
||||||
setIsNoAuthHidden(false);
|
setIsNoAuthHidden(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -98,16 +108,19 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde
|
|||||||
user: hostConfig.user,
|
user: hostConfig.user,
|
||||||
port: Number(hostConfig.port) || 22,
|
port: Number(hostConfig.port) || 22,
|
||||||
password: hostConfig.password?.trim(),
|
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);
|
socket.emit("connectToHost", cols, rows, sshConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
fitAddon.current.fit();
|
if (terminalInstance.current) {
|
||||||
resizeTerminal();
|
fitAddon.current.fit();
|
||||||
terminalInstance.current.focus();
|
resizeTerminal();
|
||||||
|
terminalInstance.current.focus();
|
||||||
|
}
|
||||||
}, 50);
|
}, 50);
|
||||||
|
|
||||||
socket.on("data", (data) => {
|
socket.on("data", (data) => {
|
||||||
@@ -117,68 +130,85 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde
|
|||||||
|
|
||||||
let isPasting = false;
|
let isPasting = false;
|
||||||
|
|
||||||
terminalInstance.current.onData((data) => {
|
if (terminalInstance.current) {
|
||||||
socketRef.current.emit("data", data);
|
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) => {
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
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;
|
let authModalShown = false;
|
||||||
|
|
||||||
socket.on("noAuthRequired", () => {
|
socket.on("noAuthRequired", () => {
|
||||||
if (!hostConfig.password?.trim() && !hostConfig.rsaKey?.trim() && !authModalShown) {
|
if (!hostConfig.password?.trim() && !hostConfig.sshKey?.trim() && !authModalShown) {
|
||||||
authModalShown = true;
|
authModalShown = true;
|
||||||
setIsNoAuthHidden(false);
|
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 () => {
|
return () => {
|
||||||
|
clearInterval(pingInterval);
|
||||||
if (terminalInstance.current) {
|
if (terminalInstance.current) {
|
||||||
terminalInstance.current.dispose();
|
terminalInstance.current.dispose();
|
||||||
terminalInstance.current = null;
|
terminalInstance.current = null;
|
||||||
@@ -202,14 +232,21 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde
|
|||||||
const parentContainer = terminalContainer.parentElement;
|
const parentContainer = terminalContainer.parentElement;
|
||||||
if (!parentContainer) return;
|
if (!parentContainer) return;
|
||||||
|
|
||||||
const observer = new ResizeObserver(() => {
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
resizeTerminal();
|
resizeTerminal();
|
||||||
});
|
});
|
||||||
|
|
||||||
observer.observe(parentContainer);
|
resizeObserver.observe(parentContainer);
|
||||||
|
|
||||||
|
const handleWindowResize = () => {
|
||||||
|
resizeTerminal();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleWindowResize);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
observer.disconnect();
|
resizeObserver.disconnect();
|
||||||
|
window.removeEventListener('resize', handleWindowResize);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -222,7 +259,7 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde
|
|||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
transform: 'translateY(5px) translateX(5px)',
|
transform: 'translateY(2px) translateX(3px)',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -235,9 +272,12 @@ NewTerminal.propTypes = {
|
|||||||
ip: PropTypes.string.isRequired,
|
ip: PropTypes.string.isRequired,
|
||||||
user: PropTypes.string.isRequired,
|
user: PropTypes.string.isRequired,
|
||||||
password: PropTypes.string,
|
password: PropTypes.string,
|
||||||
|
sshKey: PropTypes.string,
|
||||||
rsaKey: PropTypes.string,
|
rsaKey: PropTypes.string,
|
||||||
port: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
port: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
isVisible: PropTypes.bool.isRequired,
|
isVisible: PropTypes.bool.isRequired,
|
||||||
setIsNoAuthHidden: PropTypes.func.isRequired,
|
setIsNoAuthHidden: PropTypes.func.isRequired,
|
||||||
|
setErrorMessage: PropTypes.func.isRequired,
|
||||||
|
setIsErrorHidden: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
@@ -149,6 +149,27 @@ export const User = forwardRef(({ onLoginSuccess, onCreateSuccess, onDeleteSucce
|
|||||||
if (!currentUser.current) return onFailure("Not authenticated");
|
if (!currentUser.current) return onFailure("Not authenticated");
|
||||||
|
|
||||||
try {
|
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) => {
|
const response = await new Promise((resolve) => {
|
||||||
socketRef.current.emit("saveHostConfig", {
|
socketRef.current.emit("saveHostConfig", {
|
||||||
userId: currentUser.current.id,
|
userId: currentUser.current.id,
|
||||||
@@ -186,7 +207,7 @@ export const User = forwardRef(({ onLoginSuccess, onCreateSuccess, onDeleteSucce
|
|||||||
user: host.config.user || '',
|
user: host.config.user || '',
|
||||||
port: host.config.port || '22',
|
port: host.config.port || '22',
|
||||||
password: host.config.password || '',
|
password: host.config.password || '',
|
||||||
rsaKey: host.config.rsaKey || '',
|
sshKey: host.config.sshKey || '',
|
||||||
} : {}
|
} : {}
|
||||||
})).filter(host => host.config && host.config.ip && host.config.user);
|
})).filter(host => host.config && host.config.ip && host.config.user);
|
||||||
} else {
|
} else {
|
||||||
@@ -222,7 +243,18 @@ export const User = forwardRef(({ onLoginSuccess, onCreateSuccess, onDeleteSucce
|
|||||||
if (!currentUser.current) return onFailure("Not authenticated");
|
if (!currentUser.current) return onFailure("Not authenticated");
|
||||||
|
|
||||||
try {
|
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) => {
|
const response = await new Promise((resolve) => {
|
||||||
socketRef.current.emit("editHost", {
|
socketRef.current.emit("editHost", {
|
||||||
userId: currentUser.current.id,
|
userId: currentUser.current.id,
|
||||||
|
|||||||
@@ -185,19 +185,36 @@ io.of('/database.io').on('connection', (socket) => {
|
|||||||
user: hostConfig.user.trim(),
|
user: hostConfig.user.trim(),
|
||||||
port: hostConfig.port || 22,
|
port: hostConfig.port || 22,
|
||||||
password: hostConfig.password?.trim() || undefined,
|
password: hostConfig.password?.trim() || undefined,
|
||||||
rsaKey: hostConfig.rsaKey?.trim() || undefined
|
sshKey: hostConfig.sshKey?.trim() || undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const finalName = cleanConfig.name || cleanConfig.ip;
|
const finalName = cleanConfig.name || cleanConfig.ip;
|
||||||
|
|
||||||
const existingHost = await Host.findOne({
|
// Check for hosts with the same name (case insensitive)
|
||||||
name: finalName,
|
const existingHostByName = await Host.findOne({
|
||||||
createdBy: userId
|
createdBy: userId,
|
||||||
|
name: { $regex: new RegExp('^' + finalName + '$', 'i') }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingHost) {
|
if (existingHostByName) {
|
||||||
logger.warn(`Host with name ${finalName} already exists for user: ${userId}`);
|
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);
|
const encryptedConfig = encryptData(cleanConfig, userId, sessionToken);
|
||||||
@@ -397,6 +414,7 @@ io.of('/database.io').on('connection', (socket) => {
|
|||||||
return callback({ error: 'Invalid session' });
|
return callback({ error: 'Invalid session' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find the host to be edited
|
||||||
const hosts = await Host.find({ createdBy: userId });
|
const hosts = await Host.find({ createdBy: userId });
|
||||||
const host = hosts.find(h => {
|
const host = hosts.find(h => {
|
||||||
const decryptedConfig = decryptData(h.config, userId, sessionToken);
|
const decryptedConfig = decryptData(h.config, userId, sessionToken);
|
||||||
@@ -408,6 +426,37 @@ io.of('/database.io').on('connection', (socket) => {
|
|||||||
return callback({ error: 'Host not found' });
|
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 = {
|
const cleanConfig = {
|
||||||
name: newHostConfig.name?.trim(),
|
name: newHostConfig.name?.trim(),
|
||||||
folder: newHostConfig.folder?.trim() || null,
|
folder: newHostConfig.folder?.trim() || null,
|
||||||
@@ -415,7 +464,7 @@ io.of('/database.io').on('connection', (socket) => {
|
|||||||
user: newHostConfig.user.trim(),
|
user: newHostConfig.user.trim(),
|
||||||
port: newHostConfig.port || 22,
|
port: newHostConfig.port || 22,
|
||||||
password: newHostConfig.password?.trim() || undefined,
|
password: newHostConfig.password?.trim() || undefined,
|
||||||
rsaKey: newHostConfig.rsaKey?.trim() || undefined
|
sshKey: newHostConfig.sshKey?.trim() || undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const encryptedConfig = encryptData(cleanConfig, userId, sessionToken);
|
const encryptedConfig = encryptData(cleanConfig, userId, sessionToken);
|
||||||
@@ -424,6 +473,7 @@ io.of('/database.io').on('connection', (socket) => {
|
|||||||
return callback({ error: 'Configuration encryption failed' });
|
return callback({ error: 'Configuration encryption failed' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
host.name = finalName;
|
||||||
host.config = encryptedConfig;
|
host.config = encryptedConfig;
|
||||||
host.folder = cleanConfig.folder;
|
host.folder = cleanConfig.folder;
|
||||||
await host.save();
|
await host.save();
|
||||||
@@ -432,7 +482,7 @@ io.of('/database.io').on('connection', (socket) => {
|
|||||||
callback({ success: true });
|
callback({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Host edit error:', error);
|
logger.error('Host edit error:', error);
|
||||||
callback({ error: 'Failed to edit host' });
|
callback({ error: `Failed to edit host: ${error.message}` });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ const io = socketIo(server, {
|
|||||||
methods: ["GET", "POST"],
|
methods: ["GET", "POST"],
|
||||||
credentials: true
|
credentials: true
|
||||||
},
|
},
|
||||||
allowEIO3: true
|
allowEIO3: true,
|
||||||
|
pingInterval: 2500,
|
||||||
|
pingTimeout: 5000,
|
||||||
|
maxHttpBufferSize: 1e7,
|
||||||
});
|
});
|
||||||
|
|
||||||
const logger = {
|
const logger = {
|
||||||
@@ -32,7 +35,7 @@ io.on("connection", (socket) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hostConfig.password && !hostConfig.rsaKey) {
|
if (!hostConfig.password && !hostConfig.sshKey) {
|
||||||
logger.error("No authentication provided");
|
logger.error("No authentication provided");
|
||||||
socket.emit("error", "Authentication required");
|
socket.emit("error", "Authentication required");
|
||||||
return;
|
return;
|
||||||
@@ -42,18 +45,18 @@ io.on("connection", (socket) => {
|
|||||||
ip: hostConfig.ip,
|
ip: hostConfig.ip,
|
||||||
port: hostConfig.port,
|
port: hostConfig.port,
|
||||||
user: hostConfig.user,
|
user: hostConfig.user,
|
||||||
authType: hostConfig.password ? 'password' : 'public key',
|
authType: hostConfig.password ? 'password' : 'key',
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.info("Connecting with config:", safeHostConfig);
|
logger.info("Connecting with config:", safeHostConfig);
|
||||||
const { ip, port, user, password, rsaKey } = hostConfig;
|
const { ip, port, user, password, sshKey, } = hostConfig;
|
||||||
|
|
||||||
const conn = new SSHClient();
|
const conn = new SSHClient();
|
||||||
conn
|
conn
|
||||||
.on("ready", function () {
|
.on("ready", function () {
|
||||||
logger.info("SSH connection established");
|
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) {
|
if (err) {
|
||||||
logger.error("Shell error:", err.message);
|
logger.error("Shell error:", err.message);
|
||||||
socket.emit("error", err.message);
|
socket.emit("error", err.message);
|
||||||
@@ -93,12 +96,22 @@ io.on("connection", (socket) => {
|
|||||||
logger.error("Error:", err.message);
|
logger.error("Error:", err.message);
|
||||||
socket.emit("error", err.message);
|
socket.emit("error", err.message);
|
||||||
})
|
})
|
||||||
|
.on("ping", function () {
|
||||||
|
socket.emit("ping");
|
||||||
|
})
|
||||||
.connect({
|
.connect({
|
||||||
host: ip,
|
host: ip,
|
||||||
port: port,
|
port: port,
|
||||||
username: user,
|
username: user,
|
||||||
password: password,
|
password: password || undefined,
|
||||||
privateKey: rsaKey ? Buffer.from(rsaKey) : 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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
Input,
|
Input,
|
||||||
Stack,
|
Stack,
|
||||||
DialogTitle,
|
|
||||||
DialogContent,
|
|
||||||
ModalDialog,
|
ModalDialog,
|
||||||
Select,
|
Select,
|
||||||
Option,
|
Option,
|
||||||
@@ -27,19 +25,51 @@ import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
|||||||
const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidden }) => {
|
const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidden }) => {
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const [activeTab, setActiveTab] = useState(0);
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
|
const [showError, setShowError] = useState(false);
|
||||||
|
|
||||||
const handleFileChange = (e) => {
|
const handleFileChange = (e) => {
|
||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
if (file) {
|
const supportedKeyTypes = {
|
||||||
if (file.name.endsWith('.key') || file.name.endsWith('.pem') || file.name.endsWith('.pub')) {
|
'id_rsa': 'RSA',
|
||||||
const reader = new FileReader();
|
'id_ed25519': 'ED25519',
|
||||||
reader.onload = (event) => {
|
'id_ecdsa': 'ECDSA',
|
||||||
setForm({ ...form, rsaKey: event.target.result });
|
'id_dsa': 'DSA',
|
||||||
};
|
'.pem': 'PEM',
|
||||||
reader.readAsText(file);
|
'.key': 'KEY',
|
||||||
} else {
|
'.ppk': 'PPK'
|
||||||
alert("Please upload a valid public key file.");
|
};
|
||||||
}
|
|
||||||
|
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,
|
...prev,
|
||||||
authMethod: newMethod,
|
authMethod: newMethod,
|
||||||
password: "",
|
password: "",
|
||||||
rsaKey: ""
|
sshKey: "",
|
||||||
|
keyType: "",
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const isFormValid = () => {
|
const isFormValid = () => {
|
||||||
if (!form.ip || !form.user || !form.port) return false;
|
const { ip, user, port, authMethod, password, sshKey } = form;
|
||||||
const portNum = Number(form.port);
|
|
||||||
|
if (!ip?.trim() || !user?.trim() || !port) return false;
|
||||||
|
|
||||||
|
const portNum = Number(port);
|
||||||
if (isNaN(portNum) || portNum < 1 || portNum > 65535) return false;
|
if (isNaN(portNum) || portNum < 1 || portNum > 65535) return false;
|
||||||
|
|
||||||
|
if (!form.rememberHost) return true;
|
||||||
|
|
||||||
if (form.rememberHost) {
|
if (form.rememberHost) {
|
||||||
if (form.authMethod === 'Select Auth') return false;
|
if (authMethod === 'Select Auth') return false;
|
||||||
if (form.authMethod === 'rsaKey' && !form.rsaKey) return false;
|
if (authMethod === 'password' && !password?.trim()) return false;
|
||||||
if (form.authMethod === 'password' && !form.password) return false;
|
if (authMethod === 'sshKey' && !sshKey?.trim()) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -68,26 +104,29 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
|
|||||||
|
|
||||||
const handleSubmit = (event) => {
|
const handleSubmit = (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (isFormValid()) {
|
|
||||||
if (!form.rememberHost) {
|
setErrorMessage("");
|
||||||
handleAddHost();
|
setShowError(false);
|
||||||
} else {
|
|
||||||
handleAddHost();
|
|
||||||
}
|
|
||||||
|
|
||||||
setForm({
|
if (!form.ip?.trim()) {
|
||||||
name: '',
|
setErrorMessage("Please provide an IP address.");
|
||||||
folder: '',
|
setShowError(true);
|
||||||
ip: '',
|
return;
|
||||||
user: '',
|
}
|
||||||
password: '',
|
|
||||||
rsaKey: '',
|
if (form.connectionType === 'ssh' && !form.user?.trim()) {
|
||||||
port: 22,
|
setErrorMessage("Please provide a username for SSH connection.");
|
||||||
authMethod: 'Select Auth',
|
setShowError(true);
|
||||||
rememberHost: false,
|
return;
|
||||||
storePassword: true,
|
}
|
||||||
});
|
|
||||||
setIsAddHostHidden(true);
|
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',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
backdropFilter: 'blur(5px)',
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.2)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ModalDialog
|
<ModalDialog
|
||||||
layout="center"
|
layout="center"
|
||||||
|
variant="outlined"
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: theme.palette.general.tertiary,
|
backgroundColor: theme.palette.general.tertiary,
|
||||||
borderColor: theme.palette.general.secondary,
|
borderColor: theme.palette.general.secondary,
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
padding: 3,
|
padding: 0,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
maxWidth: '500px',
|
maxWidth: '500px',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -116,135 +158,225 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
|
|||||||
mx: 2,
|
mx: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogTitle sx={{ mb: 2 }}>Add Host</DialogTitle>
|
{showError && (
|
||||||
<DialogContent>
|
<div style={{
|
||||||
<form onSubmit={handleSubmit}>
|
backgroundColor: "#c53030",
|
||||||
<Tabs
|
color: "white",
|
||||||
value={activeTab}
|
padding: "10px",
|
||||||
onChange={(e, val) => setActiveTab(val)}
|
textAlign: "center",
|
||||||
sx={{
|
borderTopLeftRadius: "10px",
|
||||||
backgroundColor: theme.palette.general.disabled,
|
borderTopRightRadius: "10px"
|
||||||
borderRadius: '8px',
|
}}>
|
||||||
padding: '8px',
|
{errorMessage}
|
||||||
marginBottom: '16px',
|
</div>
|
||||||
width: '100%',
|
)}
|
||||||
}}
|
<Tabs
|
||||||
>
|
value={activeTab}
|
||||||
<TabList
|
onChange={(e, val) => setActiveTab(val)}
|
||||||
sx={{
|
sx={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
gap: 0,
|
mb: 0,
|
||||||
mb: 2,
|
backgroundColor: theme.palette.general.tertiary,
|
||||||
'& button': {
|
}}
|
||||||
flex: 1,
|
>
|
||||||
bgcolor: 'transparent',
|
<TabList
|
||||||
color: theme.palette.text.secondary,
|
sx={{
|
||||||
'&:hover': {
|
width: '100%',
|
||||||
bgcolor: 'rgba(255, 255, 255, 0.1)',
|
gap: 0,
|
||||||
},
|
borderTopLeftRadius: 10,
|
||||||
'&.Mui-selected': {
|
borderTopRightRadius: 10,
|
||||||
bgcolor: theme.palette.general.primary,
|
backgroundColor: theme.palette.general.primary,
|
||||||
color: theme.palette.text.primary,
|
'& button': {
|
||||||
'&:hover': {
|
flex: 1,
|
||||||
bgcolor: theme.palette.general.primary,
|
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>
|
<Tab sx={{ flex: 1 }}>Basic Info</Tab>
|
||||||
</TabList>
|
<Tab sx={{ flex: 1 }}>Connection</Tab>
|
||||||
|
<Tab sx={{ flex: 1 }}>Authentication</Tab>
|
||||||
|
</TabList>
|
||||||
|
|
||||||
<TabPanel value={0}>
|
<div style={{ padding: '24px', backgroundColor: theme.palette.general.tertiary }}>
|
||||||
<Stack spacing={2}>
|
<TabPanel value={0}>
|
||||||
<FormControl>
|
<Stack spacing={2}>
|
||||||
<FormLabel>Host Name</FormLabel>
|
<FormControl>
|
||||||
<Input
|
<FormLabel>Host Name</FormLabel>
|
||||||
value={form.name}
|
<Input
|
||||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
value={form.name}
|
||||||
sx={{
|
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||||
backgroundColor: theme.palette.general.primary,
|
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,
|
color: theme.palette.text.primary,
|
||||||
}}
|
},
|
||||||
/>
|
}}
|
||||||
</FormControl>
|
/>
|
||||||
<FormControl>
|
</FormControl>
|
||||||
<FormLabel>Folder</FormLabel>
|
</Stack>
|
||||||
<Input
|
</TabPanel>
|
||||||
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>
|
|
||||||
|
|
||||||
<TabPanel value={1}>
|
<TabPanel value={1}>
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
<FormControl error={!form.ip}>
|
<FormControl error={!form.ip}>
|
||||||
<FormLabel>Host IP</FormLabel>
|
<FormLabel>Host IP</FormLabel>
|
||||||
<Input
|
<Input
|
||||||
value={form.ip}
|
value={form.ip}
|
||||||
onChange={(e) => setForm({ ...form, ip: e.target.value })}
|
onChange={(e) => setForm({ ...form, ip: e.target.value })}
|
||||||
required
|
required
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: theme.palette.general.primary,
|
backgroundColor: theme.palette.general.primary,
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormControl error={!form.user}>
|
<FormControl error={!form.user}>
|
||||||
<FormLabel>Host User</FormLabel>
|
<FormLabel>Host User</FormLabel>
|
||||||
<Input
|
<Input
|
||||||
value={form.user}
|
value={form.user}
|
||||||
onChange={(e) => setForm({ ...form, user: e.target.value })}
|
onChange={(e) => setForm({ ...form, user: e.target.value })}
|
||||||
required
|
required
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: theme.palette.general.primary,
|
backgroundColor: theme.palette.general.primary,
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormControl error={form.port < 1 || form.port > 65535}>
|
<FormControl error={form.port < 1 || form.port > 65535}>
|
||||||
<FormLabel>Host Port</FormLabel>
|
<FormLabel>Host Port</FormLabel>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={form.port}
|
value={form.port}
|
||||||
onChange={(e) => setForm({ ...form, port: e.target.value })}
|
onChange={(e) => setForm({ ...form, port: e.target.value })}
|
||||||
min={1}
|
min={1}
|
||||||
max={65535}
|
max={65535}
|
||||||
required
|
required
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: theme.palette.general.primary,
|
backgroundColor: theme.palette.general.primary,
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</Stack>
|
</Stack>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
<TabPanel value={2}>
|
<TabPanel value={2}>
|
||||||
<Stack spacing={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>
|
<FormControl>
|
||||||
<FormLabel>Remember Host</FormLabel>
|
<FormLabel>Store Password</FormLabel>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={form.rememberHost}
|
checked={Boolean(form.storePassword)}
|
||||||
onChange={(e) => setForm({
|
onChange={(e) => setForm({ ...form, storePassword: e.target.checked })}
|
||||||
...form,
|
|
||||||
rememberHost: e.target.checked,
|
|
||||||
|
|
||||||
...((!e.target.checked) && {
|
|
||||||
authMethod: 'Select Auth',
|
|
||||||
password: '',
|
|
||||||
rsaKey: '',
|
|
||||||
storePassword: true
|
|
||||||
})
|
|
||||||
})}
|
|
||||||
sx={{
|
sx={{
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
'&.Mui-checked': {
|
'&.Mui-checked': {
|
||||||
@@ -253,119 +385,32 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
{form.rememberHost && (
|
)}
|
||||||
<>
|
</Stack>
|
||||||
<FormControl>
|
</TabPanel>
|
||||||
<FormLabel>Store Password</FormLabel>
|
</div>
|
||||||
<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>
|
|
||||||
|
|
||||||
{form.authMethod === 'password' && (
|
<Button
|
||||||
<FormControl error={!form.password}>
|
onClick={handleSubmit}
|
||||||
<FormLabel>Password</FormLabel>
|
disabled={!isFormValid()}
|
||||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
sx={{
|
||||||
<Input
|
backgroundColor: theme.palette.general.primary,
|
||||||
type={showPassword ? 'text' : 'password'}
|
color: theme.palette.text.primary,
|
||||||
value={form.password}
|
'&:hover': {
|
||||||
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
backgroundColor: theme.palette.general.disabled,
|
||||||
sx={{
|
},
|
||||||
backgroundColor: theme.palette.general.primary,
|
'&:disabled': {
|
||||||
color: theme.palette.text.primary,
|
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
flex: 1
|
color: 'rgba(255, 255, 255, 0.3)',
|
||||||
}}
|
},
|
||||||
/>
|
marginTop: 1,
|
||||||
<IconButton
|
width: '100%',
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
height: '40px',
|
||||||
sx={{
|
}}
|
||||||
color: theme.palette.text.primary,
|
>
|
||||||
marginLeft: 1
|
Add Host
|
||||||
}}
|
</Button>
|
||||||
>
|
</Tabs>
|
||||||
{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>
|
|
||||||
</ModalDialog>
|
</ModalDialog>
|
||||||
</Modal>
|
</Modal>
|
||||||
</CssVarsProvider>
|
</CssVarsProvider>
|
||||||
@@ -374,18 +419,7 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
|
|||||||
|
|
||||||
AddHostModal.propTypes = {
|
AddHostModal.propTypes = {
|
||||||
isHidden: PropTypes.bool.isRequired,
|
isHidden: PropTypes.bool.isRequired,
|
||||||
form: PropTypes.shape({
|
form: PropTypes.object.isRequired,
|
||||||
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,
|
|
||||||
setForm: PropTypes.func.isRequired,
|
setForm: PropTypes.func.isRequired,
|
||||||
handleAddHost: PropTypes.func.isRequired,
|
handleAddHost: PropTypes.func.isRequired,
|
||||||
setIsAddHostHidden: PropTypes.func.isRequired,
|
setIsAddHostHidden: PropTypes.func.isRequired,
|
||||||
|
|||||||
306
src/modals/AuthModal.jsx
Normal file
306
src/modals/AuthModal.jsx
Normal 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;
|
||||||
@@ -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;
|
|
||||||
@@ -8,8 +8,6 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
Input,
|
Input,
|
||||||
Stack,
|
Stack,
|
||||||
DialogTitle,
|
|
||||||
DialogContent,
|
|
||||||
ModalDialog,
|
ModalDialog,
|
||||||
Select,
|
Select,
|
||||||
Option,
|
Option,
|
||||||
@@ -24,10 +22,25 @@ import theme from '/src/theme';
|
|||||||
import Visibility from '@mui/icons-material/Visibility';
|
import Visibility from '@mui/icons-material/Visibility';
|
||||||
import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
||||||
|
|
||||||
const 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 [showPassword, setShowPassword] = useState(false);
|
||||||
const [activeTab, setActiveTab] = useState(0);
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
|
const [showError, setShowError] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isHidden && hostConfig) {
|
if (!isHidden && hostConfig) {
|
||||||
@@ -37,54 +50,84 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
|
|||||||
ip: hostConfig.ip || '',
|
ip: hostConfig.ip || '',
|
||||||
user: hostConfig.user || '',
|
user: hostConfig.user || '',
|
||||||
password: hostConfig.password || '',
|
password: hostConfig.password || '',
|
||||||
rsaKey: hostConfig.rsaKey || '',
|
sshKey: hostConfig.sshKey || '',
|
||||||
|
keyType: hostConfig.keyType || '',
|
||||||
port: hostConfig.port || 22,
|
port: hostConfig.port || 22,
|
||||||
authMethod: hostConfig.password ? 'password' : hostConfig.rsaKey ? 'rsaKey' : 'Select Auth',
|
authMethod: hostConfig.password ? 'password' : hostConfig.sshKey ? 'key' : 'Select Auth',
|
||||||
rememberHost: true,
|
rememberHost: true,
|
||||||
storePassword: !!(hostConfig.password || hostConfig.rsaKey),
|
storePassword: !!(hostConfig.password || hostConfig.sshKey),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [isHidden, hostConfig]);
|
}, [isHidden, hostConfig]);
|
||||||
|
|
||||||
const handleFileChange = (e) => {
|
const handleFileChange = (e) => {
|
||||||
const file = e.target.files[0];
|
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();
|
const reader = new FileReader();
|
||||||
reader.onload = (evt) => {
|
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);
|
reader.readAsText(file);
|
||||||
} else {
|
} 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) => {
|
const handleAuthChange = (newMethod) => {
|
||||||
setForm((prev) => ({
|
setForm((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
authMethod: newMethod
|
authMethod: newMethod,
|
||||||
}));
|
password: "",
|
||||||
};
|
sshKey: "",
|
||||||
|
keyType: "",
|
||||||
const handleStorePasswordChange = (checked) => {
|
|
||||||
setForm((prev) => ({
|
|
||||||
...prev,
|
|
||||||
storePassword: Boolean(checked),
|
|
||||||
password: checked ? prev.password : "",
|
|
||||||
rsaKey: checked ? prev.rsaKey : "",
|
|
||||||
authMethod: checked ? prev.authMethod : "Select Auth"
|
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const isFormValid = () => {
|
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;
|
if (!ip?.trim() || !user?.trim() || !port) return false;
|
||||||
|
|
||||||
const portNum = Number(port);
|
const portNum = Number(port);
|
||||||
if (isNaN(portNum) || portNum < 1 || portNum > 65535) return false;
|
if (isNaN(portNum) || portNum < 1 || portNum > 65535) return false;
|
||||||
|
|
||||||
if (Boolean(storePassword) && authMethod === 'password' && !password?.trim()) return false;
|
if (form.storePassword) {
|
||||||
if (Boolean(storePassword) && authMethod === 'rsaKey' && !rsaKey && !hostConfig?.rsaKey) return false;
|
if (authMethod === 'Select Auth') return false;
|
||||||
if (Boolean(storePassword) && authMethod === 'Select Auth') return false;
|
if (authMethod === 'password' && !password?.trim()) return false;
|
||||||
|
if (authMethod === 'key' && !sshKey?.trim()) return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
@@ -92,18 +135,49 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
|
|||||||
const handleSubmit = async (event) => {
|
const handleSubmit = async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (isLoading) return;
|
if (isLoading) return;
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
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,
|
name: form.name || form.ip,
|
||||||
folder: form.folder,
|
folder: form.folder,
|
||||||
ip: form.ip,
|
ip: form.ip,
|
||||||
user: form.user,
|
user: form.user,
|
||||||
password: form.authMethod === 'password' ? form.password : undefined,
|
|
||||||
rsaKey: form.authMethod === 'rsaKey' ? form.rsaKey : undefined,
|
|
||||||
port: String(form.port),
|
port: String(form.port),
|
||||||
});
|
};
|
||||||
|
|
||||||
|
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 {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -115,11 +189,9 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
|
|||||||
open={!isHidden}
|
open={!isHidden}
|
||||||
onClose={() => !isLoading && setIsEditHostHidden(true)}
|
onClose={() => !isLoading && setIsEditHostHidden(true)}
|
||||||
sx={{
|
sx={{
|
||||||
position: 'fixed',
|
|
||||||
inset: 0,
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
backdropFilter: 'blur(5px)',
|
backdropFilter: 'blur(5px)',
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.2)',
|
backgroundColor: 'rgba(0, 0, 0, 0.2)',
|
||||||
}}
|
}}
|
||||||
@@ -131,7 +203,7 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
|
|||||||
backgroundColor: theme.palette.general.tertiary,
|
backgroundColor: theme.palette.general.tertiary,
|
||||||
borderColor: theme.palette.general.secondary,
|
borderColor: theme.palette.general.secondary,
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
padding: 3,
|
padding: 0,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
maxWidth: '500px',
|
maxWidth: '500px',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -141,134 +213,133 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
|
|||||||
mx: 2,
|
mx: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogTitle sx={{ mb: 2 }}>Edit Host</DialogTitle>
|
{showError && (
|
||||||
<DialogContent>
|
<div style={{
|
||||||
<form onSubmit={handleSubmit}>
|
backgroundColor: "#c53030",
|
||||||
<Tabs
|
color: "white",
|
||||||
value={activeTab}
|
padding: "10px",
|
||||||
onChange={(e, val) => setActiveTab(val)}
|
textAlign: "center",
|
||||||
sx={{
|
borderTopLeftRadius: "10px",
|
||||||
backgroundColor: theme.palette.general.disabled,
|
borderTopRightRadius: "10px"
|
||||||
borderRadius: '8px',
|
}}>
|
||||||
padding: '8px',
|
{errorMessage}
|
||||||
marginBottom: '16px',
|
</div>
|
||||||
width: '100%',
|
)}
|
||||||
}}
|
<Tabs
|
||||||
>
|
value={activeTab}
|
||||||
<TabList
|
onChange={(e, val) => setActiveTab(val)}
|
||||||
sx={{
|
sx={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
gap: 0,
|
mb: 0,
|
||||||
mb: 2,
|
backgroundColor: theme.palette.general.tertiary,
|
||||||
'& button': {
|
}}
|
||||||
flex: 1,
|
>
|
||||||
bgcolor: 'transparent',
|
<TabList
|
||||||
color: theme.palette.text.secondary,
|
sx={{
|
||||||
'&:hover': {
|
width: '100%',
|
||||||
bgcolor: 'rgba(255, 255, 255, 0.1)',
|
gap: 0,
|
||||||
},
|
borderTopLeftRadius: 10,
|
||||||
'&.Mui-selected': {
|
borderTopRightRadius: 10,
|
||||||
bgcolor: theme.palette.general.primary,
|
backgroundColor: theme.palette.general.primary,
|
||||||
color: theme.palette.text.primary,
|
'& button': {
|
||||||
'&:hover': {
|
flex: 1,
|
||||||
bgcolor: theme.palette.general.primary,
|
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>
|
<Tab sx={{ flex: 1 }}>Basic Info</Tab>
|
||||||
</TabList>
|
<Tab sx={{ flex: 1 }}>Connection</Tab>
|
||||||
|
<Tab sx={{ flex: 1 }}>Authentication</Tab>
|
||||||
|
</TabList>
|
||||||
|
|
||||||
<TabPanel value={0}>
|
<div style={{ padding: '24px', backgroundColor: theme.palette.general.tertiary }}>
|
||||||
<Stack spacing={2}>
|
<TabPanel value={0}>
|
||||||
<FormControl>
|
<Stack spacing={2}>
|
||||||
<FormLabel>Host Name</FormLabel>
|
<FormControl>
|
||||||
<Input
|
<FormLabel>Host Name</FormLabel>
|
||||||
value={form.name}
|
<Input
|
||||||
onChange={(e) => setForm((prev) => ({ ...prev, name: e.target.value }))}
|
value={form.name}
|
||||||
sx={{
|
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||||
backgroundColor: theme.palette.general.primary,
|
sx={{
|
||||||
color: theme.palette.text.primary
|
backgroundColor: theme.palette.general.primary,
|
||||||
}}
|
color: theme.palette.text.primary,
|
||||||
/>
|
}}
|
||||||
</FormControl>
|
/>
|
||||||
|
</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>
|
<TabPanel value={1}>
|
||||||
<FormLabel>Folder</FormLabel>
|
<Stack spacing={2}>
|
||||||
<Input
|
<FormControl error={!form.ip}>
|
||||||
value={form.folder}
|
<FormLabel>Host IP</FormLabel>
|
||||||
onChange={(e) => setForm((prev) => ({ ...prev, folder: e.target.value }))}
|
<Input
|
||||||
sx={{
|
value={form.ip}
|
||||||
backgroundColor: theme.palette.general.primary,
|
onChange={(e) => setForm({ ...form, ip: e.target.value })}
|
||||||
color: theme.palette.text.primary
|
required
|
||||||
}}
|
sx={{
|
||||||
/>
|
backgroundColor: theme.palette.general.primary,
|
||||||
</FormControl>
|
color: theme.palette.text.primary,
|
||||||
</Stack>
|
}}
|
||||||
</TabPanel>
|
/>
|
||||||
|
</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}>
|
<TabPanel value={2}>
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
<FormControl error={!form.ip}>
|
{form.storePassword && (
|
||||||
<FormLabel>Host IP</FormLabel>
|
<>
|
||||||
<Input
|
<FormControl error={!form.authMethod || form.authMethod === 'Select Auth'}>
|
||||||
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'}>
|
|
||||||
<FormLabel>Authentication Method</FormLabel>
|
<FormLabel>Authentication Method</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
value={form.authMethod}
|
value={form.authMethod}
|
||||||
@@ -280,104 +351,126 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
|
|||||||
>
|
>
|
||||||
<Option value="Select Auth" disabled>Select Auth</Option>
|
<Option value="Select Auth" disabled>Select Auth</Option>
|
||||||
<Option value="password">Password</Option>
|
<Option value="password">Password</Option>
|
||||||
<Option value="rsaKey">Public Key</Option>
|
<Option value="key">SSH Key</Option>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
|
||||||
|
|
||||||
{form.authMethod === 'password' && form.storePassword && (
|
{form.authMethod === 'password' && (
|
||||||
<FormControl error={!form.password}>
|
<FormControl error={!form.password}>
|
||||||
<FormLabel>Password</FormLabel>
|
<FormLabel>Password</FormLabel>
|
||||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
<Input
|
<Input
|
||||||
type={showPassword ? 'text' : 'password'}
|
type={showPassword ? 'text' : 'password'}
|
||||||
value={form.password}
|
value={form.password}
|
||||||
onChange={(e) => setForm((prev) => ({ ...prev, password: e.target.value }))}
|
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: theme.palette.general.primary,
|
backgroundColor: theme.palette.general.primary,
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
flex: 1
|
flex: 1
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
sx={{
|
sx={{
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
marginLeft: 1
|
marginLeft: 1
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{showPassword ? <VisibilityOff /> : <Visibility />}
|
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{form.authMethod === 'rsaKey' && form.storePassword && (
|
{form.authMethod === 'key' && (
|
||||||
<FormControl error={!form.rsaKey && !hostConfig?.rsaKey}>
|
<Stack spacing={2}>
|
||||||
<FormLabel>Public Key</FormLabel>
|
<FormControl error={!form.sshKey}>
|
||||||
<Button
|
<FormLabel>SSH Key</FormLabel>
|
||||||
component="label"
|
<Button
|
||||||
sx={{
|
component="label"
|
||||||
backgroundColor: theme.palette.general.primary,
|
sx={{
|
||||||
color: theme.palette.text.primary,
|
backgroundColor: theme.palette.general.primary,
|
||||||
width: '100%',
|
color: theme.palette.text.primary,
|
||||||
display: 'flex',
|
width: '100%',
|
||||||
justifyContent: 'center',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
justifyContent: 'center',
|
||||||
height: '40px',
|
alignItems: 'center',
|
||||||
'&:hover': {
|
height: '40px',
|
||||||
backgroundColor: theme.palette.general.disabled,
|
'&:hover': {
|
||||||
},
|
backgroundColor: theme.palette.general.disabled,
|
||||||
}}
|
},
|
||||||
>
|
}}
|
||||||
{form.rsaKey ? 'Change Public Key File' : 'Upload Public Key File'}
|
>
|
||||||
<Input
|
{form.sshKey ? `Change ${form.keyType || 'SSH'} Key File` : 'Upload SSH Key File'}
|
||||||
type="file"
|
<Input
|
||||||
onChange={handleFileChange}
|
type="file"
|
||||||
sx={{ display: 'none' }}
|
onChange={handleFileChange}
|
||||||
/>
|
sx={{ display: 'none' }}
|
||||||
</Button>
|
/>
|
||||||
{hostConfig?.rsaKey && !form.rsaKey && (
|
</Button>
|
||||||
<FormLabel
|
{hostConfig?.sshKey && !form.sshKey && (
|
||||||
sx={{
|
<FormLabel
|
||||||
color: theme.palette.text.secondary,
|
sx={{
|
||||||
fontSize: '0.875rem',
|
color: theme.palette.text.secondary,
|
||||||
mt: 1,
|
fontSize: '0.875rem',
|
||||||
display: 'block',
|
mt: 1,
|
||||||
textAlign: 'center'
|
display: 'block',
|
||||||
}}
|
textAlign: 'center'
|
||||||
>
|
}}
|
||||||
Existing key detected. Upload to replace.
|
>
|
||||||
</FormLabel>
|
Existing {hostConfig.keyType || 'SSH'} key detected. Upload to replace.
|
||||||
)}
|
</FormLabel>
|
||||||
</FormControl>
|
)}
|
||||||
)}
|
</FormControl>
|
||||||
</Stack>
|
</Stack>
|
||||||
</TabPanel>
|
)}
|
||||||
</Tabs>
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button
|
<FormControl>
|
||||||
type="submit"
|
<FormLabel>Store Password</FormLabel>
|
||||||
disabled={!isFormValid() || isLoading}
|
<Checkbox
|
||||||
sx={{
|
checked={Boolean(form.storePassword)}
|
||||||
backgroundColor: theme.palette.general.primary,
|
onChange={(e) => setForm({
|
||||||
color: theme.palette.text.primary,
|
...form,
|
||||||
'&:hover': {
|
storePassword: e.target.checked,
|
||||||
backgroundColor: theme.palette.general.disabled
|
password: e.target.checked ? form.password : "",
|
||||||
},
|
sshKey: e.target.checked ? form.sshKey : "",
|
||||||
'&:disabled': {
|
authMethod: e.target.checked ? form.authMethod : "Select Auth"
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
})}
|
||||||
color: 'rgba(255, 255, 255, 0.3)',
|
sx={{
|
||||||
},
|
color: theme.palette.text.primary,
|
||||||
marginTop: 3,
|
'&.Mui-checked': {
|
||||||
width: '100%',
|
color: theme.palette.text.primary,
|
||||||
height: '40px',
|
},
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
{isLoading ? "Saving..." : "Save Changes"}
|
</FormControl>
|
||||||
</Button>
|
</Stack>
|
||||||
</form>
|
</TabPanel>
|
||||||
</DialogContent>
|
</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>
|
</ModalDialog>
|
||||||
</Modal>
|
</Modal>
|
||||||
</CssVarsProvider>
|
</CssVarsProvider>
|
||||||
@@ -386,11 +479,9 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
|
|||||||
|
|
||||||
EditHostModal.propTypes = {
|
EditHostModal.propTypes = {
|
||||||
isHidden: PropTypes.bool.isRequired,
|
isHidden: PropTypes.bool.isRequired,
|
||||||
form: PropTypes.object.isRequired,
|
hostConfig: PropTypes.object,
|
||||||
setForm: PropTypes.func.isRequired,
|
|
||||||
handleEditHost: PropTypes.func.isRequired,
|
|
||||||
setIsEditHostHidden: PropTypes.func.isRequired,
|
setIsEditHostHidden: PropTypes.func.isRequired,
|
||||||
hostConfig: PropTypes.object
|
handleEditHost: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EditHostModal;
|
export default EditHostModal;
|
||||||
@@ -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;
|
|
||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
Option,
|
Option,
|
||||||
} from '@mui/joy';
|
} from '@mui/joy';
|
||||||
import theme from '/src/theme';
|
import theme from '/src/theme';
|
||||||
import { useState, useEffect } from 'react';
|
import {useEffect, useState} from 'react';
|
||||||
import Visibility from '@mui/icons-material/Visibility';
|
import Visibility from '@mui/icons-material/Visibility';
|
||||||
import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
||||||
|
|
||||||
@@ -26,23 +26,91 @@ const NoAuthenticationModal = ({ isHidden, form, setForm, setIsNoAuthHidden, han
|
|||||||
if (!form.authMethod) {
|
if (!form.authMethod) {
|
||||||
setForm(prev => ({
|
setForm(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
authMethod: 'Select Auth'
|
authMethod: 'Select Auth',
|
||||||
|
password: '',
|
||||||
|
sshKey: '',
|
||||||
|
keyType: '',
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const isFormValid = () => {
|
const isFormValid = () => {
|
||||||
if (!form.authMethod || form.authMethod === 'Select Auth') return false;
|
if (!form.authMethod || form.authMethod === 'Select Auth') return false;
|
||||||
if (form.authMethod === 'rsaKey' && !form.rsaKey) return false;
|
if (form.authMethod === 'sshKey' && !form.sshKey) return false;
|
||||||
if (form.authMethod === 'password' && !form.password) return false;
|
if (form.authMethod === 'password' && !form.password) return false;
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = (event) => {
|
const handleSubmit = (e) => {
|
||||||
event.preventDefault();
|
e.preventDefault();
|
||||||
if (isFormValid()) {
|
e.stopPropagation();
|
||||||
handleAuthSubmit(form);
|
|
||||||
setForm({ authMethod: 'Select Auth', password: '', rsaKey: '' });
|
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>
|
<FormLabel>Authentication Method</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
value={form.authMethod || 'Select Auth'}
|
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={{
|
sx={{
|
||||||
backgroundColor: theme.palette.general.primary,
|
backgroundColor: theme.palette.general.primary,
|
||||||
color: theme.palette.text.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="Select Auth" disabled>Select Auth</Option>
|
||||||
<Option value="password">Password</Option>
|
<Option value="password">Password</Option>
|
||||||
<Option value="rsaKey">Public Key</Option>
|
<Option value="sshKey">SSH Key</Option >
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
@@ -102,9 +176,9 @@ const NoAuthenticationModal = ({ isHidden, form, setForm, setIsNoAuthHidden, han
|
|||||||
<FormLabel>Password</FormLabel>
|
<FormLabel>Password</FormLabel>
|
||||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
<Input
|
<Input
|
||||||
type={showPassword ? 'text' : 'password'}
|
type={showPassword ? "text" : "password"}
|
||||||
value={form.password || ''}
|
value={form.password || ''}
|
||||||
onChange={(e) => setForm(prev => ({ ...prev, password: e.target.value }))}
|
onChange={(e) => setForm({...form, password: e.target.value})}
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: theme.palette.general.primary,
|
backgroundColor: theme.palette.general.primary,
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
@@ -115,7 +189,10 @@ const NoAuthenticationModal = ({ isHidden, form, setForm, setIsNoAuthHidden, han
|
|||||||
onClick={() => setShowPassword(!showPassword)}
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
sx={{
|
sx={{
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
marginLeft: 1
|
marginLeft: 1,
|
||||||
|
'&:disabled': {
|
||||||
|
opacity: 0.5,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{showPassword ? <VisibilityOff /> : <Visibility />}
|
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||||
@@ -124,41 +201,34 @@ const NoAuthenticationModal = ({ isHidden, form, setForm, setIsNoAuthHidden, han
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{form.authMethod === 'rsaKey' && (
|
{form.authMethod === 'sshKey' && (
|
||||||
<FormControl error={!form.rsaKey}>
|
<Stack spacing={2}>
|
||||||
<FormLabel>Public Key</FormLabel>
|
<FormControl error={!form.sshKey}>
|
||||||
<Button
|
<FormLabel>SSH Key</FormLabel>
|
||||||
component="label"
|
<Button
|
||||||
sx={{
|
component="label"
|
||||||
backgroundColor: theme.palette.general.primary,
|
sx={{
|
||||||
color: theme.palette.text.primary,
|
backgroundColor: theme.palette.general.primary,
|
||||||
width: '100%',
|
color: theme.palette.text.primary,
|
||||||
display: 'flex',
|
width: '100%',
|
||||||
justifyContent: 'center',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
justifyContent: 'center',
|
||||||
height: '40px',
|
alignItems: 'center',
|
||||||
'&:hover': {
|
height: '40px',
|
||||||
backgroundColor: theme.palette.general.disabled,
|
'&: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);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
sx={{ display: 'none' }}
|
>
|
||||||
/>
|
{form.sshKey ? `Change ${form.keyType || 'SSH'} Key File` : 'Upload SSH Key File'}
|
||||||
</Button>
|
<Input
|
||||||
</FormControl>
|
type="file"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
sx={{ display: 'none' }}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -71,6 +71,10 @@ export default function ProfileModal({
|
|||||||
>
|
>
|
||||||
Delete Account
|
Delete Account
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<div className="text-center text-xs text-gray-400">
|
||||||
|
v0.2.1
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
5
src/other/eventBus.jsx
Normal file
5
src/other/eventBus.jsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import mitt from "mitt";
|
||||||
|
|
||||||
|
const eventBus = mitt();
|
||||||
|
|
||||||
|
export default eventBus;
|
||||||
Reference in New Issue
Block a user