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