diff --git a/.github/workflows/electron.yml b/.github/workflows/electron.yml index bd1a5b4c..b0dcec0e 100644 --- a/.github/workflows/electron.yml +++ b/.github/workflows/electron.yml @@ -27,7 +27,7 @@ on: jobs: build-windows: runs-on: windows-latest - if: github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'windows' || github.event.inputs.build_type == '' + if: (github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'windows' || github.event.inputs.build_type == '') && github.event.inputs.artifact_destination != 'submit' permissions: contents: write @@ -72,10 +72,6 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: npm run build && npx electron-builder --win --x64 --ia32 - - name: List release files - run: | - dir release - - name: Upload Windows x64 NSIS Installer uses: actions/upload-artifact@v4 if: hashFiles('release/termix_windows_x64_nsis.exe') != '' && github.event.inputs.artifact_destination != 'none' @@ -136,7 +132,7 @@ jobs: build-linux: runs-on: blacksmith-4vcpu-ubuntu-2404 - if: github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'linux' || github.event.inputs.build_type == '' + if: (github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'linux' || github.event.inputs.build_type == '') && github.event.inputs.artifact_destination != 'submit' permissions: contents: write @@ -199,17 +195,6 @@ jobs: cd .. - - name: List release files - run: | - ls -la release/ - - - name: Debug electron-builder output - if: always() - run: | - if [ -f "release/builder-debug.yml" ]; then - cat release/builder-debug.yml - fi - - name: Upload Linux x64 AppImage uses: actions/upload-artifact@v4 if: hashFiles('release/termix_linux_x64_appimage.AppImage') != '' && github.event.inputs.artifact_destination != 'none' @@ -282,6 +267,93 @@ jobs: path: release/termix_linux_armv7l_portable.tar.gz retention-days: 30 + - name: Install Flatpak builder and dependencies + run: | + sudo apt-get update + sudo apt-get install -y flatpak flatpak-builder imagemagick + + - name: Add Flathub repository + run: | + sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + + - name: Install Flatpak runtime and SDK + run: | + sudo flatpak install -y flathub org.freedesktop.Platform//24.08 + sudo flatpak install -y flathub org.freedesktop.Sdk//24.08 + sudo flatpak install -y flathub org.electronjs.Electron2.BaseApp//24.08 + + - name: Get version for Flatpak + id: flatpak-version + run: | + VERSION=$(node -p "require('./package.json').version") + RELEASE_DATE=$(date +%Y-%m-%d) + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "release_date=$RELEASE_DATE" >> $GITHUB_OUTPUT + + - name: Prepare Flatpak files + run: | + VERSION="${{ steps.flatpak-version.outputs.version }}" + RELEASE_DATE="${{ steps.flatpak-version.outputs.release_date }}" + + CHECKSUM_X64=$(sha256sum "release/termix_linux_x64_appimage.AppImage" | awk '{print $1}') + CHECKSUM_ARM64=$(sha256sum "release/termix_linux_arm64_appimage.AppImage" | awk '{print $1}') + + mkdir -p flatpak-build + cp flatpak/com.karmaa.termix.yml flatpak-build/ + cp flatpak/com.karmaa.termix.desktop flatpak-build/ + cp flatpak/com.karmaa.termix.metainfo.xml flatpak-build/ + cp public/icon.svg flatpak-build/com.karmaa.termix.svg + convert public/icon.png -resize 256x256 flatpak-build/icon-256.png + convert public/icon.png -resize 128x128 flatpak-build/icon-128.png + + cd flatpak-build + sed -i "s|https://github.com/Termix-SSH/Termix/releases/download/release-VERSION_PLACEHOLDER-tag/termix_linux_x64_appimage.AppImage|file://$(realpath ../release/termix_linux_x64_appimage.AppImage)|g" com.karmaa.termix.yml + sed -i "s|https://github.com/Termix-SSH/Termix/releases/download/release-VERSION_PLACEHOLDER-tag/termix_linux_arm64_appimage.AppImage|file://$(realpath ../release/termix_linux_arm64_appimage.AppImage)|g" com.karmaa.termix.yml + sed -i "s/CHECKSUM_X64_PLACEHOLDER/$CHECKSUM_X64/g" com.karmaa.termix.yml + sed -i "s/CHECKSUM_ARM64_PLACEHOLDER/$CHECKSUM_ARM64/g" com.karmaa.termix.yml + sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" com.karmaa.termix.metainfo.xml + sed -i "s/DATE_PLACEHOLDER/$RELEASE_DATE/g" com.karmaa.termix.metainfo.xml + + - name: Build Flatpak bundle + run: | + cd flatpak-build + flatpak-builder --repo=repo --force-clean --disable-rofiles-fuse build-dir com.karmaa.termix.yml + + # Determine the architecture + ARCH=$(uname -m) + if [ "$ARCH" = "x86_64" ]; then + FLATPAK_ARCH="x86_64" + elif [ "$ARCH" = "aarch64" ]; then + FLATPAK_ARCH="aarch64" + else + FLATPAK_ARCH="$ARCH" + fi + + # Build bundle for the current architecture + flatpak build-bundle repo ../release/termix_linux_flatpak.flatpak com.karmaa.termix --runtime-repo=https://flathub.org/repo/flathub.flatpakrepo + + - name: Create flatpakref file + run: | + VERSION="${{ steps.flatpak-version.outputs.version }}" + cp flatpak/com.karmaa.termix.flatpakref release/ + sed -i "s|VERSION_PLACEHOLDER|release-${VERSION}-tag|g" release/com.karmaa.termix.flatpakref + + - name: Upload Flatpak bundle + uses: actions/upload-artifact@v4 + if: hashFiles('release/termix_linux_flatpak.flatpak') != '' && github.event.inputs.artifact_destination != 'none' + with: + name: termix_linux_flatpak + path: release/termix_linux_flatpak.flatpak + retention-days: 30 + + - name: Upload Flatpakref + uses: actions/upload-artifact@v4 + if: hashFiles('release/com.karmaa.termix.flatpakref') != '' && github.event.inputs.artifact_destination != 'none' + with: + name: termix_linux_flatpakref + path: release/com.karmaa.termix.flatpakref + retention-days: 30 + build-macos: runs-on: macos-latest if: github.event.inputs.build_type == 'macos' || github.event.inputs.build_type == 'all' @@ -425,11 +497,6 @@ jobs: export GH_TOKEN="${{ secrets.GITHUB_TOKEN }}" npx electron-builder --mac dmg --universal --x64 --arm64 --publish never - - name: List release directory - if: steps.check_certs.outputs.has_certs == 'true' - run: | - ls -R release/ || echo "Release directory not found" - - name: Upload macOS MAS PKG if: steps.check_certs.outputs.has_certs == 'true' && hashFiles('release/termix_macos_universal_mas.pkg') != '' && (github.event.inputs.artifact_destination == 'file' || github.event.inputs.artifact_destination == 'release' || github.event.inputs.artifact_destination == 'submit') uses: actions/upload-artifact@v4 @@ -463,42 +530,51 @@ jobs: path: release/termix_macos_arm64_dmg.dmg retention-days: 30 - - name: Check for App Store Connect API credentials - if: steps.check_certs.outputs.has_certs == 'true' - id: check_asc_creds + - name: Get version for Homebrew + id: homebrew-version run: | - if [ -n "${{ secrets.APPLE_KEY_ID }}" ] && [ -n "${{ secrets.APPLE_ISSUER_ID }}" ] && [ -n "${{ secrets.APPLE_KEY_CONTENT }}" ]; then - echo "has_credentials=true" >> $GITHUB_OUTPUT - fi + VERSION=$(node -p "require('./package.json').version") + echo "version=$VERSION" >> $GITHUB_OUTPUT - - name: Setup Ruby for Fastlane - if: steps.check_asc_creds.outputs.has_credentials == 'true' && github.event.inputs.artifact_destination == 'submit' - uses: ruby/setup-ruby@v1 + - name: Generate Homebrew Cask + if: hashFiles('release/termix_macos_universal_dmg.dmg') != '' && (github.event.inputs.artifact_destination == 'file' || github.event.inputs.artifact_destination == 'release') + run: | + VERSION="${{ steps.homebrew-version.outputs.version }}" + DMG_PATH="release/termix_macos_universal_dmg.dmg" + + CHECKSUM=$(shasum -a 256 "$DMG_PATH" | awk '{print $1}') + + mkdir -p homebrew-generated + cp homebrew/termix.rb homebrew-generated/termix.rb + + sed -i '' "s/VERSION_PLACEHOLDER/$VERSION/g" homebrew-generated/termix.rb + sed -i '' "s/CHECKSUM_PLACEHOLDER/$CHECKSUM/g" homebrew-generated/termix.rb + sed -i '' "s|version \".*\"|version \"$VERSION\"|g" homebrew-generated/termix.rb + sed -i '' "s|sha256 \".*\"|sha256 \"$CHECKSUM\"|g" homebrew-generated/termix.rb + sed -i '' "s|release-[0-9.]*-tag|release-$VERSION-tag|g" homebrew-generated/termix.rb + + - name: Upload Homebrew Cask as artifact + uses: actions/upload-artifact@v4 + if: hashFiles('homebrew-generated/termix.rb') != '' && github.event.inputs.artifact_destination == 'file' with: - ruby-version: "3.2" - bundler-cache: false + name: termix_macos_homebrew_cask + path: homebrew-generated/termix.rb + retention-days: 30 - - name: Install Fastlane - if: steps.check_asc_creds.outputs.has_credentials == 'true' && github.event.inputs.artifact_destination == 'submit' + - name: Upload Homebrew Cask to release + if: hashFiles('homebrew-generated/termix.rb') != '' && github.event.inputs.artifact_destination == 'release' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - gem install fastlane -N + VERSION="${{ steps.homebrew-version.outputs.version }}" + RELEASE_TAG="release-$VERSION-tag" - - name: Deploy to App Store Connect (TestFlight) - if: steps.check_asc_creds.outputs.has_credentials == 'true' && github.event.inputs.artifact_destination == 'submit' - run: | - PKG_FILE=$(find release -name "*.pkg" -type f | head -n 1) - if [ -z "$PKG_FILE" ]; then + gh release list --repo ${{ github.repository }} --limit 100 | grep -q "$RELEASE_TAG" || { + echo "Release $RELEASE_TAG not found" exit 1 - fi + } - mkdir -p ~/private_keys - echo "${{ secrets.APPLE_KEY_CONTENT }}" | base64 --decode > ~/private_keys/AuthKey_${{ secrets.APPLE_KEY_ID }}.p8 - - xcrun altool --upload-app -f "$PKG_FILE" \ - --type macos \ - --apiKey "${{ secrets.APPLE_KEY_ID }}" \ - --apiIssuer "${{ secrets.APPLE_ISSUER_ID }}" - continue-on-error: true + gh release upload "$RELEASE_TAG" homebrew-generated/termix.rb --repo ${{ github.repository }} --clobber - name: Clean up keychains if: always() @@ -509,7 +585,6 @@ jobs: submit-to-chocolatey: runs-on: windows-latest if: github.event.inputs.artifact_destination == 'submit' - needs: [build-windows] permissions: contents: read @@ -525,20 +600,25 @@ jobs: $VERSION = (Get-Content package.json | ConvertFrom-Json).version echo "version=$VERSION" >> $env:GITHUB_OUTPUT - - name: Download Windows x64 MSI artifact - uses: actions/download-artifact@v4 - with: - name: termix_windows_x64_msi - path: artifact - - - name: Get MSI file info + - name: Download and prepare MSI info from public release id: msi-info run: | $VERSION = "${{ steps.package-version.outputs.version }}" - $MSI_FILE = Get-ChildItem -Path artifact -Filter "*.msi" | Select-Object -First 1 - $MSI_NAME = $MSI_FILE.Name - $CHECKSUM = (Get-FileHash -Path $MSI_FILE.FullName -Algorithm SHA256).Hash + $MSI_NAME = "termix_windows_x64_msi.msi" + $DOWNLOAD_URL = "https://github.com/Termix-SSH/Termix/releases/download/release-$($VERSION)-tag/$($MSI_NAME)" + Write-Host "Downloading from $DOWNLOAD_URL" + New-Item -ItemType Directory -Force -Path "release_asset" + $DOWNLOAD_PATH = "release_asset\$MSI_NAME" + + try { + Invoke-WebRequest -Uri $DOWNLOAD_URL -OutFile $DOWNLOAD_PATH -UseBasicParsing + } catch { + Write-Error "Failed to download MSI from $DOWNLOAD_URL. Please ensure the release and asset exist." + exit 1 + } + + $CHECKSUM = (Get-FileHash -Path $DOWNLOAD_PATH -Algorithm SHA256).Hash echo "msi_name=$MSI_NAME" >> $env:GITHUB_OUTPUT echo "checksum=$CHECKSUM" >> $env:GITHUB_OUTPUT @@ -610,7 +690,7 @@ jobs: submit-to-flatpak: runs-on: ubuntu-latest if: github.event.inputs.artifact_destination == 'submit' - needs: [build-linux] + needs: [] permissions: contents: read @@ -628,30 +708,27 @@ jobs: echo "version=$VERSION" >> $GITHUB_OUTPUT echo "release_date=$RELEASE_DATE" >> $GITHUB_OUTPUT - - name: Download Linux x64 AppImage artifact - uses: actions/download-artifact@v4 - with: - name: termix_linux_x64_appimage - path: artifact-x64 - - - name: Download Linux arm64 AppImage artifact - uses: actions/download-artifact@v4 - with: - name: termix_linux_arm64_appimage - path: artifact-arm64 - - - name: Get AppImage file info + - name: Download and prepare AppImage info from public release id: appimage-info run: | VERSION="${{ steps.package-version.outputs.version }}" + mkdir -p release_assets - APPIMAGE_X64_FILE=$(find artifact-x64 -name "*.AppImage" -type f | head -n 1) - APPIMAGE_X64_NAME=$(basename "$APPIMAGE_X64_FILE") - CHECKSUM_X64=$(sha256sum "$APPIMAGE_X64_FILE" | awk '{print $1}') + APPIMAGE_X64_NAME="termix_linux_x64_appimage.AppImage" + URL_X64="https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$APPIMAGE_X64_NAME" + PATH_X64="release_assets/$APPIMAGE_X64_NAME" + echo "Downloading x64 AppImage from $URL_X64" + curl -L -o "$PATH_X64" "$URL_X64" + chmod +x "$PATH_X64" + CHECKSUM_X64=$(sha256sum "$PATH_X64" | awk '{print $1}') - APPIMAGE_ARM64_FILE=$(find artifact-arm64 -name "*.AppImage" -type f | head -n 1) - APPIMAGE_ARM64_NAME=$(basename "$APPIMAGE_ARM64_FILE") - CHECKSUM_ARM64=$(sha256sum "$APPIMAGE_ARM64_FILE" | awk '{print $1}') + APPIMAGE_ARM64_NAME="termix_linux_arm64_appimage.AppImage" + URL_ARM64="https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$APPIMAGE_ARM64_NAME" + PATH_ARM64="release_assets/$APPIMAGE_ARM64_NAME" + echo "Downloading arm64 AppImage from $URL_ARM64" + curl -L -o "$PATH_ARM64" "$URL_ARM64" + chmod +x "$PATH_ARM64" + CHECKSUM_ARM64=$(sha256sum "$PATH_ARM64" | awk '{print $1}') echo "appimage_x64_name=$APPIMAGE_X64_NAME" >> $GITHUB_OUTPUT echo "checksum_x64=$CHECKSUM_X64" >> $GITHUB_OUTPUT @@ -690,10 +767,6 @@ jobs: sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" flatpak-submission/com.karmaa.termix.metainfo.xml sed -i "s/DATE_PLACEHOLDER/$RELEASE_DATE/g" flatpak-submission/com.karmaa.termix.metainfo.xml - - name: List submission files - run: | - ls -la flatpak-submission/ - - name: Upload Flatpak submission as artifact uses: actions/upload-artifact@v4 with: @@ -704,7 +777,7 @@ jobs: submit-to-homebrew: runs-on: macos-latest if: github.event.inputs.artifact_destination == 'submit' - needs: [build-macos] + needs: [] permissions: contents: read @@ -720,19 +793,19 @@ jobs: VERSION=$(node -p "require('./package.json').version") echo "version=$VERSION" >> $GITHUB_OUTPUT - - name: Download macOS Universal DMG artifact - uses: actions/download-artifact@v4 - with: - name: termix_macos_universal_dmg - path: artifact - - - name: Get DMG file info + - name: Download and prepare DMG info from public release id: dmg-info run: | VERSION="${{ steps.package-version.outputs.version }}" - DMG_FILE=$(find artifact -name "*.dmg" -type f | head -n 1) - DMG_NAME=$(basename "$DMG_FILE") - CHECKSUM=$(shasum -a 256 "$DMG_FILE" | awk '{print $1}') + DMG_NAME="termix_macos_universal_dmg.dmg" + URL="https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$DMG_NAME" + + mkdir -p release_asset + PATH="release_asset/$DMG_NAME" + echo "Downloading DMG from $URL" + curl -L -o "$PATH" "$URL" + + CHECKSUM=$(shasum -a 256 "$PATH" | awk '{print $1}') echo "dmg_name=$DMG_NAME" >> $GITHUB_OUTPUT echo "checksum=$CHECKSUM" >> $GITHUB_OUTPUT @@ -752,16 +825,8 @@ jobs: - name: Verify Cask syntax run: | - if ! command -v brew &> /dev/null; then - /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" - fi - ruby -c homebrew-submission/Casks/t/termix.rb - - name: List submission files - run: | - find homebrew-submission -type f - - name: Upload Homebrew submission as artifact uses: actions/upload-artifact@v4 with: @@ -789,10 +854,6 @@ jobs: env: GH_TOKEN: ${{ github.token }} - - name: Display artifact structure - run: | - ls -R artifacts/ - - name: Upload artifacts to latest release run: | cd artifacts @@ -808,3 +869,130 @@ jobs: done env: GH_TOKEN: ${{ github.token }} + + submit-to-testflight: + runs-on: macos-latest + if: github.event.inputs.artifact_destination == 'submit' + needs: [] + permissions: + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + fetch-depth: 1 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + + - name: Install dependencies + run: | + for i in 1 2 3; + do + if npm ci; then + break + else + if [ $i -eq 3 ]; then + exit 1 + fi + sleep 10 + fi + done + npm install --force @rollup/rollup-darwin-arm64 + npm install dmg-license + + - name: Check for Code Signing Certificates + id: check_certs + run: | + if [ -n "${{ secrets.MAC_BUILD_CERTIFICATE_BASE64 }}" ] && [ -n "${{ secrets.MAC_P12_PASSWORD }}" ]; then + echo "has_certs=true" >> $GITHUB_OUTPUT + fi + + - name: Import Code Signing Certificates + if: steps.check_certs.outputs.has_certs == 'true' + env: + MAC_BUILD_CERTIFICATE_BASE64: ${{ secrets.MAC_BUILD_CERTIFICATE_BASE64 }} + MAC_INSTALLER_CERTIFICATE_BASE64: ${{ secrets.MAC_INSTALLER_CERTIFICATE_BASE64 }} + MAC_P12_PASSWORD: ${{ secrets.MAC_P12_PASSWORD }} + MAC_KEYCHAIN_PASSWORD: ${{ secrets.MAC_KEYCHAIN_PASSWORD }} + run: | + APP_CERT_PATH=$RUNNER_TEMP/app_certificate.p12 + INSTALLER_CERT_PATH=$RUNNER_TEMP/installer_certificate.p12 + KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db + + echo -n "$MAC_BUILD_CERTIFICATE_BASE64" | base64 --decode -o $APP_CERT_PATH + + if [ -n "$MAC_INSTALLER_CERTIFICATE_BASE64" ]; then + echo -n "$MAC_INSTALLER_CERTIFICATE_BASE64" | base64 --decode -o $INSTALLER_CERT_PATH + fi + + security create-keychain -p "$MAC_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security set-keychain-settings -lut 21600 $KEYCHAIN_PATH + security unlock-keychain -p "$MAC_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + + security import $APP_CERT_PATH -P "$MAC_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + + if [ -f "$INSTALLER_CERT_PATH" ]; then + security import $INSTALLER_CERT_PATH -P "$MAC_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + fi + + security list-keychain -d user -s $KEYCHAIN_PATH + + security find-identity -v -p codesigning $KEYCHAIN_PATH + + - name: Build macOS App Store Package + if: steps.check_certs.outputs.has_certs == 'true' + env: + ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES: true + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + CURRENT_VERSION=$(node -p "require('./package.json').version") + BUILD_VERSION="${{ github.run_number }}" + + npm run build && npx electron-builder --mac mas --universal --config.buildVersion="$BUILD_VERSION" + + - name: Check for App Store Connect API credentials + id: check_asc_creds + run: | + if [ -n "${{ secrets.APPLE_KEY_ID }}" ] && [ -n "${{ secrets.APPLE_ISSUER_ID }}" ] && [ -n "${{ secrets.APPLE_KEY_CONTENT }}" ]; then + echo "has_credentials=true" >> $GITHUB_OUTPUT + fi + + - name: Setup Ruby for Fastlane + if: steps.check_asc_creds.outputs.has_credentials == 'true' + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.2" + bundler-cache: false + + - name: Install Fastlane + if: steps.check_asc_creds.outputs.has_credentials == 'true' + run: | + gem install fastlane -N + + - name: Deploy to App Store Connect (TestFlight) + if: steps.check_asc_creds.outputs.has_credentials == 'true' + run: | + PKG_FILE=$(find artifact-mas -name "*.pkg" -type f | head -n 1) + if [ -z "$PKG_FILE" ]; then + echo "PKG file not found, exiting." + exit 1 + fi + + mkdir -p ~/private_keys + echo "${{ secrets.APPLE_KEY_CONTENT }}" | base64 --decode > ~/private_keys/AuthKey_${{ secrets.APPLE_KEY_ID }}.p8 + + xcrun altool --upload-app -f "$PKG_FILE" \ + --type macos \ + --apiKey "${{ secrets.APPLE_KEY_ID }}" \ + --apiIssuer "${{ secrets.APPLE_ISSUER_ID }}" + continue-on-error: true + + - name: Clean up keychains + if: always() + run: | + security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true diff --git a/docker/nginx-https.conf b/docker/nginx-https.conf index 5e6126bf..0f675a6f 100644 --- a/docker/nginx-https.conf +++ b/docker/nginx-https.conf @@ -93,6 +93,15 @@ http { proxy_set_header X-Forwarded-Proto $scheme; } + location ~ ^/rbac(/.*)?$ { + proxy_pass http://127.0.0.1:30001; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + location ~ ^/credentials(/.*)?$ { proxy_pass http://127.0.0.1:30001; proxy_http_version 1.1; @@ -303,6 +312,42 @@ http { proxy_set_header X-Forwarded-Proto $scheme; } + location ~ ^/docker(/.*)?$ { + proxy_pass http://127.0.0.1:30007; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_connect_timeout 60s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; + } + + location /docker/console/ { + proxy_pass http://127.0.0.1:30008/; + proxy_http_version 1.1; + + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_read_timeout 86400s; + proxy_send_timeout 86400s; + proxy_connect_timeout 10s; + + proxy_buffering off; + proxy_request_buffering off; + + proxy_next_upstream error timeout invalid_header http_500 http_502 http_503; + } + error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; diff --git a/docker/nginx.conf b/docker/nginx.conf index db5546f0..4b72f21c 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -90,6 +90,15 @@ http { proxy_set_header X-Forwarded-Proto $scheme; } + location ~ ^/rbac(/.*)?$ { + proxy_pass http://127.0.0.1:30001; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + location ~ ^/credentials(/.*)?$ { proxy_pass http://127.0.0.1:30001; proxy_http_version 1.1; @@ -300,6 +309,42 @@ http { proxy_set_header X-Forwarded-Proto $scheme; } + location ~ ^/docker(/.*)?$ { + proxy_pass http://127.0.0.1:30007; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_connect_timeout 60s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; + } + + location /docker/console/ { + proxy_pass http://127.0.0.1:30008/; + proxy_http_version 1.1; + + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_read_timeout 86400s; + proxy_send_timeout 86400s; + proxy_connect_timeout 10s; + + proxy_buffering off; + proxy_request_buffering off; + + proxy_next_upstream error timeout invalid_header http_500 http_502 http_503; + } + error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; diff --git a/electron-builder.json b/electron-builder.json index 218153e1..8137f73d 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -124,5 +124,6 @@ "ITSAppUsesNonExemptEncryption": false, "NSAppleEventsUsageDescription": "Termix needs access to control other applications for terminal operations." } - } + }, + "generateUpdatesFilesForAllChannels": true } diff --git a/electron/main.cjs b/electron/main.cjs index 97ced567..9cd63d58 100644 --- a/electron/main.cjs +++ b/electron/main.cjs @@ -11,13 +11,11 @@ const fs = require("fs"); const os = require("os"); if (process.platform === "linux") { - app.commandLine.appendSwitch("--no-sandbox"); - app.commandLine.appendSwitch("--disable-setuid-sandbox"); - app.commandLine.appendSwitch("--disable-dev-shm-usage"); + // Enable Ozone platform auto-detection for Wayland/X11 support + app.commandLine.appendSwitch("--ozone-platform-hint=auto"); - app.disableHardwareAcceleration(); - app.commandLine.appendSwitch("--disable-gpu"); - app.commandLine.appendSwitch("--disable-gpu-compositing"); + // Enable hardware video decoding if available + app.commandLine.appendSwitch("--enable-features=VaapiVideoDecoder"); } app.commandLine.appendSwitch("--ignore-certificate-errors"); diff --git a/flatpak/com.karmaa.termix.desktop b/flatpak/com.karmaa.termix.desktop index 3aabfd06..59d27c13 100644 --- a/flatpak/com.karmaa.termix.desktop +++ b/flatpak/com.karmaa.termix.desktop @@ -1,7 +1,7 @@ [Desktop Entry] Name=Termix Comment=Web-based server management platform with SSH terminal, tunneling, and file editing -Exec=termix %U +Exec=run.sh %U Icon=com.karmaa.termix Terminal=false Type=Application diff --git a/flatpak/com.karmaa.termix.flatpakref b/flatpak/com.karmaa.termix.flatpakref new file mode 100644 index 00000000..7d2e9892 --- /dev/null +++ b/flatpak/com.karmaa.termix.flatpakref @@ -0,0 +1,12 @@ +[Flatpak Ref] +Name=Termix +Branch=stable +Title=Termix - SSH Server Management Platform +IsRuntime=false +Url=https://github.com/Termix-SSH/Termix/releases/download/VERSION_PLACEHOLDER/termix_linux_flatpak.flatpak +GPGKey= +RuntimeRepo=https://flathub.org/repo/flathub.flatpakrepo +Comment=Web-based server management platform with SSH terminal, tunneling, and file editing +Description=Termix is an open-source, forever-free, self-hosted all-in-one server management platform. It provides SSH terminal access, tunneling capabilities, and remote file management. +Icon=https://raw.githubusercontent.com/Termix-SSH/Termix/main/public/icon.png +Homepage=https://github.com/Termix-SSH/Termix diff --git a/flatpak/com.karmaa.termix.metainfo.xml b/flatpak/com.karmaa.termix.metainfo.xml index 0c3c6895..335d902c 100644 --- a/flatpak/com.karmaa.termix.metainfo.xml +++ b/flatpak/com.karmaa.termix.metainfo.xml @@ -5,7 +5,7 @@ Web-based server management platform with SSH terminal, tunneling, and file editing CC0-1.0 - GPL-3.0-or-later + Apache-2.0 bugattiguy527 diff --git a/flatpak/com.karmaa.termix.yml b/flatpak/com.karmaa.termix.yml index 4405a10f..7b67c0e7 100644 --- a/flatpak/com.karmaa.termix.yml +++ b/flatpak/com.karmaa.termix.yml @@ -1,10 +1,10 @@ app-id: com.karmaa.termix runtime: org.freedesktop.Platform -runtime-version: "23.08" +runtime-version: "24.08" sdk: org.freedesktop.Sdk base: org.electronjs.Electron2.BaseApp -base-version: "23.08" -command: termix +base-version: "24.08" +command: run.sh separate-locales: false finish-args: @@ -16,8 +16,11 @@ finish-args: - --device=dri - --filesystem=home - --socket=ssh-auth - - --talk-name=org.freedesktop.Notifications + - --socket=session-bus - --talk-name=org.freedesktop.secrets + - --env=ELECTRON_TRASH=gio + - --env=XCURSOR_PATH=/run/host/user-share/icons:/run/host/share/icons + - --env=ELECTRON_OZONE_PLATFORM_HINT=auto modules: - name: termix @@ -30,6 +33,21 @@ modules: - cp -r squashfs-root/resources /app/bin/ - cp -r squashfs-root/locales /app/bin/ || true + - cp squashfs-root/*.so /app/bin/ || true + - cp squashfs-root/*.pak /app/bin/ || true + - cp squashfs-root/*.bin /app/bin/ || true + - cp squashfs-root/*.dat /app/bin/ || true + - cp squashfs-root/*.json /app/bin/ || true + + - | + cat > run.sh << 'EOF' + #!/bin/bash + export TMPDIR="$XDG_RUNTIME_DIR/app/$FLATPAK_ID" + exec zypak-wrapper /app/bin/termix "$@" + EOF + - chmod +x run.sh + - install -Dm755 run.sh /app/bin/run.sh + - install -Dm644 com.karmaa.termix.desktop /app/share/applications/com.karmaa.termix.desktop - install -Dm644 com.karmaa.termix.metainfo.xml /app/share/metainfo/com.karmaa.termix.metainfo.xml @@ -40,14 +58,14 @@ modules: sources: - type: file - url: https://github.com/Termix-SSH/Termix/releases/download/release-VERSION_PLACEHOLDER-tag/termix_linux_x64_VERSION_PLACEHOLDER_appimage.AppImage + url: https://github.com/Termix-SSH/Termix/releases/download/release-VERSION_PLACEHOLDER-tag/termix_linux_x64_appimage.AppImage sha256: CHECKSUM_X64_PLACEHOLDER dest-filename: termix.AppImage only-arches: - x86_64 - type: file - url: https://github.com/Termix-SSH/Termix/releases/download/release-VERSION_PLACEHOLDER-tag/termix_linux_arm64_VERSION_PLACEHOLDER_appimage.AppImage + url: https://github.com/Termix-SSH/Termix/releases/download/release-VERSION_PLACEHOLDER-tag/termix_linux_arm64_appimage.AppImage sha256: CHECKSUM_ARM64_PLACEHOLDER dest-filename: termix.AppImage only-arches: diff --git a/flatpak/prepare-flatpak.sh b/flatpak/prepare-flatpak.sh deleted file mode 100644 index 05162b64..00000000 --- a/flatpak/prepare-flatpak.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash -set -e - -VERSION="$1" -CHECKSUM="$2" -RELEASE_DATE="$3" - -if [ -z "$VERSION" ] || [ -z "$CHECKSUM" ] || [ -z "$RELEASE_DATE" ]; then - echo "Usage: $0 " - echo "Example: $0 1.8.0 abc123... 2025-10-26" - exit 1 -fi - -echo "Preparing Flatpak submission for version $VERSION" - -cp public/icon.svg flatpak/com.karmaa.termix.svg -echo "✓ Copied SVG icon" - -if command -v convert &> /dev/null; then - convert public/icon.png -resize 256x256 flatpak/icon-256.png - convert public/icon.png -resize 128x128 flatpak/icon-128.png - echo "✓ Generated PNG icons" -else - cp public/icon.png flatpak/icon-256.png - cp public/icon.png flatpak/icon-128.png - echo "⚠ ImageMagick not found, using original icon" -fi - -sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" flatpak/com.karmaa.termix.yml -sed -i "s/CHECKSUM_PLACEHOLDER/$CHECKSUM/g" flatpak/com.karmaa.termix.yml -echo "✓ Updated manifest with version $VERSION" - -sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" flatpak/com.karmaa.termix.metainfo.xml -sed -i "s/DATE_PLACEHOLDER/$RELEASE_DATE/g" flatpak/com.karmaa.termix.metainfo.xml diff --git a/package-lock.json b/package-lock.json index 4658af92..8c64a2de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@hookform/resolvers": "^5.1.1", "@monaco-editor/react": "^4.7.0", "@radix-ui/react-accordion": "^1.2.11", + "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.15", @@ -26,7 +27,7 @@ "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slider": "^1.3.6", - "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-switch": "^1.2.5", "@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-tooltip": "^1.2.8", @@ -3156,6 +3157,52 @@ } } }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", @@ -3265,6 +3312,24 @@ } } }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", @@ -3331,6 +3396,24 @@ } } }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-direction": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", @@ -3523,6 +3606,24 @@ } } }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popover": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", @@ -3560,6 +3661,24 @@ } } }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", @@ -3663,6 +3782,24 @@ } } }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-progress": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", @@ -3792,6 +3929,24 @@ } } }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-separator": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", @@ -3849,9 +4004,9 @@ } }, "node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" @@ -3959,6 +4114,24 @@ } } }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", diff --git a/package.json b/package.json index a504b500..d39360eb 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@hookform/resolvers": "^5.1.1", "@monaco-editor/react": "^4.7.0", "@radix-ui/react-accordion": "^1.2.11", + "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.15", @@ -45,7 +46,7 @@ "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slider": "^1.3.6", - "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-switch": "^1.2.5", "@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-tooltip": "^1.2.8", diff --git a/src/backend/database/database.ts b/src/backend/database/database.ts index 1eca73d9..744f2889 100644 --- a/src/backend/database/database.ts +++ b/src/backend/database/database.ts @@ -8,6 +8,7 @@ import alertRoutes from "./routes/alerts.js"; import credentialsRoutes from "./routes/credentials.js"; import snippetsRoutes from "./routes/snippets.js"; import terminalRoutes from "./routes/terminal.js"; +import rbacRoutes from "./routes/rbac.js"; import cors from "cors"; import fetch from "node-fetch"; import fs from "fs"; @@ -1436,6 +1437,7 @@ app.use("/alerts", alertRoutes); app.use("/credentials", credentialsRoutes); app.use("/snippets", snippetsRoutes); app.use("/terminal", terminalRoutes); +app.use("/rbac", rbacRoutes); app.use( ( diff --git a/src/backend/database/db/index.ts b/src/backend/database/db/index.ts index 28a0387f..dff125c8 100644 --- a/src/backend/database/db/index.ts +++ b/src/backend/database/db/index.ts @@ -201,12 +201,14 @@ async function initializeCompleteDatabase(): Promise { enable_tunnel INTEGER NOT NULL DEFAULT 1, tunnel_connections TEXT, enable_file_manager INTEGER NOT NULL DEFAULT 1, + enable_docker INTEGER NOT NULL DEFAULT 0, default_path TEXT, autostart_password TEXT, autostart_key TEXT, autostart_key_password TEXT, force_keyboard_interactive TEXT, stats_config TEXT, + docker_config TEXT, terminal_config TEXT, use_socks5 INTEGER, socks5_host TEXT, @@ -333,6 +335,81 @@ async function initializeCompleteDatabase(): Promise { FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE ); + CREATE TABLE IF NOT EXISTS host_access ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + host_id INTEGER NOT NULL, + user_id TEXT, + role_id INTEGER, + granted_by TEXT NOT NULL, + permission_level TEXT NOT NULL DEFAULT 'use', + expires_at TEXT, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + last_accessed_at TEXT, + access_count INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, + FOREIGN KEY (role_id) REFERENCES roles (id) ON DELETE CASCADE, + FOREIGN KEY (granted_by) REFERENCES users (id) ON DELETE CASCADE + ); + + CREATE TABLE IF NOT EXISTS roles ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + display_name TEXT NOT NULL, + description TEXT, + is_system INTEGER NOT NULL DEFAULT 0, + permissions TEXT, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + + CREATE TABLE IF NOT EXISTS user_roles ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + role_id INTEGER NOT NULL, + granted_by TEXT, + granted_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(user_id, role_id), + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, + FOREIGN KEY (role_id) REFERENCES roles (id) ON DELETE CASCADE, + FOREIGN KEY (granted_by) REFERENCES users (id) ON DELETE SET NULL + ); + + CREATE TABLE IF NOT EXISTS audit_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + username TEXT NOT NULL, + action TEXT NOT NULL, + resource_type TEXT NOT NULL, + resource_id TEXT, + resource_name TEXT, + details TEXT, + ip_address TEXT, + user_agent TEXT, + success INTEGER NOT NULL, + error_message TEXT, + timestamp TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE + ); + + CREATE TABLE IF NOT EXISTS session_recordings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + host_id INTEGER NOT NULL, + user_id TEXT NOT NULL, + access_id INTEGER, + started_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + ended_at TEXT, + duration INTEGER, + commands TEXT, + dangerous_actions TEXT, + recording_path TEXT, + terminated_by_owner INTEGER DEFAULT 0, + termination_reason TEXT, + FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, + FOREIGN KEY (access_id) REFERENCES host_access (id) ON DELETE SET NULL + ); + `); try { @@ -491,6 +568,12 @@ const migrateSchema = () => { addColumnIfNotExists("ssh_data", "stats_config", "TEXT"); addColumnIfNotExists("ssh_data", "terminal_config", "TEXT"); addColumnIfNotExists("ssh_data", "quick_actions", "TEXT"); + addColumnIfNotExists( + "ssh_data", + "enable_docker", + "INTEGER NOT NULL DEFAULT 0", + ); + addColumnIfNotExists("ssh_data", "docker_config", "TEXT"); // SOCKS5 Proxy columns addColumnIfNotExists("ssh_data", "use_socks5", "INTEGER"); @@ -564,6 +647,331 @@ const migrateSchema = () => { } } + // RBAC Phase 1: Host Access table + try { + sqlite.prepare("SELECT id FROM host_access LIMIT 1").get(); + } catch { + try { + sqlite.exec(` + CREATE TABLE IF NOT EXISTS host_access ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + host_id INTEGER NOT NULL, + user_id TEXT, + role_id INTEGER, + granted_by TEXT NOT NULL, + permission_level TEXT NOT NULL DEFAULT 'use', + expires_at TEXT, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + last_accessed_at TEXT, + access_count INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, + FOREIGN KEY (role_id) REFERENCES roles (id) ON DELETE CASCADE, + FOREIGN KEY (granted_by) REFERENCES users (id) ON DELETE CASCADE + ); + `); + databaseLogger.info("Created host_access table", { + operation: "schema_migration", + }); + } catch (createError) { + databaseLogger.warn("Failed to create host_access table", { + operation: "schema_migration", + error: createError, + }); + } + } + + // Migration: Add role_id column to existing host_access table + try { + sqlite.prepare("SELECT role_id FROM host_access LIMIT 1").get(); + } catch { + try { + sqlite.exec("ALTER TABLE host_access ADD COLUMN role_id INTEGER REFERENCES roles(id) ON DELETE CASCADE"); + databaseLogger.info("Added role_id column to host_access table", { + operation: "schema_migration", + }); + } catch (alterError) { + databaseLogger.warn("Failed to add role_id column", { + operation: "schema_migration", + error: alterError, + }); + } + } + + // RBAC Phase 2: Roles tables + try { + sqlite.prepare("SELECT id FROM roles LIMIT 1").get(); + } catch { + try { + sqlite.exec(` + CREATE TABLE IF NOT EXISTS roles ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + display_name TEXT NOT NULL, + description TEXT, + is_system INTEGER NOT NULL DEFAULT 0, + permissions TEXT, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + `); + databaseLogger.info("Created roles table", { + operation: "schema_migration", + }); + } catch (createError) { + databaseLogger.warn("Failed to create roles table", { + operation: "schema_migration", + error: createError, + }); + } + } + + try { + sqlite.prepare("SELECT id FROM user_roles LIMIT 1").get(); + } catch { + try { + sqlite.exec(` + CREATE TABLE IF NOT EXISTS user_roles ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + role_id INTEGER NOT NULL, + granted_by TEXT, + granted_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(user_id, role_id), + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, + FOREIGN KEY (role_id) REFERENCES roles (id) ON DELETE CASCADE, + FOREIGN KEY (granted_by) REFERENCES users (id) ON DELETE SET NULL + ); + `); + databaseLogger.info("Created user_roles table", { + operation: "schema_migration", + }); + } catch (createError) { + databaseLogger.warn("Failed to create user_roles table", { + operation: "schema_migration", + error: createError, + }); + } + } + + // RBAC Phase 3: Audit logging tables + try { + sqlite.prepare("SELECT id FROM audit_logs LIMIT 1").get(); + } catch { + try { + sqlite.exec(` + CREATE TABLE IF NOT EXISTS audit_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + username TEXT NOT NULL, + action TEXT NOT NULL, + resource_type TEXT NOT NULL, + resource_id TEXT, + resource_name TEXT, + details TEXT, + ip_address TEXT, + user_agent TEXT, + success INTEGER NOT NULL, + error_message TEXT, + timestamp TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE + ); + `); + databaseLogger.info("Created audit_logs table", { + operation: "schema_migration", + }); + } catch (createError) { + databaseLogger.warn("Failed to create audit_logs table", { + operation: "schema_migration", + error: createError, + }); + } + } + + try { + sqlite.prepare("SELECT id FROM session_recordings LIMIT 1").get(); + } catch { + try { + sqlite.exec(` + CREATE TABLE IF NOT EXISTS session_recordings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + host_id INTEGER NOT NULL, + user_id TEXT NOT NULL, + access_id INTEGER, + started_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + ended_at TEXT, + duration INTEGER, + commands TEXT, + dangerous_actions TEXT, + recording_path TEXT, + terminated_by_owner INTEGER DEFAULT 0, + termination_reason TEXT, + FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, + FOREIGN KEY (access_id) REFERENCES host_access (id) ON DELETE SET NULL + ); + `); + databaseLogger.info("Created session_recordings table", { + operation: "schema_migration", + }); + } catch (createError) { + databaseLogger.warn("Failed to create session_recordings table", { + operation: "schema_migration", + error: createError, + }); + } + } + + // Clean up old system roles and seed correct ones + try { + // First, check what roles exist + const existingRoles = sqlite.prepare("SELECT name, is_system FROM roles").all() as Array<{ name: string; is_system: number }>; + databaseLogger.info("Current roles in database", { + operation: "schema_migration", + roles: existingRoles, + }); + + // Migration: Remove ALL old unwanted roles (system or not) and keep only admin and user + try { + const validSystemRoles = ['admin', 'user']; + const unwantedRoleNames = ['superAdmin', 'powerUser', 'readonly', 'member']; + let deletedCount = 0; + + // First delete known unwanted role names + const deleteByName = sqlite.prepare("DELETE FROM roles WHERE name = ?"); + for (const roleName of unwantedRoleNames) { + const result = deleteByName.run(roleName); + if (result.changes > 0) { + deletedCount += result.changes; + databaseLogger.info(`Deleted role by name: ${roleName}`, { + operation: "schema_migration", + }); + } + } + + // Then delete any system roles that are not admin or user + const deleteOldSystemRole = sqlite.prepare("DELETE FROM roles WHERE name = ? AND is_system = 1"); + for (const role of existingRoles) { + if (role.is_system === 1 && !validSystemRoles.includes(role.name) && !unwantedRoleNames.includes(role.name)) { + const result = deleteOldSystemRole.run(role.name); + if (result.changes > 0) { + deletedCount += result.changes; + databaseLogger.info(`Deleted system role: ${role.name}`, { + operation: "schema_migration", + }); + } + } + } + + databaseLogger.info("Cleanup completed", { + operation: "schema_migration", + deletedCount, + }); + } catch (cleanupError) { + databaseLogger.warn("Failed to clean up old system roles", { + operation: "schema_migration", + error: cleanupError, + }); + } + + // Ensure only admin and user system roles exist + const systemRoles = [ + { + name: "admin", + displayName: "rbac.roles.admin", + description: "Administrator with full access", + permissions: null, + }, + { + name: "user", + displayName: "rbac.roles.user", + description: "Regular user", + permissions: null, + }, + ]; + + for (const role of systemRoles) { + const existingRole = sqlite.prepare("SELECT id FROM roles WHERE name = ?").get(role.name); + if (!existingRole) { + // Create if doesn't exist + try { + sqlite.prepare(` + INSERT INTO roles (name, display_name, description, is_system, permissions) + VALUES (?, ?, ?, 1, ?) + `).run(role.name, role.displayName, role.description, role.permissions); + } catch (insertError) { + databaseLogger.warn(`Failed to create system role: ${role.name}`, { + operation: "schema_migration", + error: insertError, + }); + } + } + } + + databaseLogger.info("System roles migration completed", { + operation: "schema_migration", + }); + + // Migrate existing is_admin users to roles + try { + const adminUsers = sqlite.prepare("SELECT id FROM users WHERE is_admin = 1").all() as { id: string }[]; + const normalUsers = sqlite.prepare("SELECT id FROM users WHERE is_admin = 0").all() as { id: string }[]; + + const adminRole = sqlite.prepare("SELECT id FROM roles WHERE name = 'admin'").get() as { id: number } | undefined; + const userRole = sqlite.prepare("SELECT id FROM roles WHERE name = 'user'").get() as { id: number } | undefined; + + if (adminRole) { + const insertUserRole = sqlite.prepare(` + INSERT OR IGNORE INTO user_roles (user_id, role_id, granted_at) + VALUES (?, ?, CURRENT_TIMESTAMP) + `); + + for (const admin of adminUsers) { + try { + insertUserRole.run(admin.id, adminRole.id); + } catch (error) { + // Ignore duplicate errors + } + } + + databaseLogger.info("Migrated admin users to admin role", { + operation: "schema_migration", + count: adminUsers.length, + }); + } + + if (userRole) { + const insertUserRole = sqlite.prepare(` + INSERT OR IGNORE INTO user_roles (user_id, role_id, granted_at) + VALUES (?, ?, CURRENT_TIMESTAMP) + `); + + for (const user of normalUsers) { + try { + insertUserRole.run(user.id, userRole.id); + } catch (error) { + // Ignore duplicate errors + } + } + + databaseLogger.info("Migrated normal users to user role", { + operation: "schema_migration", + count: normalUsers.length, + }); + } + } catch (migrationError) { + databaseLogger.warn("Failed to migrate existing users to roles", { + operation: "schema_migration", + error: migrationError, + }); + } + } catch (seedError) { + databaseLogger.warn("Failed to seed system roles", { + operation: "schema_migration", + error: seedError, + }); + } + databaseLogger.success("Schema migration completed", { operation: "schema_migration", }); diff --git a/src/backend/database/db/schema.ts b/src/backend/database/db/schema.ts index 7c61aaa7..39c091e6 100644 --- a/src/backend/database/db/schema.ts +++ b/src/backend/database/db/schema.ts @@ -86,6 +86,9 @@ export const sshData = sqliteTable("ssh_data", { enableFileManager: integer("enable_file_manager", { mode: "boolean" }) .notNull() .default(true), + enableDocker: integer("enable_docker", { mode: "boolean" }) + .notNull() + .default(false), defaultPath: text("default_path"), statsConfig: text("stats_config"), terminalConfig: text("terminal_config"), @@ -284,3 +287,140 @@ export const commandHistory = sqliteTable("command_history", { .notNull() .default(sql`CURRENT_TIMESTAMP`), }); + +// RBAC Phase 1: Host Sharing +export const hostAccess = sqliteTable("host_access", { + id: integer("id").primaryKey({ autoIncrement: true }), + hostId: integer("host_id") + .notNull() + .references(() => sshData.id, { onDelete: "cascade" }), + + // Share target: either userId OR roleId (at least one must be set) + userId: text("user_id") + .references(() => users.id, { onDelete: "cascade" }), // Optional + roleId: integer("role_id") + .references(() => roles.id, { onDelete: "cascade" }), // Optional + + grantedBy: text("granted_by") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + + // Permission level + permissionLevel: text("permission_level") + .notNull() + .default("use"), // "view" | "use" | "manage" + + // Time-based access + expiresAt: text("expires_at"), // NULL = never expires + + // Metadata + createdAt: text("created_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + lastAccessedAt: text("last_accessed_at"), + accessCount: integer("access_count").notNull().default(0), +}); + +// RBAC Phase 2: Roles +export const roles = sqliteTable("roles", { + id: integer("id").primaryKey({ autoIncrement: true }), + name: text("name").notNull().unique(), + displayName: text("display_name").notNull(), // For i18n + description: text("description"), + + // System roles cannot be deleted + isSystem: integer("is_system", { mode: "boolean" }) + .notNull() + .default(false), + + // Permissions stored as JSON array (optional - used for grouping only in current phase) + permissions: text("permissions"), // ["hosts.*", "credentials.read", ...] - optional + + createdAt: text("created_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: text("updated_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); + +export const userRoles = sqliteTable("user_roles", { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + roleId: integer("role_id") + .notNull() + .references(() => roles.id, { onDelete: "cascade" }), + + grantedBy: text("granted_by").references(() => users.id, { + onDelete: "set null", + }), + grantedAt: text("granted_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); + +// RBAC Phase 3: Audit Logging +export const auditLogs = sqliteTable("audit_logs", { + id: integer("id").primaryKey({ autoIncrement: true }), + + // Who + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + username: text("username").notNull(), // Snapshot in case user deleted + + // What + action: text("action").notNull(), // "create", "read", "update", "delete", "share" + resourceType: text("resource_type").notNull(), // "host", "credential", "user", "session" + resourceId: text("resource_id"), // Can be text or number, store as text + resourceName: text("resource_name"), // Human-readable identifier + + // Context + details: text("details"), // JSON: { oldValue, newValue, reason, ... } + ipAddress: text("ip_address"), + userAgent: text("user_agent"), + + // Result + success: integer("success", { mode: "boolean" }).notNull(), + errorMessage: text("error_message"), + + // When + timestamp: text("timestamp") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); + +export const sessionRecordings = sqliteTable("session_recordings", { + id: integer("id").primaryKey({ autoIncrement: true }), + + hostId: integer("host_id") + .notNull() + .references(() => sshData.id, { onDelete: "cascade" }), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + accessId: integer("access_id").references(() => hostAccess.id, { + onDelete: "set null", + }), + + // Session info + startedAt: text("started_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + endedAt: text("ended_at"), + duration: integer("duration"), // seconds + + // Command log (lightweight) + commands: text("commands"), // JSON: [{ts, cmd, exitCode, blocked}] + dangerousActions: text("dangerous_actions"), // JSON: blocked commands + + // Full recording (optional, heavy) + recordingPath: text("recording_path"), // Path to .cast file + + // Metadata + terminatedByOwner: integer("terminated_by_owner", { mode: "boolean" }) + .default(false), + terminationReason: text("termination_reason"), +}); diff --git a/src/backend/database/routes/credentials.ts b/src/backend/database/routes/credentials.ts index df9ab936..60a53d9c 100644 --- a/src/backend/database/routes/credentials.ts +++ b/src/backend/database/routes/credentials.ts @@ -1,4 +1,7 @@ -import type { AuthenticatedRequest } from "../../../types/index.js"; +import type { + AuthenticatedRequest, + CredentialBackend, +} from "../../../types/index.js"; import express from "express"; import { db } from "../db/index.js"; import { sshCredentials, sshCredentialUsage, sshData } from "../db/schema.js"; @@ -1124,10 +1127,9 @@ router.post( async function deploySSHKeyToHost( hostConfig: Record, - publicKey: string, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _credentialData: Record, + credData: CredentialBackend, ): Promise<{ success: boolean; message?: string; error?: string }> { + const publicKey = credData.public_key as string; return new Promise((resolve) => { const conn = new Client(); @@ -1248,7 +1250,7 @@ async function deploySSHKeyToHost( .replace(/'/g, "'\\''"); conn.exec( - `printf '%s\\n' '${escapedKey}' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys`, + `printf '%s\\n' '${escapedKey} ${credData.name}@Termix' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys`, (err, stream) => { if (err) { clearTimeout(addTimeout); @@ -1510,7 +1512,7 @@ router.post( }); } - const credData = credential[0]; + const credData = credential[0] as unknown as CredentialBackend; if (credData.authType !== "key") { return res.status(400).json({ @@ -1519,7 +1521,7 @@ router.post( }); } - const publicKey = credData.public_key || credData.publicKey; + const publicKey = credData.public_key; if (!publicKey) { return res.status(400).json({ success: false, @@ -1601,7 +1603,6 @@ router.post( const deployResult = await deploySSHKeyToHost( hostConfig, - publicKey as string, credData, ); diff --git a/src/backend/database/routes/rbac.ts b/src/backend/database/routes/rbac.ts new file mode 100644 index 00000000..64d21ade --- /dev/null +++ b/src/backend/database/routes/rbac.ts @@ -0,0 +1,908 @@ +import type { AuthenticatedRequest } from "../../../types/index.js"; +import express from "express"; +import { db } from "../db/index.js"; +import { + hostAccess, + sshData, + users, + roles, + userRoles, + auditLogs, +} from "../db/schema.js"; +import { eq, and, desc, sql, or, isNull, gte } from "drizzle-orm"; +import type { Request, Response } from "express"; +import { databaseLogger } from "../../utils/logger.js"; +import { AuthManager } from "../../utils/auth-manager.js"; +import { PermissionManager } from "../../utils/permission-manager.js"; + +const router = express.Router(); + +const authManager = AuthManager.getInstance(); +const permissionManager = PermissionManager.getInstance(); + +const authenticateJWT = authManager.createAuthMiddleware(); + +function isNonEmptyString(value: unknown): value is string { + return typeof value === "string" && value.trim().length > 0; +} + +/** + * Share a host with a user or role + * POST /rbac/host/:id/share + */ +router.post( + "/host/:id/share", + authenticateJWT, + async (req: AuthenticatedRequest, res: Response) => { + const hostId = parseInt(req.params.id, 10); + const userId = req.userId!; + + if (isNaN(hostId)) { + return res.status(400).json({ error: "Invalid host ID" }); + } + + try { + const { + targetType = "user", // "user" or "role" + targetUserId, + targetRoleId, + durationHours, + permissionLevel = "use", + } = req.body; + + // Validate target type + if (!["user", "role"].includes(targetType)) { + return res + .status(400) + .json({ error: "Invalid target type. Must be 'user' or 'role'" }); + } + + // Validate required fields based on target type + if (targetType === "user" && !isNonEmptyString(targetUserId)) { + return res + .status(400) + .json({ error: "Target user ID is required when sharing with user" }); + } + if (targetType === "role" && !targetRoleId) { + return res + .status(400) + .json({ error: "Target role ID is required when sharing with role" }); + } + + // Verify user owns the host + const host = await db + .select() + .from(sshData) + .where(and(eq(sshData.id, hostId), eq(sshData.userId, userId))) + .limit(1); + + if (host.length === 0) { + databaseLogger.warn("Attempt to share host not owned by user", { + operation: "share_host", + userId, + hostId, + }); + return res.status(403).json({ error: "Not host owner" }); + } + + // Verify target exists (user or role) + if (targetType === "user") { + const targetUser = await db + .select({ id: users.id, username: users.username }) + .from(users) + .where(eq(users.id, targetUserId)) + .limit(1); + + if (targetUser.length === 0) { + return res.status(404).json({ error: "Target user not found" }); + } + } else { + const targetRole = await db + .select({ id: roles.id, name: roles.name }) + .from(roles) + .where(eq(roles.id, targetRoleId)) + .limit(1); + + if (targetRole.length === 0) { + return res.status(404).json({ error: "Target role not found" }); + } + } + + // Calculate expiry time + let expiresAt: string | null = null; + if ( + durationHours && + typeof durationHours === "number" && + durationHours > 0 + ) { + const expiryDate = new Date(); + expiryDate.setHours(expiryDate.getHours() + durationHours); + expiresAt = expiryDate.toISOString(); + } + + // Validate permission level + const validLevels = ["view", "use", "manage"]; + if (!validLevels.includes(permissionLevel)) { + return res.status(400).json({ + error: "Invalid permission level", + validLevels, + }); + } + + // Check if access already exists + const whereConditions = [eq(hostAccess.hostId, hostId)]; + if (targetType === "user") { + whereConditions.push(eq(hostAccess.userId, targetUserId)); + } else { + whereConditions.push(eq(hostAccess.roleId, targetRoleId)); + } + + const existing = await db + .select() + .from(hostAccess) + .where(and(...whereConditions)) + .limit(1); + + if (existing.length > 0) { + // Update existing access + await db + .update(hostAccess) + .set({ + permissionLevel, + expiresAt, + }) + .where(eq(hostAccess.id, existing[0].id)); + + databaseLogger.info("Updated existing host access", { + operation: "share_host", + hostId, + targetType, + targetUserId: targetType === "user" ? targetUserId : undefined, + targetRoleId: targetType === "role" ? targetRoleId : undefined, + permissionLevel, + expiresAt, + }); + + return res.json({ + success: true, + message: "Host access updated", + expiresAt, + }); + } + + // Create new access + const result = await db.insert(hostAccess).values({ + hostId, + userId: targetType === "user" ? targetUserId : null, + roleId: targetType === "role" ? targetRoleId : null, + grantedBy: userId, + permissionLevel, + expiresAt, + }); + + databaseLogger.info("Created host access", { + operation: "share_host", + hostId, + hostName: host[0].name, + targetType, + targetUserId: targetType === "user" ? targetUserId : undefined, + targetRoleId: targetType === "role" ? targetRoleId : undefined, + permissionLevel, + expiresAt, + }); + + res.json({ + success: true, + message: `Host shared successfully with ${targetType}`, + expiresAt, + }); + } catch (error) { + databaseLogger.error("Failed to share host", error, { + operation: "share_host", + hostId, + userId, + }); + res.status(500).json({ error: "Failed to share host" }); + } + }, +); + +/** + * Revoke host access + * DELETE /rbac/host/:id/access/:accessId + */ +router.delete( + "/host/:id/access/:accessId", + authenticateJWT, + async (req: AuthenticatedRequest, res: Response) => { + const hostId = parseInt(req.params.id, 10); + const accessId = parseInt(req.params.accessId, 10); + const userId = req.userId!; + + if (isNaN(hostId) || isNaN(accessId)) { + return res.status(400).json({ error: "Invalid ID" }); + } + + try { + // Verify user owns the host + const host = await db + .select() + .from(sshData) + .where(and(eq(sshData.id, hostId), eq(sshData.userId, userId))) + .limit(1); + + if (host.length === 0) { + return res.status(403).json({ error: "Not host owner" }); + } + + // Delete the access + await db.delete(hostAccess).where(eq(hostAccess.id, accessId)); + + databaseLogger.info("Revoked host access", { + operation: "revoke_host_access", + hostId, + accessId, + userId, + }); + + res.json({ success: true, message: "Access revoked" }); + } catch (error) { + databaseLogger.error("Failed to revoke host access", error, { + operation: "revoke_host_access", + hostId, + accessId, + userId, + }); + res.status(500).json({ error: "Failed to revoke access" }); + } + }, +); + +/** + * Get host access list + * GET /rbac/host/:id/access + */ +router.get( + "/host/:id/access", + authenticateJWT, + async (req: AuthenticatedRequest, res: Response) => { + const hostId = parseInt(req.params.id, 10); + const userId = req.userId!; + + if (isNaN(hostId)) { + return res.status(400).json({ error: "Invalid host ID" }); + } + + try { + // Verify user owns the host + const host = await db + .select() + .from(sshData) + .where(and(eq(sshData.id, hostId), eq(sshData.userId, userId))) + .limit(1); + + if (host.length === 0) { + return res.status(403).json({ error: "Not host owner" }); + } + + // Get all access records (both user and role based) + const rawAccessList = await db + .select({ + id: hostAccess.id, + userId: hostAccess.userId, + roleId: hostAccess.roleId, + username: users.username, + roleName: roles.name, + roleDisplayName: roles.displayName, + grantedBy: hostAccess.grantedBy, + grantedByUsername: sql`(SELECT username FROM users WHERE id = ${hostAccess.grantedBy})`, + permissionLevel: hostAccess.permissionLevel, + expiresAt: hostAccess.expiresAt, + createdAt: hostAccess.createdAt, + lastAccessedAt: hostAccess.lastAccessedAt, + accessCount: hostAccess.accessCount, + }) + .from(hostAccess) + .leftJoin(users, eq(hostAccess.userId, users.id)) + .leftJoin(roles, eq(hostAccess.roleId, roles.id)) + .where(eq(hostAccess.hostId, hostId)) + .orderBy(desc(hostAccess.createdAt)); + + // Format access list with type information + const accessList = rawAccessList.map((access) => ({ + id: access.id, + targetType: access.userId ? "user" : "role", + userId: access.userId, + roleId: access.roleId, + username: access.username, + roleName: access.roleName, + roleDisplayName: access.roleDisplayName, + grantedBy: access.grantedBy, + grantedByUsername: access.grantedByUsername, + permissionLevel: access.permissionLevel, + expiresAt: access.expiresAt, + createdAt: access.createdAt, + lastAccessedAt: access.lastAccessedAt, + accessCount: access.accessCount, + })); + + res.json({ accessList }); + } catch (error) { + databaseLogger.error("Failed to get host access list", error, { + operation: "get_host_access_list", + hostId, + userId, + }); + res.status(500).json({ error: "Failed to get access list" }); + } + }, +); + +/** + * Get user's shared hosts (hosts shared WITH this user) + * GET /rbac/shared-hosts + */ +router.get( + "/shared-hosts", + authenticateJWT, + async (req: AuthenticatedRequest, res: Response) => { + const userId = req.userId!; + + try { + const now = new Date().toISOString(); + + const sharedHosts = await db + .select({ + id: sshData.id, + name: sshData.name, + ip: sshData.ip, + port: sshData.port, + username: sshData.username, + folder: sshData.folder, + tags: sshData.tags, + permissionLevel: hostAccess.permissionLevel, + expiresAt: hostAccess.expiresAt, + grantedBy: hostAccess.grantedBy, + ownerUsername: users.username, + }) + .from(hostAccess) + .innerJoin(sshData, eq(hostAccess.hostId, sshData.id)) + .innerJoin(users, eq(sshData.userId, users.id)) + .where( + and( + eq(hostAccess.userId, userId), + or(isNull(hostAccess.expiresAt), gte(hostAccess.expiresAt, now)), + ), + ) + .orderBy(desc(hostAccess.createdAt)); + + res.json({ sharedHosts }); + } catch (error) { + databaseLogger.error("Failed to get shared hosts", error, { + operation: "get_shared_hosts", + userId, + }); + res.status(500).json({ error: "Failed to get shared hosts" }); + } + }, +); + +/** + * Get all roles + * GET /rbac/roles + */ +router.get( + "/roles", + authenticateJWT, + permissionManager.requireAdmin(), + async (req: AuthenticatedRequest, res: Response) => { + try { + const allRoles = await db + .select() + .from(roles) + .orderBy(roles.isSystem, roles.name); + + const rolesWithParsedPermissions = allRoles.map((role) => ({ + ...role, + permissions: JSON.parse(role.permissions), + })); + + res.json({ roles: rolesWithParsedPermissions }); + } catch (error) { + databaseLogger.error("Failed to get roles", error, { + operation: "get_roles", + }); + res.status(500).json({ error: "Failed to get roles" }); + } + }, +); + +// ============================================================================ +// Role Management (CRUD) +// ============================================================================ + +/** + * Get all roles + * GET /rbac/roles + */ +router.get( + "/roles", + authenticateJWT, + async (req: AuthenticatedRequest, res: Response) => { + try { + const rolesList = await db + .select({ + id: roles.id, + name: roles.name, + displayName: roles.displayName, + description: roles.description, + isSystem: roles.isSystem, + createdAt: roles.createdAt, + updatedAt: roles.updatedAt, + }) + .from(roles) + .orderBy(roles.isSystem, roles.name); + + res.json({ roles: rolesList }); + } catch (error) { + databaseLogger.error("Failed to get roles", error, { + operation: "get_roles", + }); + res.status(500).json({ error: "Failed to get roles" }); + } + }, +); + +/** + * Create new role + * POST /rbac/roles + */ +router.post( + "/roles", + authenticateJWT, + permissionManager.requireAdmin(), + async (req: AuthenticatedRequest, res: Response) => { + const { name, displayName, description } = req.body; + + // Validate required fields + if (!isNonEmptyString(name) || !isNonEmptyString(displayName)) { + return res.status(400).json({ + error: "Role name and display name are required", + }); + } + + // Validate name format (alphanumeric, underscore, hyphen only) + if (!/^[a-z0-9_-]+$/.test(name)) { + return res.status(400).json({ + error: + "Role name must contain only lowercase letters, numbers, underscores, and hyphens", + }); + } + + try { + // Check if role name already exists + const existing = await db + .select({ id: roles.id }) + .from(roles) + .where(eq(roles.name, name)) + .limit(1); + + if (existing.length > 0) { + return res.status(409).json({ + error: "A role with this name already exists", + }); + } + + // Create new role + const result = await db.insert(roles).values({ + name, + displayName, + description: description || null, + isSystem: false, + permissions: null, // Roles are for grouping only + }); + + const newRoleId = result.lastInsertRowid; + + databaseLogger.info("Created new role", { + operation: "create_role", + roleId: newRoleId, + roleName: name, + }); + + res.status(201).json({ + success: true, + roleId: newRoleId, + message: "Role created successfully", + }); + } catch (error) { + databaseLogger.error("Failed to create role", error, { + operation: "create_role", + roleName: name, + }); + res.status(500).json({ error: "Failed to create role" }); + } + }, +); + +/** + * Update role + * PUT /rbac/roles/:id + */ +router.put( + "/roles/:id", + authenticateJWT, + permissionManager.requireAdmin(), + async (req: AuthenticatedRequest, res: Response) => { + const roleId = parseInt(req.params.id, 10); + const { displayName, description } = req.body; + + if (isNaN(roleId)) { + return res.status(400).json({ error: "Invalid role ID" }); + } + + // Validate at least one field to update + if (!displayName && description === undefined) { + return res.status(400).json({ + error: "At least one field (displayName or description) is required", + }); + } + + try { + // Get existing role + const existingRole = await db + .select({ + id: roles.id, + name: roles.name, + isSystem: roles.isSystem, + }) + .from(roles) + .where(eq(roles.id, roleId)) + .limit(1); + + if (existingRole.length === 0) { + return res.status(404).json({ error: "Role not found" }); + } + + // Build update object + const updates: { + displayName?: string; + description?: string | null; + updatedAt: string; + } = { + updatedAt: new Date().toISOString(), + }; + + if (displayName) { + updates.displayName = displayName; + } + + if (description !== undefined) { + updates.description = description || null; + } + + // Update role + await db.update(roles).set(updates).where(eq(roles.id, roleId)); + + databaseLogger.info("Updated role", { + operation: "update_role", + roleId, + roleName: existingRole[0].name, + }); + + res.json({ + success: true, + message: "Role updated successfully", + }); + } catch (error) { + databaseLogger.error("Failed to update role", error, { + operation: "update_role", + roleId, + }); + res.status(500).json({ error: "Failed to update role" }); + } + }, +); + +/** + * Delete role + * DELETE /rbac/roles/:id + */ +router.delete( + "/roles/:id", + authenticateJWT, + permissionManager.requireAdmin(), + async (req: AuthenticatedRequest, res: Response) => { + const roleId = parseInt(req.params.id, 10); + + if (isNaN(roleId)) { + return res.status(400).json({ error: "Invalid role ID" }); + } + + try { + // Get role details + const role = await db + .select({ + id: roles.id, + name: roles.name, + isSystem: roles.isSystem, + }) + .from(roles) + .where(eq(roles.id, roleId)) + .limit(1); + + if (role.length === 0) { + return res.status(404).json({ error: "Role not found" }); + } + + // Cannot delete system roles + if (role[0].isSystem) { + return res.status(403).json({ + error: "Cannot delete system roles", + }); + } + + // Check if role is in use + const usageCount = await db + .select({ count: sql`count(*)` }) + .from(userRoles) + .where(eq(userRoles.roleId, roleId)); + + if (usageCount[0].count > 0) { + return res.status(409).json({ + error: `Cannot delete role: ${usageCount[0].count} user(s) are assigned to this role`, + usageCount: usageCount[0].count, + }); + } + + // Check if role is used in host_access + const hostAccessCount = await db + .select({ count: sql`count(*)` }) + .from(hostAccess) + .where(eq(hostAccess.roleId, roleId)); + + if (hostAccessCount[0].count > 0) { + return res.status(409).json({ + error: `Cannot delete role: ${hostAccessCount[0].count} host(s) are shared with this role`, + hostAccessCount: hostAccessCount[0].count, + }); + } + + // Delete role + await db.delete(roles).where(eq(roles.id, roleId)); + + databaseLogger.info("Deleted role", { + operation: "delete_role", + roleId, + roleName: role[0].name, + }); + + res.json({ + success: true, + message: "Role deleted successfully", + }); + } catch (error) { + databaseLogger.error("Failed to delete role", error, { + operation: "delete_role", + roleId, + }); + res.status(500).json({ error: "Failed to delete role" }); + } + }, +); + +// ============================================================================ +// User-Role Assignment +// ============================================================================ + +/** + * Assign role to user + * POST /rbac/users/:userId/roles + */ +router.post( + "/users/:userId/roles", + authenticateJWT, + permissionManager.requireAdmin(), + async (req: AuthenticatedRequest, res: Response) => { + const targetUserId = req.params.userId; + const currentUserId = req.userId!; + + try { + const { roleId } = req.body; + + if (typeof roleId !== "number") { + return res.status(400).json({ error: "Role ID is required" }); + } + + // Verify target user exists + const targetUser = await db + .select() + .from(users) + .where(eq(users.id, targetUserId)) + .limit(1); + + if (targetUser.length === 0) { + return res.status(404).json({ error: "User not found" }); + } + + // Verify role exists + const role = await db + .select() + .from(roles) + .where(eq(roles.id, roleId)) + .limit(1); + + if (role.length === 0) { + return res.status(404).json({ error: "Role not found" }); + } + + // Prevent manual assignment of system roles + if (role[0].isSystem) { + return res.status(403).json({ + error: + "System roles (admin, user) are automatically assigned and cannot be manually assigned", + }); + } + + // Check if already assigned + const existing = await db + .select() + .from(userRoles) + .where( + and(eq(userRoles.userId, targetUserId), eq(userRoles.roleId, roleId)), + ) + .limit(1); + + if (existing.length > 0) { + return res.status(409).json({ error: "Role already assigned" }); + } + + // Assign role + await db.insert(userRoles).values({ + userId: targetUserId, + roleId, + grantedBy: currentUserId, + }); + + // Invalidate permission cache + permissionManager.invalidateUserPermissionCache(targetUserId); + + databaseLogger.info("Assigned role to user", { + operation: "assign_role", + targetUserId, + roleId, + roleName: role[0].name, + grantedBy: currentUserId, + }); + + res.json({ + success: true, + message: "Role assigned successfully", + }); + } catch (error) { + databaseLogger.error("Failed to assign role", error, { + operation: "assign_role", + targetUserId, + }); + res.status(500).json({ error: "Failed to assign role" }); + } + }, +); + +/** + * Remove role from user + * DELETE /rbac/users/:userId/roles/:roleId + */ +router.delete( + "/users/:userId/roles/:roleId", + authenticateJWT, + permissionManager.requireAdmin(), + async (req: AuthenticatedRequest, res: Response) => { + const targetUserId = req.params.userId; + const roleId = parseInt(req.params.roleId, 10); + + if (isNaN(roleId)) { + return res.status(400).json({ error: "Invalid role ID" }); + } + + try { + // Verify role exists and get its details + const role = await db + .select({ + id: roles.id, + name: roles.name, + isSystem: roles.isSystem, + }) + .from(roles) + .where(eq(roles.id, roleId)) + .limit(1); + + if (role.length === 0) { + return res.status(404).json({ error: "Role not found" }); + } + + // Prevent removal of system roles + if (role[0].isSystem) { + return res.status(403).json({ + error: + "System roles (admin, user) are automatically assigned and cannot be removed", + }); + } + + // Delete the user-role assignment + await db + .delete(userRoles) + .where( + and(eq(userRoles.userId, targetUserId), eq(userRoles.roleId, roleId)), + ); + + // Invalidate permission cache + permissionManager.invalidateUserPermissionCache(targetUserId); + + databaseLogger.info("Removed role from user", { + operation: "remove_role", + targetUserId, + roleId, + }); + + res.json({ + success: true, + message: "Role removed successfully", + }); + } catch (error) { + databaseLogger.error("Failed to remove role", error, { + operation: "remove_role", + targetUserId, + roleId, + }); + res.status(500).json({ error: "Failed to remove role" }); + } + }, +); + +/** + * Get user's roles + * GET /rbac/users/:userId/roles + */ +router.get( + "/users/:userId/roles", + authenticateJWT, + async (req: AuthenticatedRequest, res: Response) => { + const targetUserId = req.params.userId; + const currentUserId = req.userId!; + + // Users can only see their own roles unless they're admin + if ( + targetUserId !== currentUserId && + !(await permissionManager.isAdmin(currentUserId)) + ) { + return res.status(403).json({ error: "Access denied" }); + } + + try { + const userRolesList = await db + .select({ + id: userRoles.id, + roleId: roles.id, + roleName: roles.name, + roleDisplayName: roles.displayName, + description: roles.description, + isSystem: roles.isSystem, + grantedAt: userRoles.grantedAt, + }) + .from(userRoles) + .innerJoin(roles, eq(userRoles.roleId, roles.id)) + .where(eq(userRoles.userId, targetUserId)); + + res.json({ roles: userRolesList }); + } catch (error) { + databaseLogger.error("Failed to get user roles", error, { + operation: "get_user_roles", + targetUserId, + }); + res.status(500).json({ error: "Failed to get user roles" }); + } + }, +); + +export default router; diff --git a/src/backend/database/routes/ssh.ts b/src/backend/database/routes/ssh.ts index a50a1d8c..c3e5510c 100644 --- a/src/backend/database/routes/ssh.ts +++ b/src/backend/database/routes/ssh.ts @@ -11,8 +11,10 @@ import { sshFolders, commandHistory, recentActivity, + hostAccess, + userRoles, } from "../db/schema.js"; -import { eq, and, desc, isNotNull, or } from "drizzle-orm"; +import { eq, and, desc, isNotNull, or, isNull, gte, sql, inArray } from "drizzle-orm"; import type { Request, Response } from "express"; import multer from "multer"; import { sshLogger } from "../../utils/logger.js"; @@ -235,6 +237,7 @@ router.post( enableTerminal, enableTunnel, enableFileManager, + enableDocker, defaultPath, tunnelConnections, jumpHosts, @@ -293,6 +296,7 @@ router.post( ? JSON.stringify(quickActions) : null, enableFileManager: enableFileManager ? 1 : 0, + enableDocker: enableDocker ? 1 : 0, defaultPath: defaultPath || null, statsConfig: statsConfig ? JSON.stringify(statsConfig) : null, terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null, @@ -360,6 +364,7 @@ router.post( ? JSON.parse(createdHost.jumpHosts as string) : [], enableFileManager: !!createdHost.enableFileManager, + enableDocker: !!createdHost.enableDocker, statsConfig: createdHost.statsConfig ? JSON.parse(createdHost.statsConfig as string) : undefined, @@ -476,6 +481,7 @@ router.put( enableTerminal, enableTunnel, enableFileManager, + enableDocker, defaultPath, tunnelConnections, jumpHosts, @@ -528,6 +534,7 @@ router.put( ? JSON.stringify(quickActions) : null, enableFileManager: enableFileManager ? 1 : 0, + enableDocker: enableDocker ? 1 : 0, defaultPath: defaultPath || null, statsConfig: statsConfig ? JSON.stringify(statsConfig) : null, terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null, @@ -613,9 +620,13 @@ router.put( ? JSON.parse(updatedHost.jumpHosts as string) : [], enableFileManager: !!updatedHost.enableFileManager, + enableDocker: !!updatedHost.enableDocker, statsConfig: updatedHost.statsConfig ? JSON.parse(updatedHost.statsConfig as string) : undefined, + dockerConfig: updatedHost.dockerConfig + ? JSON.parse(updatedHost.dockerConfig as string) + : undefined, }; const resolvedHost = (await resolveHostCredentials(baseHost)) || baseHost; @@ -687,8 +698,98 @@ router.get( return res.status(400).json({ error: "Invalid userId" }); } try { + const now = new Date().toISOString(); + + // Get user's role IDs + const userRoleIds = await db + .select({ roleId: userRoles.roleId }) + .from(userRoles) + .where(eq(userRoles.userId, userId)); + const roleIds = userRoleIds.map((r) => r.roleId); + + // Query own hosts + shared hosts with access check + const rawData = await db + .select({ + // All ssh_data fields + id: sshData.id, + userId: sshData.userId, + name: sshData.name, + ip: sshData.ip, + port: sshData.port, + username: sshData.username, + folder: sshData.folder, + tags: sshData.tags, + pin: sshData.pin, + authType: sshData.authType, + password: sshData.password, + key: sshData.key, + keyPassword: sshData.key_password, + keyType: sshData.keyType, + enableTerminal: sshData.enableTerminal, + enableTunnel: sshData.enableTunnel, + tunnelConnections: sshData.tunnelConnections, + jumpHosts: sshData.jumpHosts, + enableFileManager: sshData.enableFileManager, + defaultPath: sshData.defaultPath, + autostartPassword: sshData.autostartPassword, + autostartKey: sshData.autostartKey, + autostartKeyPassword: sshData.autostartKeyPassword, + forceKeyboardInteractive: sshData.forceKeyboardInteractive, + statsConfig: sshData.statsConfig, + terminalConfig: sshData.terminalConfig, + createdAt: sshData.createdAt, + updatedAt: sshData.updatedAt, + credentialId: sshData.credentialId, + overrideCredentialUsername: sshData.overrideCredentialUsername, + quickActions: sshData.quickActions, + + // Shared access info + isShared: sql`${hostAccess.id} IS NOT NULL`, + permissionLevel: hostAccess.permissionLevel, + expiresAt: hostAccess.expiresAt, + }) + .from(sshData) + .leftJoin( + hostAccess, + and( + eq(hostAccess.hostId, sshData.id), + or( + eq(hostAccess.userId, userId), + roleIds.length > 0 ? inArray(hostAccess.roleId, roleIds) : sql`false`, + ), + or( + isNull(hostAccess.expiresAt), + gte(hostAccess.expiresAt, now), + ), + ), + ) + .where( + or( + eq(sshData.userId, userId), // Own hosts + and( + // Shared to user directly (not expired) + eq(hostAccess.userId, userId), + or( + isNull(hostAccess.expiresAt), + gte(hostAccess.expiresAt, now), + ), + ), + roleIds.length > 0 + ? and( + // Shared to user's role (not expired) + inArray(hostAccess.roleId, roleIds), + or( + isNull(hostAccess.expiresAt), + gte(hostAccess.expiresAt, now), + ), + ) + : sql`false`, + ), + ); + + // Decrypt and format the data const data = await SimpleDBOps.select( - db.select().from(sshData).where(eq(sshData.userId, userId)), + Promise.resolve(rawData), "ssh_data", userId, ); @@ -714,6 +815,7 @@ router.get( ? JSON.parse(row.quickActions as string) : [], enableFileManager: !!row.enableFileManager, + enableDocker: !!row.enableDocker, statsConfig: row.statsConfig ? JSON.parse(row.statsConfig as string) : undefined, @@ -724,6 +826,11 @@ router.get( socks5ProxyChain: row.socks5ProxyChain ? JSON.parse(row.socks5ProxyChain as string) : [], + + // Add shared access metadata + isShared: !!row.isShared, + permissionLevel: row.permissionLevel || undefined, + sharedExpiresAt: row.expiresAt || undefined, }; return (await resolveHostCredentials(baseHost)) || baseHost; @@ -1492,6 +1599,29 @@ async function resolveHostCredentials( host: Record, ): Promise> { try { + // Skip credential resolution for shared hosts + // Shared users cannot access the owner's encrypted credentials + if (host.isShared && host.credentialId) { + sshLogger.info( + `Skipping credential resolution for shared host ${host.id} with credentialId ${host.credentialId}`, + { + operation: "resolve_host_credentials_shared", + hostId: host.id as number, + isShared: host.isShared, + }, + ); + // Return host without resolving credentials + // The frontend should handle credential auth for shared hosts differently + const result = { ...host }; + if (host.key_password !== undefined) { + if (result.keyPassword === undefined) { + result.keyPassword = host.key_password; + } + delete result.key_password; + } + return result; + } + if (host.credentialId && host.userId) { const credentialId = host.credentialId as number; const userId = host.userId as string; diff --git a/src/backend/database/routes/users.ts b/src/backend/database/routes/users.ts index 473654d9..814f06bc 100644 --- a/src/backend/database/routes/users.ts +++ b/src/backend/database/routes/users.ts @@ -15,6 +15,8 @@ import { sshCredentialUsage, recentActivity, snippets, + roles, + userRoles, } from "../db/schema.js"; import { eq, and } from "drizzle-orm"; import bcrypt from "bcryptjs"; @@ -210,6 +212,41 @@ router.post("/create", async (req, res) => { totp_backup_codes: null, }); + // Assign default role to new user + try { + const defaultRoleName = isFirstUser ? "admin" : "user"; + const defaultRole = await db + .select({ id: roles.id }) + .from(roles) + .where(eq(roles.name, defaultRoleName)) + .limit(1); + + if (defaultRole.length > 0) { + await db.insert(userRoles).values({ + userId: id, + roleId: defaultRole[0].id, + grantedBy: id, // Self-assigned during registration + }); + authLogger.info("Assigned default role to new user", { + operation: "assign_default_role", + userId: id, + roleName: defaultRoleName, + }); + } else { + authLogger.warn("Default role not found during user registration", { + operation: "assign_default_role", + userId: id, + roleName: defaultRoleName, + }); + } + } catch (roleError) { + authLogger.error("Failed to assign default role", roleError, { + operation: "assign_default_role", + userId: id, + }); + // Don't fail user creation if role assignment fails + } + try { await authManager.registerUser(id, password); } catch (encryptionError) { @@ -816,6 +853,41 @@ router.get("/oidc/callback", async (req, res) => { scopes: String(config.scopes), }); + // Assign default role to new OIDC user + try { + const defaultRoleName = isFirstUser ? "admin" : "user"; + const defaultRole = await db + .select({ id: roles.id }) + .from(roles) + .where(eq(roles.name, defaultRoleName)) + .limit(1); + + if (defaultRole.length > 0) { + await db.insert(userRoles).values({ + userId: id, + roleId: defaultRole[0].id, + grantedBy: id, // Self-assigned during registration + }); + authLogger.info("Assigned default role to new OIDC user", { + operation: "assign_default_role_oidc", + userId: id, + roleName: defaultRoleName, + }); + } else { + authLogger.warn("Default role not found during OIDC user registration", { + operation: "assign_default_role_oidc", + userId: id, + roleName: defaultRoleName, + }); + } + } catch (roleError) { + authLogger.error("Failed to assign default role to OIDC user", roleError, { + operation: "assign_default_role_oidc", + userId: id, + }); + // Don't fail user creation if role assignment fails + } + try { const sessionDurationMs = deviceInfo.type === "desktop" || deviceInfo.type === "mobile" diff --git a/scripts/enable-ssl.sh b/src/backend/scripts/enable-ssl.sh similarity index 100% rename from scripts/enable-ssl.sh rename to src/backend/scripts/enable-ssl.sh diff --git a/scripts/setup-ssl.sh b/src/backend/scripts/setup-ssl.sh similarity index 100% rename from scripts/setup-ssl.sh rename to src/backend/scripts/setup-ssl.sh diff --git a/src/backend/ssh/docker-console.ts b/src/backend/ssh/docker-console.ts new file mode 100644 index 00000000..3cc28394 --- /dev/null +++ b/src/backend/ssh/docker-console.ts @@ -0,0 +1,687 @@ +import { Client as SSHClient } from "ssh2"; +import { WebSocketServer, WebSocket } from "ws"; +import { parse as parseUrl } from "url"; +import { AuthManager } from "../utils/auth-manager.js"; +import { sshData, sshCredentials } from "../database/db/schema.js"; +import { and, eq } from "drizzle-orm"; +import { getDb } from "../database/db/index.js"; +import { SimpleDBOps } from "../utils/simple-db-ops.js"; +import { systemLogger } from "../utils/logger.js"; +import type { SSHHost } from "../../types/index.js"; + +const dockerConsoleLogger = systemLogger; + +interface SSHSession { + client: SSHClient; + stream: any; + isConnected: boolean; + containerId?: string; + shell?: string; +} + +const activeSessions = new Map(); + +// WebSocket server on port 30008 +const wss = new WebSocketServer({ + port: 30008, + verifyClient: async (info, callback) => { + try { + const url = parseUrl(info.req.url || "", true); + const token = url.query.token as string; + + if (!token) { + dockerConsoleLogger.warn("WebSocket connection rejected: No token", { + operation: "ws_verify", + }); + return callback(false, 401, "Authentication required"); + } + + const authManager = AuthManager.getInstance(); + const decoded = await authManager.verifyJWTToken(token); + + if (!decoded || !decoded.userId) { + dockerConsoleLogger.warn( + "WebSocket connection rejected: Invalid token", + { + operation: "ws_verify", + }, + ); + return callback(false, 401, "Invalid token"); + } + + // Store userId in the request for later use + (info.req as any).userId = decoded.userId; + + dockerConsoleLogger.info("WebSocket connection verified", { + operation: "ws_verify", + userId: decoded.userId, + }); + + callback(true); + } catch (error) { + dockerConsoleLogger.error("WebSocket verification error", error, { + operation: "ws_verify", + }); + callback(false, 500, "Authentication failed"); + } + }, +}); + +// Helper function to detect available shell in container +async function detectShell( + session: SSHSession, + containerId: string, +): Promise { + const shells = ["bash", "sh", "ash"]; + + for (const shell of shells) { + try { + await new Promise((resolve, reject) => { + session.client.exec( + `docker exec ${containerId} which ${shell}`, + (err, stream) => { + if (err) return reject(err); + + let output = ""; + stream.on("data", (data: Buffer) => { + output += data.toString(); + }); + + stream.on("close", (code: number) => { + if (code === 0 && output.trim()) { + resolve(); + } else { + reject(new Error(`Shell ${shell} not found`)); + } + }); + + stream.stderr.on("data", () => { + // Ignore stderr + }); + }, + ); + }); + + // If we get here, the shell was found + return shell; + } catch { + // Try next shell + continue; + } + } + + // Default to sh if nothing else works + return "sh"; +} + +// Helper function to create jump host chain +async function createJumpHostChain( + jumpHosts: any[], + userId: string, +): Promise { + if (!jumpHosts || jumpHosts.length === 0) { + return null; + } + + let currentClient: SSHClient | null = null; + + for (let i = 0; i < jumpHosts.length; i++) { + const jumpHostId = jumpHosts[i].hostId; + + // Fetch jump host from database + const jumpHostData = await SimpleDBOps.select( + getDb() + .select() + .from(sshData) + .where(and(eq(sshData.id, jumpHostId), eq(sshData.userId, userId))), + "ssh_data", + userId, + ); + + if (jumpHostData.length === 0) { + throw new Error(`Jump host ${jumpHostId} not found`); + } + + const jumpHost = jumpHostData[0] as unknown as SSHHost; + if (typeof jumpHost.jumpHosts === "string" && jumpHost.jumpHosts) { + try { + jumpHost.jumpHosts = JSON.parse(jumpHost.jumpHosts); + } catch (e) { + dockerConsoleLogger.error("Failed to parse jump hosts", e, { + hostId: jumpHost.id, + }); + jumpHost.jumpHosts = []; + } + } + + // Resolve credentials for jump host + let resolvedCredentials: any = { + password: jumpHost.password, + sshKey: jumpHost.key, + keyPassword: jumpHost.keyPassword, + authType: jumpHost.authType, + }; + + if (jumpHost.credentialId) { + const credentials = await SimpleDBOps.select( + getDb() + .select() + .from(sshCredentials) + .where( + and( + eq(sshCredentials.id, jumpHost.credentialId as number), + eq(sshCredentials.userId, userId), + ), + ), + "ssh_credentials", + userId, + ); + + if (credentials.length > 0) { + const credential = credentials[0]; + resolvedCredentials = { + password: credential.password, + sshKey: + credential.private_key || credential.privateKey || credential.key, + keyPassword: credential.key_password || credential.keyPassword, + authType: credential.auth_type || credential.authType, + }; + } + } + + const client = new SSHClient(); + + const config: any = { + host: jumpHost.ip, + port: jumpHost.port || 22, + username: jumpHost.username, + tryKeyboard: true, + readyTimeout: 60000, + keepaliveInterval: 30000, + keepaliveCountMax: 120, + tcpKeepAlive: true, + tcpKeepAliveInitialDelay: 30000, + }; + + // Set authentication + if ( + resolvedCredentials.authType === "password" && + resolvedCredentials.password + ) { + config.password = resolvedCredentials.password; + } else if ( + resolvedCredentials.authType === "key" && + resolvedCredentials.sshKey + ) { + const cleanKey = resolvedCredentials.sshKey + .trim() + .replace(/\r\n/g, "\n") + .replace(/\r/g, "\n"); + config.privateKey = Buffer.from(cleanKey, "utf8"); + if (resolvedCredentials.keyPassword) { + config.passphrase = resolvedCredentials.keyPassword; + } + } + + // If we have a previous client, use it as the sock + if (currentClient) { + await new Promise((resolve, reject) => { + currentClient!.forwardOut( + "127.0.0.1", + 0, + jumpHost.ip, + jumpHost.port || 22, + (err, stream) => { + if (err) return reject(err); + config.sock = stream; + resolve(); + }, + ); + }); + } + + await new Promise((resolve, reject) => { + client.on("ready", () => resolve()); + client.on("error", reject); + client.connect(config); + }); + + currentClient = client; + } + + return currentClient; +} + +// Handle WebSocket connections +wss.on("connection", async (ws: WebSocket, req) => { + const userId = (req as any).userId; + const sessionId = `docker-console-${Date.now()}-${Math.random()}`; + + dockerConsoleLogger.info("Docker console WebSocket connected", { + operation: "ws_connect", + sessionId, + userId, + }); + + let sshSession: SSHSession | null = null; + + ws.on("message", async (data) => { + try { + const message = JSON.parse(data.toString()); + + switch (message.type) { + case "connect": { + const { hostConfig, containerId, shell, cols, rows } = + message.data as { + hostConfig: SSHHost; + containerId: string; + shell?: string; + cols?: number; + rows?: number; + }; + + if ( + typeof hostConfig.jumpHosts === "string" && + hostConfig.jumpHosts + ) { + try { + hostConfig.jumpHosts = JSON.parse(hostConfig.jumpHosts); + } catch (e) { + dockerConsoleLogger.error("Failed to parse jump hosts", e, { + hostId: hostConfig.id, + }); + hostConfig.jumpHosts = []; + } + } + + if (!hostConfig || !containerId) { + ws.send( + JSON.stringify({ + type: "error", + message: "Host configuration and container ID are required", + }), + ); + return; + } + + // Check if Docker is enabled for this host + if (!hostConfig.enableDocker) { + ws.send( + JSON.stringify({ + type: "error", + message: + "Docker is not enabled for this host. Enable it in Host Settings.", + }), + ); + return; + } + + try { + // Resolve credentials + let resolvedCredentials: any = { + password: hostConfig.password, + sshKey: hostConfig.key, + keyPassword: hostConfig.keyPassword, + authType: hostConfig.authType, + }; + + if (hostConfig.credentialId) { + const credentials = await SimpleDBOps.select( + getDb() + .select() + .from(sshCredentials) + .where( + and( + eq(sshCredentials.id, hostConfig.credentialId as number), + eq(sshCredentials.userId, userId), + ), + ), + "ssh_credentials", + userId, + ); + + if (credentials.length > 0) { + const credential = credentials[0]; + resolvedCredentials = { + password: credential.password, + sshKey: + credential.private_key || + credential.privateKey || + credential.key, + keyPassword: + credential.key_password || credential.keyPassword, + authType: credential.auth_type || credential.authType, + }; + } + } + + // Create SSH client + const client = new SSHClient(); + + const config: any = { + host: hostConfig.ip, + port: hostConfig.port || 22, + username: hostConfig.username, + tryKeyboard: true, + readyTimeout: 60000, + keepaliveInterval: 30000, + keepaliveCountMax: 120, + tcpKeepAlive: true, + tcpKeepAliveInitialDelay: 30000, + }; + + // Set authentication + if ( + resolvedCredentials.authType === "password" && + resolvedCredentials.password + ) { + config.password = resolvedCredentials.password; + } else if ( + resolvedCredentials.authType === "key" && + resolvedCredentials.sshKey + ) { + const cleanKey = resolvedCredentials.sshKey + .trim() + .replace(/\r\n/g, "\n") + .replace(/\r/g, "\n"); + config.privateKey = Buffer.from(cleanKey, "utf8"); + if (resolvedCredentials.keyPassword) { + config.passphrase = resolvedCredentials.keyPassword; + } + } + + // Handle jump hosts if configured + if (hostConfig.jumpHosts && hostConfig.jumpHosts.length > 0) { + const jumpClient = await createJumpHostChain( + hostConfig.jumpHosts, + userId, + ); + if (jumpClient) { + const stream = await new Promise((resolve, reject) => { + jumpClient.forwardOut( + "127.0.0.1", + 0, + hostConfig.ip, + hostConfig.port || 22, + (err, stream) => { + if (err) return reject(err); + resolve(stream); + }, + ); + }); + config.sock = stream; + } + } + + // Connect to SSH + await new Promise((resolve, reject) => { + client.on("ready", () => resolve()); + client.on("error", reject); + client.connect(config); + }); + + sshSession = { + client, + stream: null, + isConnected: true, + containerId, + }; + + activeSessions.set(sessionId, sshSession); + + // Detect or use provided shell + const detectedShell = + shell || (await detectShell(sshSession, containerId)); + sshSession.shell = detectedShell; + + // Create docker exec PTY + const execCommand = `docker exec -it ${containerId} /bin/${detectedShell}`; + + client.exec( + execCommand, + { + pty: { + term: "xterm-256color", + cols: cols || 80, + rows: rows || 24, + }, + }, + (err, stream) => { + if (err) { + dockerConsoleLogger.error( + "Failed to create docker exec", + err, + { + operation: "docker_exec", + sessionId, + containerId, + }, + ); + + ws.send( + JSON.stringify({ + type: "error", + message: `Failed to start console: ${err.message}`, + }), + ); + return; + } + + sshSession!.stream = stream; + + // Forward stream output to WebSocket + stream.on("data", (data: Buffer) => { + if (ws.readyState === WebSocket.OPEN) { + ws.send( + JSON.stringify({ + type: "output", + data: data.toString("utf8"), + }), + ); + } + }); + + stream.stderr.on("data", (data: Buffer) => { + if (ws.readyState === WebSocket.OPEN) { + ws.send( + JSON.stringify({ + type: "output", + data: data.toString("utf8"), + }), + ); + } + }); + + stream.on("close", () => { + if (ws.readyState === WebSocket.OPEN) { + ws.send( + JSON.stringify({ + type: "disconnected", + message: "Console session ended", + }), + ); + } + + // Cleanup + if (sshSession) { + sshSession.client.end(); + activeSessions.delete(sessionId); + } + }); + + ws.send( + JSON.stringify({ + type: "connected", + data: { shell: detectedShell }, + }), + ); + + dockerConsoleLogger.info("Docker console session started", { + operation: "console_start", + sessionId, + containerId, + shell: detectedShell, + }); + }, + ); + } catch (error) { + dockerConsoleLogger.error("Failed to connect to container", error, { + operation: "console_connect", + sessionId, + containerId: message.data.containerId, + }); + + ws.send( + JSON.stringify({ + type: "error", + message: + error instanceof Error + ? error.message + : "Failed to connect to container", + }), + ); + } + break; + } + + case "input": { + if (sshSession && sshSession.stream) { + sshSession.stream.write(message.data); + } + break; + } + + case "resize": { + if (sshSession && sshSession.stream) { + const { cols, rows } = message.data; + sshSession.stream.setWindow(rows, cols); + + dockerConsoleLogger.debug("Console resized", { + operation: "console_resize", + sessionId, + cols, + rows, + }); + } + break; + } + + case "disconnect": { + if (sshSession) { + if (sshSession.stream) { + sshSession.stream.end(); + } + sshSession.client.end(); + activeSessions.delete(sessionId); + + dockerConsoleLogger.info("Docker console disconnected", { + operation: "console_disconnect", + sessionId, + }); + + ws.send( + JSON.stringify({ + type: "disconnected", + message: "Disconnected from container", + }), + ); + } + break; + } + + case "ping": { + // Respond with pong to acknowledge keepalive + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: "pong" })); + } + break; + } + + default: + dockerConsoleLogger.warn("Unknown message type", { + operation: "ws_message", + type: message.type, + }); + } + } catch (error) { + dockerConsoleLogger.error("WebSocket message error", error, { + operation: "ws_message", + sessionId, + }); + + ws.send( + JSON.stringify({ + type: "error", + message: error instanceof Error ? error.message : "An error occurred", + }), + ); + } + }); + + ws.on("close", () => { + dockerConsoleLogger.info("WebSocket connection closed", { + operation: "ws_close", + sessionId, + }); + + // Cleanup SSH session if still active + if (sshSession) { + if (sshSession.stream) { + sshSession.stream.end(); + } + sshSession.client.end(); + activeSessions.delete(sessionId); + } + }); + + ws.on("error", (error) => { + dockerConsoleLogger.error("WebSocket error", error, { + operation: "ws_error", + sessionId, + }); + + // Cleanup + if (sshSession) { + if (sshSession.stream) { + sshSession.stream.end(); + } + sshSession.client.end(); + activeSessions.delete(sessionId); + } + }); +}); + +dockerConsoleLogger.info( + "Docker console WebSocket server started on port 30008", + { + operation: "startup", + }, +); + +// Graceful shutdown +process.on("SIGTERM", () => { + dockerConsoleLogger.info("Shutting down Docker console server...", { + operation: "shutdown", + }); + + // Close all active sessions + activeSessions.forEach((session, sessionId) => { + if (session.stream) { + session.stream.end(); + } + session.client.end(); + dockerConsoleLogger.info("Closed session during shutdown", { + operation: "shutdown", + sessionId, + }); + }); + + activeSessions.clear(); + + wss.close(() => { + dockerConsoleLogger.info("Docker console server closed", { + operation: "shutdown", + }); + process.exit(0); + }); +}); diff --git a/src/backend/ssh/docker.ts b/src/backend/ssh/docker.ts new file mode 100644 index 00000000..dff81f9c --- /dev/null +++ b/src/backend/ssh/docker.ts @@ -0,0 +1,1464 @@ +import express from "express"; +import cors from "cors"; +import cookieParser from "cookie-parser"; +import { Client as SSHClient } from "ssh2"; +import type { ClientChannel } from "ssh2"; +import { getDb } from "../database/db/index.js"; +import { sshData, sshCredentials } from "../database/db/schema.js"; +import { eq, and } from "drizzle-orm"; +import { logger } from "../utils/logger.js"; +import { SimpleDBOps } from "../utils/simple-db-ops.js"; +import { AuthManager } from "../utils/auth-manager.js"; +import type { AuthenticatedRequest, SSHHost } from "../../types/index.js"; + +// Create dedicated logger for Docker operations +const dockerLogger = logger; + +// SSH Session Management +interface SSHSession { + client: SSHClient; + isConnected: boolean; + lastActive: number; + timeout?: NodeJS.Timeout; + activeOperations: number; + hostId?: number; +} + +const sshSessions: Record = {}; + +// Session cleanup with 60-minute idle timeout +const SESSION_IDLE_TIMEOUT = 60 * 60 * 1000; + +function cleanupSession(sessionId: string) { + const session = sshSessions[sessionId]; + if (session) { + if (session.activeOperations > 0) { + dockerLogger.warn( + `Deferring session cleanup for ${sessionId} - ${session.activeOperations} active operations`, + { + operation: "cleanup_deferred", + sessionId, + activeOperations: session.activeOperations, + }, + ); + scheduleSessionCleanup(sessionId); + return; + } + + try { + session.client.end(); + } catch (error) { + dockerLogger.debug("Error ending SSH client during cleanup", { error }); + } + clearTimeout(session.timeout); + delete sshSessions[sessionId]; + dockerLogger.info("Docker SSH session cleaned up", { + operation: "session_cleanup", + sessionId, + }); + } +} + +function scheduleSessionCleanup(sessionId: string) { + const session = sshSessions[sessionId]; + if (session) { + if (session.timeout) clearTimeout(session.timeout); + + session.timeout = setTimeout(() => { + cleanupSession(sessionId); + }, SESSION_IDLE_TIMEOUT); + } +} + +// Helper function to resolve jump host +async function resolveJumpHost( + hostId: number, + userId: string, +): Promise { + try { + const hosts = await SimpleDBOps.select( + getDb() + .select() + .from(sshData) + .where(and(eq(sshData.id, hostId), eq(sshData.userId, userId))), + "ssh_data", + userId, + ); + + if (hosts.length === 0) { + return null; + } + + const host = hosts[0]; + + if (host.credentialId) { + const credentials = await SimpleDBOps.select( + getDb() + .select() + .from(sshCredentials) + .where( + and( + eq(sshCredentials.id, host.credentialId as number), + eq(sshCredentials.userId, userId), + ), + ), + "ssh_credentials", + userId, + ); + + if (credentials.length > 0) { + const credential = credentials[0]; + return { + ...host, + password: credential.password, + key: + credential.private_key || credential.privateKey || credential.key, + keyPassword: credential.key_password || credential.keyPassword, + keyType: credential.key_type || credential.keyType, + authType: credential.auth_type || credential.authType, + }; + } + } + + return host; + } catch (error) { + dockerLogger.error("Failed to resolve jump host", error, { + operation: "resolve_jump_host", + hostId, + userId, + }); + return null; + } +} + +// Helper function to create jump host chain +async function createJumpHostChain( + jumpHosts: Array<{ hostId: number }>, + userId: string, +): Promise { + if (!jumpHosts || jumpHosts.length === 0) { + return null; + } + + let currentClient: SSHClient | null = null; + const clients: SSHClient[] = []; + + try { + for (let i = 0; i < jumpHosts.length; i++) { + const jumpHostConfig = await resolveJumpHost(jumpHosts[i].hostId, userId); + + if (!jumpHostConfig) { + dockerLogger.error(`Jump host ${i + 1} not found`, undefined, { + operation: "jump_host_chain", + hostId: jumpHosts[i].hostId, + }); + clients.forEach((c) => c.end()); + return null; + } + + const jumpClient = new SSHClient(); + clients.push(jumpClient); + + const connected = await new Promise((resolve) => { + const timeout = setTimeout(() => { + resolve(false); + }, 30000); + + jumpClient.on("ready", () => { + clearTimeout(timeout); + resolve(true); + }); + + jumpClient.on("error", (err) => { + clearTimeout(timeout); + dockerLogger.error(`Jump host ${i + 1} connection failed`, err, { + operation: "jump_host_connect", + hostId: jumpHostConfig.id, + ip: jumpHostConfig.ip, + }); + resolve(false); + }); + + const connectConfig: any = { + host: jumpHostConfig.ip, + port: jumpHostConfig.port || 22, + username: jumpHostConfig.username, + tryKeyboard: true, + readyTimeout: 30000, + }; + + if (jumpHostConfig.authType === "password" && jumpHostConfig.password) { + connectConfig.password = jumpHostConfig.password; + } else if (jumpHostConfig.authType === "key" && jumpHostConfig.key) { + const cleanKey = jumpHostConfig.key + .trim() + .replace(/\r\n/g, "\n") + .replace(/\r/g, "\n"); + connectConfig.privateKey = Buffer.from(cleanKey, "utf8"); + if (jumpHostConfig.keyPassword) { + connectConfig.passphrase = jumpHostConfig.keyPassword; + } + } + + if (currentClient) { + currentClient.forwardOut( + "127.0.0.1", + 0, + jumpHostConfig.ip, + jumpHostConfig.port || 22, + (err, stream) => { + if (err) { + clearTimeout(timeout); + resolve(false); + return; + } + connectConfig.sock = stream; + jumpClient.connect(connectConfig); + }, + ); + } else { + jumpClient.connect(connectConfig); + } + }); + + if (!connected) { + clients.forEach((c) => c.end()); + return null; + } + + currentClient = jumpClient; + } + + return currentClient; + } catch (error) { + dockerLogger.error("Failed to create jump host chain", error, { + operation: "jump_host_chain", + }); + clients.forEach((c) => c.end()); + return null; + } +} + +// Helper function to execute Docker CLI commands +async function executeDockerCommand( + session: SSHSession, + command: string, +): Promise { + return new Promise((resolve, reject) => { + session.client.exec(command, (err, stream) => { + if (err) { + dockerLogger.error("Docker command execution error", err, { + operation: "execute_docker_command", + command, + }); + return reject(err); + } + + let stdout = ""; + let stderr = ""; + + stream.on("close", (code: number) => { + if (code !== 0) { + dockerLogger.error("Docker command failed", undefined, { + operation: "execute_docker_command", + command, + exitCode: code, + stderr, + }); + reject(new Error(stderr || `Command exited with code ${code}`)); + } else { + resolve(stdout); + } + }); + + stream.on("data", (data: Buffer) => { + stdout += data.toString(); + }); + + stream.stderr.on("data", (data: Buffer) => { + stderr += data.toString(); + }); + + stream.on("error", (streamErr: Error) => { + dockerLogger.error("Docker command stream error", streamErr, { + operation: "execute_docker_command", + command, + }); + reject(streamErr); + }); + }); + }); +} + +// Express app setup +const app = express(); + +app.use( + cors({ + origin: (origin, callback) => { + if (!origin) return callback(null, true); + + const allowedOrigins = [ + "http://localhost:5173", + "http://localhost:3000", + "http://127.0.0.1:5173", + "http://127.0.0.1:3000", + ]; + + if (origin.startsWith("https://")) { + return callback(null, true); + } + + if (origin.startsWith("http://")) { + return callback(null, true); + } + + if (allowedOrigins.includes(origin)) { + return callback(null, true); + } + + callback(new Error("Not allowed by CORS")); + }, + credentials: true, + methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + allowedHeaders: [ + "Content-Type", + "Authorization", + "User-Agent", + "X-Electron-App", + ], + }), +); + +app.use(cookieParser()); +app.use(express.json({ limit: "100mb" })); +app.use(express.urlencoded({ limit: "100mb", extended: true })); + +// Initialize AuthManager and apply middleware +const authManager = AuthManager.getInstance(); +app.use(authManager.createAuthMiddleware()); + +// Session management endpoints + +// POST /docker/ssh/connect - Establish SSH session +app.post("/docker/ssh/connect", async (req, res) => { + const { sessionId, hostId } = req.body; + const userId = (req as any).userId; + + if (!userId) { + dockerLogger.error( + "Docker SSH connection rejected: no authenticated user", + { + operation: "docker_connect_auth", + sessionId, + }, + ); + return res.status(401).json({ error: "Authentication required" }); + } + + if (!SimpleDBOps.isUserDataUnlocked(userId)) { + return res.status(401).json({ + error: "Session expired - please log in again", + code: "SESSION_EXPIRED", + }); + } + + if (!sessionId || !hostId) { + dockerLogger.warn("Missing Docker SSH connection parameters", { + operation: "docker_connect", + sessionId, + hasHostId: !!hostId, + }); + return res.status(400).json({ error: "Missing sessionId or hostId" }); + } + + try { + // Get host configuration + const hosts = await SimpleDBOps.select( + getDb() + .select() + .from(sshData) + .where(and(eq(sshData.id, hostId), eq(sshData.userId, userId))), + "ssh_data", + userId, + ); + + if (hosts.length === 0) { + return res.status(404).json({ error: "Host not found" }); + } + + const host = hosts[0] as unknown as SSHHost; + if (typeof host.jumpHosts === "string" && host.jumpHosts) { + try { + host.jumpHosts = JSON.parse(host.jumpHosts); + } catch (e) { + dockerLogger.error("Failed to parse jump hosts", e, { + hostId: host.id, + }); + host.jumpHosts = []; + } + } + + // Check if Docker is enabled for this host + if (!host.enableDocker) { + dockerLogger.warn("Docker not enabled for host", { + operation: "docker_connect", + hostId, + userId, + }); + return res.status(403).json({ + error: + "Docker is not enabled for this host. Enable it in Host Settings.", + code: "DOCKER_DISABLED", + }); + } + + // Clean up existing session if any + if (sshSessions[sessionId]) { + cleanupSession(sessionId); + } + + // Resolve credentials + let resolvedCredentials: any = { + password: host.password, + sshKey: host.key, + keyPassword: host.keyPassword, + authType: host.authType, + }; + + if (host.credentialId) { + const credentials = await SimpleDBOps.select( + getDb() + .select() + .from(sshCredentials) + .where( + and( + eq(sshCredentials.id, host.credentialId as number), + eq(sshCredentials.userId, userId), + ), + ), + "ssh_credentials", + userId, + ); + + if (credentials.length > 0) { + const credential = credentials[0]; + resolvedCredentials = { + password: credential.password, + sshKey: + credential.private_key || credential.privateKey || credential.key, + keyPassword: credential.key_password || credential.keyPassword, + authType: credential.auth_type || credential.authType, + }; + } + } + + // Create SSH client + const client = new SSHClient(); + + const config: any = { + host: host.ip, + port: host.port || 22, + username: host.username, + tryKeyboard: true, + keepaliveInterval: 30000, + keepaliveCountMax: 3, + readyTimeout: 60000, + tcpKeepAlive: true, + tcpKeepAliveInitialDelay: 30000, + }; + + // Set authentication + if ( + resolvedCredentials.authType === "password" && + resolvedCredentials.password + ) { + config.password = resolvedCredentials.password; + } else if ( + resolvedCredentials.authType === "key" && + resolvedCredentials.sshKey + ) { + const cleanKey = resolvedCredentials.sshKey + .trim() + .replace(/\r\n/g, "\n") + .replace(/\r/g, "\n"); + config.privateKey = Buffer.from(cleanKey, "utf8"); + if (resolvedCredentials.keyPassword) { + config.passphrase = resolvedCredentials.keyPassword; + } + } + + let responseSent = false; + + client.on("ready", () => { + if (responseSent) return; + responseSent = true; + + sshSessions[sessionId] = { + client, + isConnected: true, + lastActive: Date.now(), + activeOperations: 0, + hostId, + }; + + scheduleSessionCleanup(sessionId); + + dockerLogger.info("Docker SSH session established", { + operation: "docker_connect", + sessionId, + hostId, + userId, + }); + + res.json({ success: true, message: "SSH connection established" }); + }); + + client.on("error", (err) => { + if (responseSent) return; + responseSent = true; + + dockerLogger.error("Docker SSH connection failed", err, { + operation: "docker_connect", + sessionId, + hostId, + userId, + }); + + res.status(500).json({ + success: false, + message: err.message || "SSH connection failed", + }); + }); + + client.on("close", () => { + if (sshSessions[sessionId]) { + sshSessions[sessionId].isConnected = false; + cleanupSession(sessionId); + } + }); + + // Handle jump hosts if configured + if (host.jumpHosts && host.jumpHosts.length > 0) { + const jumpClient = await createJumpHostChain( + host.jumpHosts as Array<{ hostId: number }>, + userId, + ); + + if (!jumpClient) { + return res.status(500).json({ + error: "Failed to establish jump host chain", + }); + } + + jumpClient.forwardOut( + "127.0.0.1", + 0, + host.ip, + host.port || 22, + (err, stream) => { + if (err) { + dockerLogger.error("Failed to forward through jump host", err, { + operation: "docker_jump_forward", + sessionId, + hostId, + }); + jumpClient.end(); + if (!responseSent) { + responseSent = true; + return res.status(500).json({ + error: "Failed to forward through jump host: " + err.message, + }); + } + return; + } + + config.sock = stream; + client.connect(config); + }, + ); + } else { + client.connect(config); + } + } catch (error) { + dockerLogger.error("Docker SSH connection error", error, { + operation: "docker_connect", + sessionId, + hostId, + userId, + }); + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : "Unknown error", + }); + } +}); + +// POST /docker/ssh/disconnect - Close SSH session +app.post("/docker/ssh/disconnect", async (req, res) => { + const { sessionId } = req.body; + + if (!sessionId) { + return res.status(400).json({ error: "Session ID is required" }); + } + + cleanupSession(sessionId); + + dockerLogger.info("Docker SSH session disconnected", { + operation: "docker_disconnect", + sessionId, + }); + + res.json({ success: true, message: "SSH session disconnected" }); +}); + +// POST /docker/ssh/keepalive - Keep session alive +app.post("/docker/ssh/keepalive", async (req, res) => { + const { sessionId } = req.body; + + if (!sessionId) { + return res.status(400).json({ error: "Session ID is required" }); + } + + const session = sshSessions[sessionId]; + + if (!session || !session.isConnected) { + return res.status(400).json({ + error: "SSH session not found or not connected", + connected: false, + }); + } + + session.lastActive = Date.now(); + scheduleSessionCleanup(sessionId); + + res.json({ + success: true, + connected: true, + message: "Session keepalive successful", + lastActive: session.lastActive, + }); +}); + +// GET /docker/ssh/status - Check session status +app.get("/docker/ssh/status", async (req, res) => { + const sessionId = req.query.sessionId as string; + + if (!sessionId) { + return res.status(400).json({ error: "Session ID is required" }); + } + + const isConnected = !!sshSessions[sessionId]?.isConnected; + + res.json({ success: true, connected: isConnected }); +}); + +// GET /docker/validate/:sessionId - Validate Docker availability +app.get("/docker/validate/:sessionId", async (req, res) => { + const { sessionId } = req.params; + const userId = (req as any).userId; + + if (!userId) { + return res.status(401).json({ error: "Authentication required" }); + } + + const session = sshSessions[sessionId]; + + if (!session || !session.isConnected) { + return res.status(400).json({ + error: "SSH session not found or not connected", + }); + } + + session.lastActive = Date.now(); + session.activeOperations++; + + try { + // Check if Docker is installed + try { + const versionOutput = await executeDockerCommand( + session, + "docker --version", + ); + const versionMatch = versionOutput.match(/Docker version ([^\s,]+)/); + const version = versionMatch ? versionMatch[1] : "unknown"; + + // Check if Docker daemon is running + try { + await executeDockerCommand(session, "docker ps >/dev/null 2>&1"); + + session.activeOperations--; + return res.json({ + available: true, + version, + }); + } catch (daemonError) { + session.activeOperations--; + const errorMsg = + daemonError instanceof Error ? daemonError.message : ""; + + if (errorMsg.includes("Cannot connect to the Docker daemon")) { + return res.json({ + available: false, + error: + "Docker daemon is not running. Start it with: sudo systemctl start docker", + code: "DAEMON_NOT_RUNNING", + }); + } + + if (errorMsg.includes("permission denied")) { + return res.json({ + available: false, + error: + "Permission denied. Add your user to the docker group: sudo usermod -aG docker $USER", + code: "PERMISSION_DENIED", + }); + } + + return res.json({ + available: false, + error: errorMsg, + code: "DOCKER_ERROR", + }); + } + } catch (installError) { + session.activeOperations--; + return res.json({ + available: false, + error: + "Docker is not installed on this host. Please install Docker to use this feature.", + code: "NOT_INSTALLED", + }); + } + } catch (error) { + session.activeOperations--; + dockerLogger.error("Docker validation error", error, { + operation: "docker_validate", + sessionId, + userId, + }); + + res.status(500).json({ + available: false, + error: error instanceof Error ? error.message : "Validation failed", + }); + } +}); + +// GET /docker/containers/:sessionId - List all containers +app.get("/docker/containers/:sessionId", async (req, res) => { + const { sessionId } = req.params; + const all = req.query.all !== "false"; // Default to true + const userId = (req as any).userId; + + if (!userId) { + return res.status(401).json({ error: "Authentication required" }); + } + + const session = sshSessions[sessionId]; + + if (!session || !session.isConnected) { + return res.status(400).json({ + error: "SSH session not found or not connected", + }); + } + + session.lastActive = Date.now(); + session.activeOperations++; + + try { + const allFlag = all ? "-a " : ""; + const command = `docker ps ${allFlag}--format '{"id":"{{.ID}}","name":"{{.Names}}","image":"{{.Image}}","status":"{{.Status}}","state":"{{.State}}","ports":"{{.Ports}}","created":"{{.CreatedAt}}"}'`; + + const output = await executeDockerCommand(session, command); + + const containers = output + .split("\n") + .filter((line) => line.trim()) + .map((line) => { + try { + return JSON.parse(line); + } catch (e) { + dockerLogger.warn("Failed to parse container line", { + operation: "parse_container", + line, + }); + return null; + } + }) + .filter((c) => c !== null); + + session.activeOperations--; + + res.json(containers); + } catch (error) { + session.activeOperations--; + dockerLogger.error("Failed to list Docker containers", error, { + operation: "list_containers", + sessionId, + userId, + }); + + res.status(500).json({ + error: + error instanceof Error ? error.message : "Failed to list containers", + }); + } +}); + +// GET /docker/containers/:sessionId/:containerId - Get container details +app.get("/docker/containers/:sessionId/:containerId", async (req, res) => { + const { sessionId, containerId } = req.params; + const userId = (req as any).userId; + + if (!userId) { + return res.status(401).json({ error: "Authentication required" }); + } + + const session = sshSessions[sessionId]; + + if (!session || !session.isConnected) { + return res.status(400).json({ + error: "SSH session not found or not connected", + }); + } + + session.lastActive = Date.now(); + session.activeOperations++; + + try { + const command = `docker inspect ${containerId}`; + const output = await executeDockerCommand(session, command); + const details = JSON.parse(output); + + session.activeOperations--; + + if (details && details.length > 0) { + res.json(details[0]); + } else { + res.status(404).json({ + error: "Container not found", + code: "CONTAINER_NOT_FOUND", + }); + } + } catch (error) { + session.activeOperations--; + + const errorMsg = error instanceof Error ? error.message : ""; + if (errorMsg.includes("No such container")) { + return res.status(404).json({ + error: "Container not found", + code: "CONTAINER_NOT_FOUND", + }); + } + + dockerLogger.error("Failed to get container details", error, { + operation: "get_container_details", + sessionId, + containerId, + userId, + }); + + res.status(500).json({ + error: errorMsg || "Failed to get container details", + }); + } +}); + +// POST /docker/containers/:sessionId/:containerId/start - Start container +app.post( + "/docker/containers/:sessionId/:containerId/start", + async (req, res) => { + const { sessionId, containerId } = req.params; + const userId = (req as any).userId; + + if (!userId) { + return res.status(401).json({ error: "Authentication required" }); + } + + const session = sshSessions[sessionId]; + + if (!session || !session.isConnected) { + return res.status(400).json({ + error: "SSH session not found or not connected", + }); + } + + session.lastActive = Date.now(); + session.activeOperations++; + + try { + await executeDockerCommand(session, `docker start ${containerId}`); + + session.activeOperations--; + + dockerLogger.info("Container started", { + operation: "start_container", + sessionId, + containerId, + userId, + }); + + res.json({ + success: true, + message: "Container started successfully", + }); + } catch (error) { + session.activeOperations--; + + const errorMsg = error instanceof Error ? error.message : ""; + if (errorMsg.includes("No such container")) { + return res.status(404).json({ + success: false, + error: "Container not found", + code: "CONTAINER_NOT_FOUND", + }); + } + + dockerLogger.error("Failed to start container", error, { + operation: "start_container", + sessionId, + containerId, + userId, + }); + + res.status(500).json({ + success: false, + error: errorMsg || "Failed to start container", + }); + } + }, +); + +// POST /docker/containers/:sessionId/:containerId/stop - Stop container +app.post( + "/docker/containers/:sessionId/:containerId/stop", + async (req, res) => { + const { sessionId, containerId } = req.params; + const userId = (req as any).userId; + + if (!userId) { + return res.status(401).json({ error: "Authentication required" }); + } + + const session = sshSessions[sessionId]; + + if (!session || !session.isConnected) { + return res.status(400).json({ + error: "SSH session not found or not connected", + }); + } + + session.lastActive = Date.now(); + session.activeOperations++; + + try { + await executeDockerCommand(session, `docker stop ${containerId}`); + + session.activeOperations--; + + dockerLogger.info("Container stopped", { + operation: "stop_container", + sessionId, + containerId, + userId, + }); + + res.json({ + success: true, + message: "Container stopped successfully", + }); + } catch (error) { + session.activeOperations--; + + const errorMsg = error instanceof Error ? error.message : ""; + if (errorMsg.includes("No such container")) { + return res.status(404).json({ + success: false, + error: "Container not found", + code: "CONTAINER_NOT_FOUND", + }); + } + + dockerLogger.error("Failed to stop container", error, { + operation: "stop_container", + sessionId, + containerId, + userId, + }); + + res.status(500).json({ + success: false, + error: errorMsg || "Failed to stop container", + }); + } + }, +); + +// POST /docker/containers/:sessionId/:containerId/restart - Restart container +app.post( + "/docker/containers/:sessionId/:containerId/restart", + async (req, res) => { + const { sessionId, containerId } = req.params; + const userId = (req as any).userId; + + if (!userId) { + return res.status(401).json({ error: "Authentication required" }); + } + + const session = sshSessions[sessionId]; + + if (!session || !session.isConnected) { + return res.status(400).json({ + error: "SSH session not found or not connected", + }); + } + + session.lastActive = Date.now(); + session.activeOperations++; + + try { + await executeDockerCommand(session, `docker restart ${containerId}`); + + session.activeOperations--; + + dockerLogger.info("Container restarted", { + operation: "restart_container", + sessionId, + containerId, + userId, + }); + + res.json({ + success: true, + message: "Container restarted successfully", + }); + } catch (error) { + session.activeOperations--; + + const errorMsg = error instanceof Error ? error.message : ""; + if (errorMsg.includes("No such container")) { + return res.status(404).json({ + success: false, + error: "Container not found", + code: "CONTAINER_NOT_FOUND", + }); + } + + dockerLogger.error("Failed to restart container", error, { + operation: "restart_container", + sessionId, + containerId, + userId, + }); + + res.status(500).json({ + success: false, + error: errorMsg || "Failed to restart container", + }); + } + }, +); + +// POST /docker/containers/:sessionId/:containerId/pause - Pause container +app.post( + "/docker/containers/:sessionId/:containerId/pause", + async (req, res) => { + const { sessionId, containerId } = req.params; + const userId = (req as any).userId; + + if (!userId) { + return res.status(401).json({ error: "Authentication required" }); + } + + const session = sshSessions[sessionId]; + + if (!session || !session.isConnected) { + return res.status(400).json({ + error: "SSH session not found or not connected", + }); + } + + session.lastActive = Date.now(); + session.activeOperations++; + + try { + await executeDockerCommand(session, `docker pause ${containerId}`); + + session.activeOperations--; + + dockerLogger.info("Container paused", { + operation: "pause_container", + sessionId, + containerId, + userId, + }); + + res.json({ + success: true, + message: "Container paused successfully", + }); + } catch (error) { + session.activeOperations--; + + const errorMsg = error instanceof Error ? error.message : ""; + if (errorMsg.includes("No such container")) { + return res.status(404).json({ + success: false, + error: "Container not found", + code: "CONTAINER_NOT_FOUND", + }); + } + + dockerLogger.error("Failed to pause container", error, { + operation: "pause_container", + sessionId, + containerId, + userId, + }); + + res.status(500).json({ + success: false, + error: errorMsg || "Failed to pause container", + }); + } + }, +); + +// POST /docker/containers/:sessionId/:containerId/unpause - Unpause container +app.post( + "/docker/containers/:sessionId/:containerId/unpause", + async (req, res) => { + const { sessionId, containerId } = req.params; + const userId = (req as any).userId; + + if (!userId) { + return res.status(401).json({ error: "Authentication required" }); + } + + const session = sshSessions[sessionId]; + + if (!session || !session.isConnected) { + return res.status(400).json({ + error: "SSH session not found or not connected", + }); + } + + session.lastActive = Date.now(); + session.activeOperations++; + + try { + await executeDockerCommand(session, `docker unpause ${containerId}`); + + session.activeOperations--; + + dockerLogger.info("Container unpaused", { + operation: "unpause_container", + sessionId, + containerId, + userId, + }); + + res.json({ + success: true, + message: "Container unpaused successfully", + }); + } catch (error) { + session.activeOperations--; + + const errorMsg = error instanceof Error ? error.message : ""; + if (errorMsg.includes("No such container")) { + return res.status(404).json({ + success: false, + error: "Container not found", + code: "CONTAINER_NOT_FOUND", + }); + } + + dockerLogger.error("Failed to unpause container", error, { + operation: "unpause_container", + sessionId, + containerId, + userId, + }); + + res.status(500).json({ + success: false, + error: errorMsg || "Failed to unpause container", + }); + } + }, +); + +// DELETE /docker/containers/:sessionId/:containerId/remove - Remove container +app.delete( + "/docker/containers/:sessionId/:containerId/remove", + async (req, res) => { + const { sessionId, containerId } = req.params; + const force = req.query.force === "true"; + const userId = (req as any).userId; + + if (!userId) { + return res.status(401).json({ error: "Authentication required" }); + } + + const session = sshSessions[sessionId]; + + if (!session || !session.isConnected) { + return res.status(400).json({ + error: "SSH session not found or not connected", + }); + } + + session.lastActive = Date.now(); + session.activeOperations++; + + try { + const forceFlag = force ? "-f " : ""; + await executeDockerCommand( + session, + `docker rm ${forceFlag}${containerId}`, + ); + + session.activeOperations--; + + dockerLogger.info("Container removed", { + operation: "remove_container", + sessionId, + containerId, + force, + userId, + }); + + res.json({ + success: true, + message: "Container removed successfully", + }); + } catch (error) { + session.activeOperations--; + + const errorMsg = error instanceof Error ? error.message : ""; + if (errorMsg.includes("No such container")) { + return res.status(404).json({ + success: false, + error: "Container not found", + code: "CONTAINER_NOT_FOUND", + }); + } + + if (errorMsg.includes("cannot remove a running container")) { + return res.status(400).json({ + success: false, + error: + "Cannot remove a running container. Stop it first or use force.", + code: "CONTAINER_RUNNING", + }); + } + + dockerLogger.error("Failed to remove container", error, { + operation: "remove_container", + sessionId, + containerId, + userId, + }); + + res.status(500).json({ + success: false, + error: errorMsg || "Failed to remove container", + }); + } + }, +); + +// GET /docker/containers/:sessionId/:containerId/logs - Get container logs +app.get("/docker/containers/:sessionId/:containerId/logs", async (req, res) => { + const { sessionId, containerId } = req.params; + const tail = req.query.tail ? parseInt(req.query.tail as string) : 100; + const timestamps = req.query.timestamps === "true"; + const since = req.query.since as string; + const until = req.query.until as string; + const userId = (req as any).userId; + + if (!userId) { + return res.status(401).json({ error: "Authentication required" }); + } + + const session = sshSessions[sessionId]; + + if (!session || !session.isConnected) { + return res.status(400).json({ + error: "SSH session not found or not connected", + }); + } + + session.lastActive = Date.now(); + session.activeOperations++; + + try { + let command = `docker logs ${containerId}`; + + if (tail && tail > 0) { + command += ` --tail ${tail}`; + } + + if (timestamps) { + command += " --timestamps"; + } + + if (since) { + command += ` --since ${since}`; + } + + if (until) { + command += ` --until ${until}`; + } + + const logs = await executeDockerCommand(session, command); + + session.activeOperations--; + + res.json({ + success: true, + logs, + }); + } catch (error) { + session.activeOperations--; + + const errorMsg = error instanceof Error ? error.message : ""; + if (errorMsg.includes("No such container")) { + return res.status(404).json({ + success: false, + error: "Container not found", + code: "CONTAINER_NOT_FOUND", + }); + } + + dockerLogger.error("Failed to get container logs", error, { + operation: "get_logs", + sessionId, + containerId, + userId, + }); + + res.status(500).json({ + success: false, + error: errorMsg || "Failed to get container logs", + }); + } +}); + +// GET /docker/containers/:sessionId/:containerId/stats - Get container stats +app.get( + "/docker/containers/:sessionId/:containerId/stats", + async (req, res) => { + const { sessionId, containerId } = req.params; + const userId = (req as any).userId; + + if (!userId) { + return res.status(401).json({ error: "Authentication required" }); + } + + const session = sshSessions[sessionId]; + + if (!session || !session.isConnected) { + return res.status(400).json({ + error: "SSH session not found or not connected", + }); + } + + session.lastActive = Date.now(); + session.activeOperations++; + + try { + const command = `docker stats ${containerId} --no-stream --format '{"cpu":"{{.CPUPerc}}","memory":"{{.MemUsage}}","memoryPercent":"{{.MemPerc}}","netIO":"{{.NetIO}}","blockIO":"{{.BlockIO}}","pids":"{{.PIDs}}"}'`; + + const output = await executeDockerCommand(session, command); + const rawStats = JSON.parse(output.trim()); + + // Parse memory usage (e.g., "1.5GiB / 8GiB" -> { used: "1.5GiB", limit: "8GiB" }) + const memoryParts = rawStats.memory.split(" / "); + const memoryUsed = memoryParts[0]?.trim() || "0B"; + const memoryLimit = memoryParts[1]?.trim() || "0B"; + + // Parse network I/O (e.g., "1.5MB / 2.3MB" -> { input: "1.5MB", output: "2.3MB" }) + const netIOParts = rawStats.netIO.split(" / "); + const netInput = netIOParts[0]?.trim() || "0B"; + const netOutput = netIOParts[1]?.trim() || "0B"; + + // Parse block I/O (e.g., "10MB / 5MB" -> { read: "10MB", write: "5MB" }) + const blockIOParts = rawStats.blockIO.split(" / "); + const blockRead = blockIOParts[0]?.trim() || "0B"; + const blockWrite = blockIOParts[1]?.trim() || "0B"; + + const stats = { + cpu: rawStats.cpu, + memoryUsed, + memoryLimit, + memoryPercent: rawStats.memoryPercent, + netInput, + netOutput, + blockRead, + blockWrite, + pids: rawStats.pids, + }; + + session.activeOperations--; + + res.json(stats); + } catch (error) { + session.activeOperations--; + + const errorMsg = error instanceof Error ? error.message : ""; + if (errorMsg.includes("No such container")) { + return res.status(404).json({ + success: false, + error: "Container not found", + code: "CONTAINER_NOT_FOUND", + }); + } + + dockerLogger.error("Failed to get container stats", error, { + operation: "get_stats", + sessionId, + containerId, + userId, + }); + + res.status(500).json({ + success: false, + error: errorMsg || "Failed to get container stats", + }); + } + }, +); + +// Start server +const PORT = 30007; + +app.listen(PORT, async () => { + try { + await authManager.initialize(); + dockerLogger.info(`Docker backend server started on port ${PORT}`); + } catch (err) { + dockerLogger.error("Failed to initialize Docker backend", err, { + operation: "startup", + }); + } +}); + +// Graceful shutdown +process.on("SIGINT", () => { + dockerLogger.info("Shutting down Docker backend"); + Object.keys(sshSessions).forEach((sessionId) => { + cleanupSession(sessionId); + }); + process.exit(0); +}); + +process.on("SIGTERM", () => { + dockerLogger.info("Shutting down Docker backend"); + Object.keys(sshSessions).forEach((sessionId) => { + cleanupSession(sessionId); + }); + process.exit(0); +}); diff --git a/src/backend/ssh/terminal.ts b/src/backend/ssh/terminal.ts index 1a67c853..b41ebab3 100644 --- a/src/backend/ssh/terminal.ts +++ b/src/backend/ssh/terminal.ts @@ -323,7 +323,6 @@ wss.on("connection", async (ws: WebSocket, req) => { let sshConn: Client | null = null; let sshStream: ClientChannel | null = null; - let pingInterval: NodeJS.Timeout | null = null; let keyboardInteractiveFinish: ((responses: string[]) => void) | null = null; let totpPromptSent = false; let isKeyboardInteractive = false; @@ -809,8 +808,6 @@ wss.on("connection", async (ws: WebSocket, req) => { ); }); - setupPingInterval(); - if (initialPath && initialPath.trim() !== "") { const cdCommand = `cd "${initialPath.replace(/"/g, '\\"')}" && pwd\n`; stream.write(cdCommand); @@ -1333,11 +1330,6 @@ wss.on("connection", async (ws: WebSocket, req) => { clearTimeout(timeoutId); } - if (pingInterval) { - clearInterval(pingInterval); - pingInterval = null; - } - if (sshStream) { try { sshStream.end(); @@ -1374,24 +1366,12 @@ wss.on("connection", async (ws: WebSocket, req) => { }, 100); } - function setupPingInterval() { - pingInterval = setInterval(() => { - if (sshConn && sshStream) { - try { - sshStream.write("\x00"); - } catch (e: unknown) { - sshLogger.error( - "SSH keepalive failed: " + - (e instanceof Error ? e.message : "Unknown error"), - ); - cleanupSSH(); - } - } else if (!sshConn || !sshStream) { - if (pingInterval) { - clearInterval(pingInterval); - pingInterval = null; - } - } - }, 30000); - } + // Note: PTY-level keepalive (writing \x00 to the stream) was removed. + // It was causing ^@ characters to appear in terminals with echoctl enabled. + // SSH-level keepalive is configured via connectConfig (keepaliveInterval, + // keepaliveCountMax, tcpKeepAlive), which handles connection health monitoring + // without producing visible output on the terminal. + // + // See: https://github.com/Termix-SSH/Support/issues/232 + // See: https://github.com/Termix-SSH/Support/issues/309 }); diff --git a/src/backend/starter.ts b/src/backend/starter.ts index b74c9b11..10bb8802 100644 --- a/src/backend/starter.ts +++ b/src/backend/starter.ts @@ -102,6 +102,8 @@ import { systemLogger, versionLogger } from "./utils/logger.js"; await import("./ssh/tunnel.js"); await import("./ssh/file-manager.js"); await import("./ssh/server-stats.js"); + await import("./ssh/docker.js"); + await import("./ssh/docker-console.js"); await import("./dashboard.js"); process.on("SIGINT", () => { diff --git a/src/backend/utils/logger.ts b/src/backend/utils/logger.ts index 41f44982..cb5ff611 100644 --- a/src/backend/utils/logger.ts +++ b/src/backend/utils/logger.ts @@ -36,7 +36,7 @@ const SENSITIVE_FIELDS = [ const TRUNCATE_FIELDS = ["data", "content", "body", "response", "request"]; -class Logger { +export class Logger { private serviceName: string; private serviceIcon: string; private serviceColor: string; diff --git a/src/backend/utils/permission-manager.ts b/src/backend/utils/permission-manager.ts new file mode 100644 index 00000000..3bf470da --- /dev/null +++ b/src/backend/utils/permission-manager.ts @@ -0,0 +1,456 @@ +import type { Request, Response, NextFunction } from "express"; +import { db } from "../database/db/index.js"; +import { + hostAccess, + roles, + userRoles, + sshData, + users, +} from "../database/db/schema.js"; +import { eq, and, or, isNull, gte, sql } from "drizzle-orm"; +import { databaseLogger } from "./logger.js"; + +interface AuthenticatedRequest extends Request { + userId?: string; + dataKey?: Buffer; +} + +interface HostAccessInfo { + hasAccess: boolean; + isOwner: boolean; + isShared: boolean; + permissionLevel?: string; + expiresAt?: string | null; +} + +interface PermissionCheckResult { + allowed: boolean; + reason?: string; +} + +class PermissionManager { + private static instance: PermissionManager; + private permissionCache: Map; + private readonly CACHE_TTL = 5 * 60 * 1000; // 5 minutes + + private constructor() { + this.permissionCache = new Map(); + + // Auto-cleanup expired host access every 1 minute + setInterval( + () => { + this.cleanupExpiredAccess().catch((error) => { + databaseLogger.error( + "Failed to run periodic host access cleanup", + error, + { + operation: "host_access_cleanup_periodic", + }, + ); + }); + }, + 60 * 1000, + ); + + // Clear permission cache every 5 minutes + setInterval( + () => { + this.clearPermissionCache(); + }, + this.CACHE_TTL, + ); + } + + static getInstance(): PermissionManager { + if (!this.instance) { + this.instance = new PermissionManager(); + } + return this.instance; + } + + /** + * Clean up expired host access entries + */ + private async cleanupExpiredAccess(): Promise { + try { + const now = new Date().toISOString(); + const result = await db + .delete(hostAccess) + .where( + and( + sql`${hostAccess.expiresAt} IS NOT NULL`, + sql`${hostAccess.expiresAt} <= ${now}`, + ), + ) + .returning({ id: hostAccess.id }); + + if (result.length > 0) { + databaseLogger.info("Cleaned up expired host access", { + operation: "host_access_cleanup", + count: result.length, + }); + } + } catch (error) { + databaseLogger.error("Failed to cleanup expired host access", error, { + operation: "host_access_cleanup_failed", + }); + } + } + + /** + * Clear permission cache + */ + private clearPermissionCache(): void { + this.permissionCache.clear(); + } + + /** + * Invalidate permission cache for a specific user + */ + invalidateUserPermissionCache(userId: string): void { + this.permissionCache.delete(userId); + } + + /** + * Get user permissions from roles + */ + async getUserPermissions(userId: string): Promise { + // Check cache first + const cached = this.permissionCache.get(userId); + if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) { + return cached.permissions; + } + + try { + const userRoleRecords = await db + .select({ + permissions: roles.permissions, + }) + .from(userRoles) + .innerJoin(roles, eq(userRoles.roleId, roles.id)) + .where(eq(userRoles.userId, userId)); + + const allPermissions = new Set(); + for (const record of userRoleRecords) { + try { + const permissions = JSON.parse(record.permissions) as string[]; + for (const perm of permissions) { + allPermissions.add(perm); + } + } catch (parseError) { + databaseLogger.warn("Failed to parse role permissions", { + operation: "get_user_permissions", + userId, + error: parseError, + }); + } + } + + const permissionsArray = Array.from(allPermissions); + + // Cache the result + this.permissionCache.set(userId, { + permissions: permissionsArray, + timestamp: Date.now(), + }); + + return permissionsArray; + } catch (error) { + databaseLogger.error("Failed to get user permissions", error, { + operation: "get_user_permissions", + userId, + }); + return []; + } + } + + /** + * Check if user has a specific permission + * Supports wildcards: "hosts.*", "*" + */ + async hasPermission( + userId: string, + permission: string, + ): Promise { + const userPermissions = await this.getUserPermissions(userId); + + // Check for wildcard "*" (god mode) + if (userPermissions.includes("*")) { + return true; + } + + // Check exact match + if (userPermissions.includes(permission)) { + return true; + } + + // Check wildcard matches + const parts = permission.split("."); + for (let i = parts.length; i > 0; i--) { + const wildcardPermission = parts.slice(0, i).join(".") + ".*"; + if (userPermissions.includes(wildcardPermission)) { + return true; + } + } + + return false; + } + + /** + * Check if user can access a specific host + */ + async canAccessHost( + userId: string, + hostId: number, + action: "read" | "write" | "execute" | "delete" | "share" = "read", + ): Promise { + try { + // Check if user is the owner + const host = await db + .select() + .from(sshData) + .where(and(eq(sshData.id, hostId), eq(sshData.userId, userId))) + .limit(1); + + if (host.length > 0) { + return { + hasAccess: true, + isOwner: true, + isShared: false, + }; + } + + // Check if host is shared with user + const now = new Date().toISOString(); + const sharedAccess = await db + .select() + .from(hostAccess) + .where( + and( + eq(hostAccess.hostId, hostId), + eq(hostAccess.userId, userId), + or( + isNull(hostAccess.expiresAt), + gte(hostAccess.expiresAt, now), + ), + ), + ) + .limit(1); + + if (sharedAccess.length > 0) { + const access = sharedAccess[0]; + + // Check permission level for write/delete actions + if (action === "write" || action === "delete") { + const level = access.permissionLevel; + if (level === "readonly") { + return { + hasAccess: false, + isOwner: false, + isShared: true, + permissionLevel: level, + expiresAt: access.expiresAt, + }; + } + } + + // Update last accessed time + try { + db.update(hostAccess) + .set({ + lastAccessedAt: now, + accessCount: sql`${hostAccess.accessCount} + 1`, + }) + .where(eq(hostAccess.id, access.id)) + .run(); + } catch (error) { + databaseLogger.warn("Failed to update host access stats", { + operation: "update_host_access_stats", + error, + }); + } + + return { + hasAccess: true, + isOwner: false, + isShared: true, + permissionLevel: access.permissionLevel, + expiresAt: access.expiresAt, + }; + } + + return { + hasAccess: false, + isOwner: false, + isShared: false, + }; + } catch (error) { + databaseLogger.error("Failed to check host access", error, { + operation: "can_access_host", + userId, + hostId, + action, + }); + return { + hasAccess: false, + isOwner: false, + isShared: false, + }; + } + } + + /** + * Check if user is admin (backward compatibility) + */ + async isAdmin(userId: string): Promise { + try { + // Check old is_admin field + const user = await db + .select({ isAdmin: users.is_admin }) + .from(users) + .where(eq(users.id, userId)) + .limit(1); + + if (user.length > 0 && user[0].isAdmin) { + return true; + } + + // Check if user has admin or super_admin role + const adminRoles = await db + .select({ roleName: roles.name }) + .from(userRoles) + .innerJoin(roles, eq(userRoles.roleId, roles.id)) + .where( + and( + eq(userRoles.userId, userId), + or(eq(roles.name, "admin"), eq(roles.name, "super_admin")), + ), + ); + + return adminRoles.length > 0; + } catch (error) { + databaseLogger.error("Failed to check admin status", error, { + operation: "is_admin", + userId, + }); + return false; + } + } + + /** + * Middleware: Require specific permission + */ + requirePermission(permission: string) { + return async ( + req: AuthenticatedRequest, + res: Response, + next: NextFunction, + ) => { + const userId = req.userId; + + if (!userId) { + return res.status(401).json({ error: "Not authenticated" }); + } + + const hasPermission = await this.hasPermission(userId, permission); + + if (!hasPermission) { + databaseLogger.warn("Permission denied", { + operation: "permission_check", + userId, + permission, + path: req.path, + }); + + return res.status(403).json({ + error: "Insufficient permissions", + required: permission, + }); + } + + next(); + }; + } + + /** + * Middleware: Require host access + */ + requireHostAccess( + hostIdParam: string = "id", + action: "read" | "write" | "execute" | "delete" | "share" = "read", + ) { + return async ( + req: AuthenticatedRequest, + res: Response, + next: NextFunction, + ) => { + const userId = req.userId; + + if (!userId) { + return res.status(401).json({ error: "Not authenticated" }); + } + + const hostId = parseInt(req.params[hostIdParam], 10); + + if (isNaN(hostId)) { + return res.status(400).json({ error: "Invalid host ID" }); + } + + const accessInfo = await this.canAccessHost(userId, hostId, action); + + if (!accessInfo.hasAccess) { + databaseLogger.warn("Host access denied", { + operation: "host_access_check", + userId, + hostId, + action, + }); + + return res.status(403).json({ + error: "Access denied to host", + hostId, + action, + }); + } + + // Attach access info to request for use in route handlers + (req as any).hostAccessInfo = accessInfo; + + next(); + }; + } + + /** + * Middleware: Require admin role (backward compatible) + */ + requireAdmin() { + return async ( + req: AuthenticatedRequest, + res: Response, + next: NextFunction, + ) => { + const userId = req.userId; + + if (!userId) { + return res.status(401).json({ error: "Not authenticated" }); + } + + const isAdmin = await this.isAdmin(userId); + + if (!isAdmin) { + databaseLogger.warn("Admin access denied", { + operation: "admin_check", + userId, + path: req.path, + }); + + return res.status(403).json({ error: "Admin access required" }); + } + + next(); + }; + } +} + +export { PermissionManager }; +export type { AuthenticatedRequest, HostAccessInfo, PermissionCheckResult }; diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx new file mode 100644 index 00000000..5284517e --- /dev/null +++ b/src/components/ui/alert-dialog.tsx @@ -0,0 +1,155 @@ +import * as React from "react"; +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; + +import { cn } from "@/lib/utils"; +import { buttonVariants } from "@/components/ui/button"; + +function AlertDialog({ + ...props +}: React.ComponentProps) { + return ; +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + ); +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogCancel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +}; diff --git a/src/constants/terminal-themes.ts b/src/constants/terminal-themes.ts index 63f55055..393188f8 100644 --- a/src/constants/terminal-themes.ts +++ b/src/constants/terminal-themes.ts @@ -705,6 +705,7 @@ export const DEFAULT_TERMINAL_CONFIG = { startupSnippetId: null as number | null, autoMosh: false, moshCommand: "mosh-server new -s -l LANG=en_US.UTF-8", + sudoPasswordAutoFill: false, }; export type TerminalConfigType = typeof DEFAULT_TERMINAL_CONFIG; diff --git a/src/hooks/use-confirmation.ts b/src/hooks/use-confirmation.ts index 18037d8f..c8b7fe73 100644 --- a/src/hooks/use-confirmation.ts +++ b/src/hooks/use-confirmation.ts @@ -36,24 +36,50 @@ export function useConfirmation() { }; const confirmWithToast = ( - message: string, - callback: () => void, - variant: "default" | "destructive" = "default", - ) => { - const actionText = variant === "destructive" ? "Delete" : "Confirm"; - const cancelText = "Cancel"; + opts: ConfirmationOptions | string, + callback?: () => void, + variant?: "default" | "destructive", + ): Promise => { + // Legacy signature support + if (typeof opts === "string" && callback) { + const actionText = variant === "destructive" ? "Delete" : "Confirm"; + const cancelText = "Cancel"; - toast(message, { - action: { - label: actionText, - onClick: callback, - }, - cancel: { - label: cancelText, - onClick: () => {}, - }, - duration: 10000, - className: variant === "destructive" ? "border-red-500" : "", + toast(opts, { + action: { + label: actionText, + onClick: callback, + }, + cancel: { + label: cancelText, + onClick: () => {}, + }, + duration: 10000, + className: variant === "destructive" ? "border-red-500" : "", + }); + return Promise.resolve(true); + } + + // New Promise-based signature + return new Promise((resolve) => { + const options = opts as ConfirmationOptions; + const actionText = options.confirmText || "Confirm"; + const cancelText = options.cancelText || "Cancel"; + const variantClass = options.variant === "destructive" ? "border-red-500" : ""; + + toast(options.title, { + description: options.description, + action: { + label: actionText, + onClick: () => resolve(true), + }, + cancel: { + label: cancelText, + onClick: () => resolve(false), + }, + duration: 10000, + className: variantClass, + }); }); }; diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts index 4e440866..0a723003 100644 --- a/src/i18n/i18n.ts +++ b/src/i18n/i18n.ts @@ -8,12 +8,13 @@ import deTranslation from "../locales/de/translation.json"; import ptbrTranslation from "../locales/pt-BR/translation.json"; import ruTranslation from "../locales/ru/translation.json"; import frTranslation from "../locales/fr/translation.json"; +import koTranslation from "../locales/ko/translation.json"; i18n .use(LanguageDetector) .use(initReactI18next) .init({ - supportedLngs: ["en", "zh", "de", "ptbr", "ru", "fr"], + supportedLngs: ["en", "zh", "de", "ptbr", "ru", "fr", "ko"], fallbackLng: "en", debug: false, @@ -44,6 +45,9 @@ i18n fr: { translation: frTranslation, }, + ko: { + translation: koTranslation, + }, }, interpolation: { diff --git a/src/locales/de/translation.json b/src/locales/de/translation.json index c6cdb3fa..6adcebfc 100644 --- a/src/locales/de/translation.json +++ b/src/locales/de/translation.json @@ -6,7 +6,7 @@ "createCredential": "Anmeldeinformationen erstellen", "editCredential": "Anmeldeinformationen bearbeiten", "viewCredential": "Anmeldeinformationen anzeigen", - "duplicateCredential": "Doppelte Anmeldeinformationen", + "duplicateCredential": "Anmeldeinformationen duplizieren", "deleteCredential": "Anmeldeinformationen löschen", "updateCredential": "Anmeldedaten aktualisieren", "credentialName": "Name der Anmeldeinformationen", @@ -28,7 +28,7 @@ "failedToFetchCredentials": "Anmeldeinformationen konnten nicht abgerufen werden", "credentialDeletedSuccessfully": "Anmeldeinformationen erfolgreich gelöscht", "failedToDeleteCredential": "Fehler beim Löschen der Anmeldeinformationen", - "confirmDeleteCredential": "Möchten Sie die Anmeldeinformationen „ {{name}} “ wirklich löschen?", + "confirmDeleteCredential": "Möchten Sie die Anmeldeinformationen „{{name}}“ wirklich löschen?", "credentialCreatedSuccessfully": "Anmeldeinformationen erfolgreich erstellt", "credentialUpdatedSuccessfully": "Anmeldeinformationen erfolgreich aktualisiert", "failedToSaveCredential": "Fehler beim Speichern der Anmeldeinformationen", @@ -43,7 +43,7 @@ "refresh": "Aktualisieren", "passwordRequired": "Passwort erforderlich", "sshKeyRequired": "SSH-Schlüssel ist erforderlich", - "credentialAddedSuccessfully": "Anmeldeinformationen „ {{name}} “ erfolgreich hinzugefügt", + "credentialAddedSuccessfully": "Anmeldeinformationen „{{name}}“ erfolgreich hinzugefügt", "general": "Allgemein", "description": "Beschreibung", "folder": "Ordner", @@ -91,11 +91,11 @@ "generateKeyPairDescription": "Generieren Sie ein neues SSH-Schlüsselpaar. Wenn Sie den Schlüssel mit einer Passphrase schützen möchten, geben Sie diese zunächst in das Feld Schlüsselkennwort unten ein.", "deploySSHKey": "SSH-Schlüssel bereitstellen", "deploySSHKeyDescription": "Bereitstellen des öffentlichen Schlüssels auf dem Zielserver", - "sourceCredential": "Quellanmeldeinformationen", + "sourceCredential": "Quell-Anmeldeinformationen", "targetHost": "Ziel-Host", "deploymentProcess": "Bereitstellungsprozess", - "deploymentProcessDescription": "Dadurch wird der öffentliche Schlüssel sicher zur Datei ~\/.ssh\/authorized_keys des Zielhosts hinzugefügt, ohne vorhandene Schlüssel zu überschreiben. Der Vorgang ist umkehrbar.", - "chooseHostToDeploy": "Wählen Sie einen Host für die Bereitstellung aus ...", + "deploymentProcessDescription": "Dadurch wird der öffentliche Schlüssel sicher zur Datei ~/.ssh/authorized_keys des Zielhosts hinzugefügt, ohne vorhandene Schlüssel zu überschreiben. Der Vorgang ist umkehrbar.", + "chooseHostToDeploy": "Wählen Sie einen Host für die Bereitstellung aus...", "deploying": "Bereitstellen...", "name": "Name", "noHostsAvailable": "Keine Hosts verfügbar", @@ -106,13 +106,13 @@ "selectOrCreateFolder": "Ordner auswählen oder erstellen", "noFolder": "Kein Ordner", "orCreateNewFolder": "Oder erstellen Sie einen neuen Ordner", - "addTag": "Schlüsselwort hinzufügen", + "addTag": "Schlagwort hinzufügen", "saving": "Speichern...", "overview": "Überblick", "security": "Sicherheit", "usage": "Verwendung", "securityDetails": "Sicherheitsdetails", - "securityDetailsDescription": "Anzeigen verschlüsselter Anmeldeinformationen", + "securityDetailsDescription": "Verschlüsselte Anmeldeinformationen anzeigen", "credentialSecured": "Anmeldeinformationen gesichert", "credentialSecuredDescription": "Alle sensiblen Daten werden mit AES-256 verschlüsselt", "passwordAuthentication": "Kennwortauthentifizierung", @@ -135,12 +135,12 @@ "listView": "Liste", "folderView": "Ordner", "unknownCredential": "Unbekannt", - "confirmRemoveFromFolder": "Sind Sie sicher, dass Sie \"{{name}}\" aus Ordner \"{{folder}}\" entfernen möchten? Die Zugangsdaten werden in den Bereich \"Nicht kategorisiert\" verschoben.", - "removedFromFolder": "Anmeldeinformationen „ {{name}} “ erfolgreich aus dem Ordner entfernt", + "confirmRemoveFromFolder": "Sind Sie sicher, dass Sie \"{{name}}\" aus Ordner \"{{folder}}\" entfernen möchten? Die Zugangsdaten werden in den Bereich \"Unkategorisiert\" verschoben.", + "removedFromFolder": "Anmeldeinformationen „{{name}}“ erfolgreich aus dem Ordner entfernt", "failedToRemoveFromFolder": "Anmeldeinformationen konnten nicht aus dem Ordner entfernt werden", - "folderRenamed": "Ordner „ {{oldName}} “ erfolgreich in „ {{newName}} “ umbenannt", + "folderRenamed": "Ordner „{{oldName}}“ erfolgreich in „{{newName}}“ umbenannt", "failedToRenameFolder": "Ordner konnte nicht umbenannt werden", - "movedToFolder": "Anmeldeinformationen „ {{name}} “ wurden erfolgreich nach „ {{folder}} “ verschoben.", + "movedToFolder": "Anmeldeinformationen „{{name}}“ wurden erfolgreich nach „{{folder}}“ verschoben.", "failedToMoveToFolder": "Anmeldeinformationen konnten nicht in den Ordner verschoben werden", "sshPublicKey": "Öffentlicher SSH-Schlüssel", "publicKeyNote": "Der öffentliche Schlüssel ist optional, wird jedoch zur Schlüsselvalidierung empfohlen", @@ -153,7 +153,7 @@ "generatePublicKey": "Aus privatem Schlüssel generieren", "publicKeyGeneratedSuccessfully": "Öffentlicher Schlüssel erfolgreich generiert", "detectedKeyType": "Erkannter Schlüsseltyp", - "detectingKeyType": "erkennen...", + "detectingKeyType": "Erkennen...", "optional": "Optional", "generateKeyPairNew": "Neues Schlüsselpaar generieren", "generateEd25519": "Ed25519 generieren", @@ -164,13 +164,14 @@ "generateKeyPairNote": "Generieren Sie direkt ein neues SSH-Schlüsselpaar. Dadurch werden alle vorhandenen Schlüssel im Formular ersetzt.", "invalidKey": "Ungültiger Schlüssel", "detectionError": "Erkennungsfehler", - "unknown": "Unbekannt" + "unknown": "Unbekannt", + "credentialId": "Anmeldedaten ID" }, "dragIndicator": { "error": "Fehler: {{error}}", "dragging": "Ziehen von {{fileName}}", "preparing": "{{fileName}} wird vorbereitet", - "readySingle": "Bereit zum Download {{fileName}}", + "readySingle": "Bereit zum Herunterladen {{fileName}}", "readyMultiple": "Bereit zum Herunterladen von {{count}} Dateien", "batchDrag": "Ziehen Sie {{count}} Dateien auf den Desktop", "dragToDesktop": "Auf den Desktop ziehen", @@ -179,14 +180,14 @@ "sshTools": { "title": "SSH-Tools", "closeTools": "SSH-Tools schließen", - "keyRecording": "Tastenaufzeichnung", - "startKeyRecording": "Tastenaufzeichnung starten", - "stopKeyRecording": "Tastenerfassung stoppen", + "keyRecording": "Tastatureingabe auf allen Terminals", + "startKeyRecording": "Übertragung starten", + "stopKeyRecording": "Übertragung stoppen", "selectTerminals": "Terminals auswählen:", "typeCommands": "Geben Sie Befehle ein (alle Tasten werden unterstützt):", "commandsWillBeSent": "Befehle werden an {{count}} ausgewählte Terminals gesendet.", "settings": "Einstellungen", - "enableRightClickCopyPaste": "Rechtsklick-Kopieren\/Einfügen aktivieren", + "enableRightClickCopyPaste": "Rechtsklick-Kopieren/Einfügen aktivieren", "shareIdeas": "Haben Sie Vorschläge welche weiteren SSH-Tools ergänzt werden sollen? Dann teilen Sie diese gerne mit uns auf" }, "commandHistory": { @@ -225,25 +226,29 @@ "saveError": "Fehler beim Speichern der Konfiguration", "saving": "Speichern...", "saveConfig": "Konfiguration speichern", - "helpText": "Geben Sie die URL ein, unter der Ihr Termix-Server ausgeführt wird (z. B. http:\/\/localhost:30001 oder https:\/\/your-server.com)." + "helpText": "Geben Sie die URL ein, unter der Ihr Termix-Server ausgeführt wird (z. B. http://localhost:30001 oder https://ihr-server.com).", + "changeServer": "Server ändern", + "mustIncludeProtocol": "Server-URL muss mit http:// oder https:// beginnen", + "notValidatedWarning": "URL nicht validiert - stellen Sie sicher, dass sie korrekt ist", + "warning": "Warnung" }, "versionCheck": { "error": "Fehler bei der Versionsprüfung", - "checkFailed": "Suche nach Updates fehlgeschlagen", + "checkFailed": "Suche nach Aktualisierungen fehlgeschlagen", "upToDate": "App ist auf dem neuesten Stand", "currentVersion": "Sie verwenden Version {{version}}", - "updateAvailable": "Update verfügbar", + "updateAvailable": "Aktualisierung verfügbar", "newVersionAvailable": "Eine neue Version ist verfügbar! Sie verwenden {{current}}, aber {{latest}} ist verfügbar.", "releasedOn": "Veröffentlicht am {{date}}", - "downloadUpdate": "Update herunterladen", + "downloadUpdate": "Aktualisierung herunterladen", "dismiss": "Schließen", - "checking": "Suche nach Updates...", - "checkUpdates": "Nach Updates suchen", - "checkingUpdates": "Suche nach Updates...", + "checking": "Suche nach Aktualisierungen...", + "checkUpdates": "Nach Aktualisierungen suchen", + "checkingUpdates": "Suche nach Aktualisierungen...", "refresh": "Aktualisieren", "updateRequired": "Aktualisierung erforderlich", - "updateDismissed": "Update-Benachrichtigung abgelehnt", - "noUpdatesFound": "Keine Updates gefunden" + "updateDismissed": "Aktualisierungs-Benachrichtigung ausgeblendet", + "noUpdatesFound": "Keine Aktualisierungen gefunden" }, "common": { "close": "Schließen", @@ -252,7 +257,7 @@ "offline": "Offline", "continue": "Fortsetzen", "maintenance": "Wartung", - "degraded": "Herabgestuft", + "degraded": "Eingeschränkt", "discord": "Discord", "error": "Fehler", "warning": "Warnung", @@ -262,19 +267,19 @@ "required": "Erforderlich", "optional": "Optional", "clear": "Löschen", - "toggleSidebar": "Seitenleiste ein-\/ausblenden", + "toggleSidebar": "Seitenleiste ein-/ausblenden", "sidebar": "Seitenleiste", "home": "Startseite", "expired": "Abgelaufen", "expiresToday": "Läuft heute ab", "expiresTomorrow": "Läuft morgen ab", "expiresInDays": "Läuft in {{days}} Tagen ab", - "updateAvailable": "Update verfügbar", + "updateAvailable": "Aktualisierung verfügbar", "sshPath": "SSH-Pfad", "localPath": "Lokaler Pfad", "noAuthCredentials": "Für diesen SSH-Host sind keine Anmeldeinformationen verfügbar", "noReleases": "Keine Releases", - "updatesAndReleases": "Updates & Veröffentlichungen", + "updatesAndReleases": "Aktualisierungen & Veröffentlichungen", "newVersionAvailable": "Eine neue Version ({{version}}) ist verfügbar.", "failedToFetchUpdateInfo": "Abrufen der Aktualisierungsinformationen fehlgeschlagen", "preRelease": "Vorabversion", @@ -299,14 +304,14 @@ "username": "Benutzername", "name": "Name", "login": "Anmelden", - "logout": "Ausloggen", + "logout": "Abmelden", "register": "Registrieren", "password": "Passwort", "version": "Version", "confirmPassword": "Passwort bestätigen", "back": "Zurück", "email": "E-Mail", - "submit": "Senden", + "submit": "Absenden", "change": "Ändern", "save": "Speichern", "saving": "Speichern...", @@ -337,7 +342,7 @@ "changeAccountPassword": "Passwort für Ihr Konto ändern", "passwordResetTitle": "Passwort zurücksetzen", "passwordResetDescription": "Sie sind dabei, Ihr Passwort zurückzusetzen. Dadurch werden Sie von allen aktiven Sitzungen abgemeldet.", - "enterSixDigitCode": "Geben Sie den 6-stelligen Code aus den Docker-Container-Protokollen \/ logs für den Benutzer ein:", + "enterSixDigitCode": "Geben Sie den 6-stelligen Code aus den Docker-Container-Protokollen / logs für den Benutzer ein:", "enterNewPassword": "Geben Sie Ihr neues Passwort für den Benutzer ein:", "passwordsDoNotMatch": "Passwörter stimmen nicht überein", "passwordMinLength": "Das Passwort muss mindestens 6 Zeichen lang sein", @@ -358,7 +363,7 @@ "tunnels": "Tunnel", "fileManager": "Dateimanager", "serverStats": "Serverstatus", - "admin": "Administrator", + "admin": "Administration", "userProfile": "Benutzerprofil", "tools": "Werkzeuge", "newTab": "Neuer Tab", @@ -366,11 +371,12 @@ "closeTab": "Tab schließen", "sshManager": "SSH-Manager", "hostManager": "Host-Manager", - "cannotSplitTab": "Diese Registerkarte kann nicht geteilt werden", - "tabNavigation": "Registerkarte Navigation" + "cannotSplitTab": "Dieser Tab kann nicht geteilt werden", + "tabNavigation": "Tab-Navigation", + "snippets": "Snippets" }, "admin": { - "title": "Admin-Einstellungen", + "title": "Administration", "oidc": "OIDC", "users": "Benutzer", "userManagement": "Benutzerverwaltung", @@ -384,14 +390,14 @@ "issuerUrl": "Aussteller-URL", "authorizationUrl": "Autorisierungs-URL", "tokenUrl": "Token-URL", - "updateSettings": "Update-Einstellungen", + "updateSettings": "Einstellungen aktualisieren", "confirmDelete": "Möchten Sie diesen Benutzer wirklich löschen?", - "confirmMakeAdmin": "Sind Sie sicher, dass Sie diesen Benutzer zum Admin machen möchten?", + "confirmMakeAdmin": "Sind Sie sicher, dass Sie diesen Benutzer zum Administrator machen möchten?", "confirmRemoveAdmin": "Möchten Sie die Administratorrechte für diesen Benutzer wirklich entfernen?", "externalAuthentication": "Externe Authentifizierung (OIDC)", - "configureExternalProvider": "Externen Identitätsanbieter für OIDC\/OAuth2-Authentifizierung konfigurieren.", + "configureExternalProvider": "Externen Identitätsanbieter für OIDC/OAuth2-Authentifizierung konfigurieren.", "userIdentifierPath": "Pfad für Benutzerkennung", - "displayNamePath": "Anzeigenamenpfad", + "displayNamePath": "Pfad für Anzeigename", "scopes": "Scopes", "saving": "Speichern...", "saveConfiguration": "Konfiguration speichern", @@ -399,45 +405,65 @@ "success": "Erfolgreich", "loading": "Laden...", "refresh": "Aktualisieren", - "loadingUsers": "Benutzer werden geladen …", + "loadingUsers": "Benutzer werden geladen...", "username": "Benutzername", "type": "Typ", "actions": "Aktionen", "external": "Extern", "local": "Lokal", - "adminManagement": "Admin Verwaltung", + "adminManagement": "Administrator-Verwaltung", "makeUserAdmin": "Benutzer zum Administrator machen", "adding": "Hinzufügen...", "currentAdmins": "Aktuelle Administratoren", - "adminBadge": "Admin", + "adminBadge": "Administrator", "removeAdminButton": "Administrator entfernen", "general": "Allgemein", "userRegistration": "Benutzerregistrierung", "allowNewAccountRegistration": "Registrierung neuer Konten zulassen", + "allowPasswordLogin": "Benutzername/Passwort-Anmeldung zulassen", "missingRequiredFields": "Fehlende Pflichtfelder: {{fields}}", "oidcConfigurationUpdated": "OIDC-Konfiguration erfolgreich aktualisiert!", "failedToFetchOidcConfig": "OIDC-Konfiguration konnte nicht abgerufen werden", "failedToFetchRegistrationStatus": "Abrufen des Registrierungsstatus fehlgeschlagen", + "failedToFetchPasswordLoginStatus": "Abrufen des Passwort-Login-Status fehlgeschlagen", "failedToFetchUsers": "Benutzer konnten nicht abgerufen werden", "oidcConfigurationDisabled": "OIDC-Konfiguration erfolgreich deaktiviert!", "failedToUpdateOidcConfig": "Aktualisierung der OIDC-Konfiguration fehlgeschlagen", "failedToDisableOidcConfig": "OIDC-Konfiguration konnte nicht deaktiviert werden", - "enterUsernameToMakeAdmin": "Geben Sie den Benutzernamen ein, um zum Administrator zu werden", + "enterUsernameToMakeAdmin": "Geben Sie den Benutzernamen ein, um Administrator zu werden", "userIsNowAdmin": "Der Benutzer {{username}} ist jetzt ein Administrator", "failedToMakeUserAdmin": "Fehler beim Festlegen des Benutzers als Administrator", - "removeAdminStatus": "Admin-Status von {{username}} entfernen?", - "adminStatusRemoved": "Admin-Status von {{username}} entfernt", - "failedToRemoveAdminStatus": "Admin-Status konnte nicht entfernt werden", + "removeAdminStatus": "Administrator-Status von {{username}} entfernen?", + "adminStatusRemoved": "Administrator-Status von {{username}} entfernt", + "failedToRemoveAdminStatus": "Administrator-Status konnte nicht entfernt werden", "userDeletedSuccessfully": "Benutzer {{username}} wurde erfolgreich gelöscht", "failedToDeleteUser": "Benutzer konnte nicht gelöscht werden", - "overrideUserInfoUrl": "URL für Benutzerinformationen überschreiben (nicht erforderlich)", + "overrideUserInfoUrl": "URL für Benutzerinformationen überschreiben (optional)", "failedToFetchSessions": "Fehler beim Abrufen der Sitzungen", "sessionRevokedSuccessfully": "Sitzung erfolgreich widerrufen", "failedToRevokeSession": "Sitzung konnte nicht widerrufen werden", - "confirmRevokeSession": "Möchten Sie diese Sitzung wirklich beenden?", - "confirmRevokeAllSessions": "Möchten Sie wirklich alle Sitzungen dieses Benutzers beenden?", + "confirmRevokeSession": "Möchten Sie diese Sitzung wirklich widerrufen?", + "confirmRevokeAllSessions": "Möchten Sie wirklich alle Sitzungen dieses Benutzers widerrufen?", "failedToRevokeSessions": "Sitzungen konnten nicht widerrufen werden", - "sessionsRevokedSuccessfully": "Sitzungen erfolgreich beendet", + "sessionsRevokedSuccessfully": "Sitzungen erfolgreich widerrufen", + "linkToPasswordAccount": "Mit Passwort-Konto verknüpfen", + "linkOIDCDialogTitle": "OIDC-Konto mit Passwort-Konto verknüpfen", + "linkOIDCDialogDescription": "Verknüpfen Sie {{username}} (OIDC-Benutzer) mit einem bestehenden Passwort-Konto. Dies aktiviert die duale Authentifizierung für das Passwort-Konto.", + "linkOIDCWarningTitle": "Warnung: OIDC-Benutzerdaten werden gelöscht", + "linkOIDCActionDeleteUser": "Löschen Sie das OIDC-Benutzerkonto und alle seine Daten", + "linkOIDCActionAddCapability": "Fügen Sie dem Ziel-Passwort-Konto die OIDC-Anmeldefunktion hinzu", + "linkOIDCActionDualAuth": "Erlauben Sie dem Passwort-Konto, sich sowohl mit Passwort als auch mit OIDC anzumelden", + "linkTargetUsernameLabel": "Benutzername des Ziel-Passwort-Kontos", + "linkTargetUsernamePlaceholder": "Geben Sie den Benutzernamen des Passwort-Kontos ein", + "linkAccountsButton": "Konten verknüpfen", + "linkingAccounts": "Verknüpfen...", + "accountsLinkedSuccessfully": "Der OIDC-Benutzer {{oidcUsername}} wurde mit {{targetUsername}} verknüpft", + "failedToLinkAccounts": "Konten konnten nicht verknüpft werden", + "linkTargetUsernameRequired": "Ziel-Benutzername ist erforderlich", + "unlinkOIDCTitle": "OIDC-Authentifizierung trennen", + "unlinkOIDCDescription": "OIDC-Authentifizierung von {{username}} entfernen? Der Benutzer kann sich danach nur noch mit Benutzername/Passwort anmelden.", + "unlinkOIDCSuccess": "OIDC von {{username}} getrennt", + "failedToUnlinkOIDC": "OIDC konnte nicht getrennt werden", "databaseSecurity": "Datenbanksicherheit", "encryptionStatus": "Verschlüsselungsstatus", "encryptionEnabled": "Verschlüsselung aktiviert", @@ -457,7 +483,7 @@ "enableAes256EncryptionWithDeviceBinding": "Aktivieren Sie AES-256-Verschlüsselung mit umgebungsgebundener Master-Schlüssel-Sicherung. Dadurch entsteht Sicherheitsniveau in Unternehmensqualität für SSH-Schlüssel, Passwörter und Authentifizierungs-Token.", "featuresEnabled": "Aktivierte Funktionen:", "aes256GcmAuthenticatedEncryption": "Authentifizierte Verschlüsselung mit AES-256-GCM", - "deviceFingerprintMasterKeyProtection": "Schutz des Master-Schlüssels der Umgebungs-Fingerabdruckkennung (KEK)", + "deviceFingerprintMasterKeyProtection": "Schutz des Master-Schlüssels durch Umgebungs-Fingerabdruck (KEK)", "pbkdf2KeyDerivation": "PBKDF2-Schlüsselableitung mit 100.000 Iterationen", "automaticKeyManagement": "Automatische Schlüsselverwaltung und -rotation", "initializing": "Initialisierung läuft...", @@ -481,8 +507,8 @@ "loadingEncryptionStatus": "Verschlüsselungsstatus wird geladen...", "testMigrationDescription": "Überprüfen, dass vorhandene Daten sicher in ein verschlüsseltes Format migriert werden können, ohne tatsächlich irgendwelche Daten zu ändern", "serverMigrationGuide": "Leitfaden zur Servermigration", - "migrationInstructions": "So migrieren Sie verschlüsselte Daten auf einen neuen Server: 1) Datenbankdateien sichern, 2) Umgebungsvariable DB_ENCRYPTION_KEY=\"your-key\" auf dem neuen Server setzen, 3) Datenbankdateien wiederherstellen", - "environmentProtection": "Umweltschutz", + "migrationInstructions": "So migrieren Sie verschlüsselte Daten auf einen neuen Server: 1) Datenbankdateien sichern, 2) Umgebungsvariable DB_ENCRYPTION_KEY=\"Ihre-schlüssel\" auf dem neuen Server setzen, 3) Datenbankdateien wiederherstellen", + "environmentProtection": "Umgebungsschutz", "environmentProtectionDesc": "Schützt Verschlüsselungsschlüssel basierend auf Serverumgebungsinformationen (Hostname, Pfade usw.), migrierbar über Umgebungsvariablen", "verificationCompleted": "Kompatibilitätsprüfung abgeschlossen – keine Daten wurden geändert", "verificationInProgress": "Überprüfung abgeschlossen", @@ -501,7 +527,7 @@ "stableMacAddressFiltering": "Stabiles MAC-Adressfiltering", "databaseFileEncryption": "Datenbankdatei-Verschlüsselung", "dualLayerProtection": "Dualer Schutz mit zwei Ebenen aktiv", - "bothFieldAndFileEncryptionActive": "Sowohl die Feld- als auch die Datei­ebene sind jetzt verschlüsselt – für maximale Sicherheit", + "bothFieldAndFileEncryptionActive": "Sowohl die Feld- als auch die Dateiebene sind jetzt verschlüsselt – für maximale Sicherheit", "fieldLevelAes256Encryption": "Feldbasierte AES-256-Verschlüsselung für sensible Daten", "fileLevelDatabaseEncryption": "Dateiebene-Datenbankverschlüsselung mit Hardwarebindung", "hardwareBoundFileKeys": "Hardwaregebundene Dateiverschlüsselungsschlüssel", @@ -512,7 +538,7 @@ "encryptedBackupCreatedSuccessfully": "Verschlüsselte Sicherung erfolgreich erstellt", "backupCreationFailed": "Erstellung des Backups fehlgeschlagen", "databaseMigration": "Datenbankmigration", - "exportForMigration": "Export für Migration", + "exportForMigration": "Exportieren für Migration", "exportDatabaseForHardwareMigration": "Datenbank als SQLite-Datei mit entschlüsselten Daten für die Migration auf neue Hardware exportieren", "exportDatabase": "SQLite-Datenbank exportieren", "exporting": "Wird exportiert...", @@ -520,7 +546,7 @@ "exportContainsDecryptedData": "SQLite-Export enthält entschlüsselte Daten – sicher aufbewahren!", "databaseExportedSuccessfully": "SQLite-Datenbank erfolgreich exportiert", "databaseExportFailed": "Export der SQLite-Datenbank fehlgeschlagen", - "importFromMigration": "Import aus Migration", + "importFromMigration": "Importieren aus Migration", "importDatabaseFromAnotherSystem": "SQLite-Datenbank von einem anderen System oder einer anderen Hardware importieren", "importDatabase": "SQLite-Datenbank importieren", "importing": "Importieren...", @@ -529,7 +555,7 @@ "pleaseSelectImportFile": "Bitte wählen Sie eine SQLite-Importdatei aus", "databaseImportedSuccessfully": "SQLite-Datenbank erfolgreich importiert", "databaseImportFailed": "Import der SQLite-Datenbank fehlgeschlagen", - "manageEncryptionAndBackups": "Verschlüsselungsschlüssel, Databasesicherheit und Sicherungsabläufe verwalten", + "manageEncryptionAndBackups": "Verschlüsselungsschlüssel, Datenbanksicherheit und Sicherungsabläufe verwalten", "activeSecurityFeatures": "Derzeit aktive Sicherheitsmaßnahmen und Schutzvorkehrungen", "deviceBindingTechnology": "Fortschrittliche, hardwarebasierte Technologie zum Schutz von Schlüsseln", "backupAndRecovery": "Optionen für die sichere Erstellung von Backups und die Wiederherstellung der Datenbank", @@ -549,11 +575,11 @@ "migrate": "Migrieren", "backup": "Backup", "createBackup": "Backup erstellen", - "exportImport": "Export\/Import", - "export": "Export", - "import": "Import", + "exportImport": "Exportieren/Importieren", + "export": "Exportieren", + "import": "Importieren", "passwordRequired": "Passwort erforderlich", - "confirmExport": "Export bestätigen", + "confirmExport": "Exportieren bestätigen", "exportDescription": "SSH-Hosts und Anmeldedaten als SQLite-Datei exportieren", "importDescription": "SQLite-Datei mit inkrementellem Zusammenführen importieren (überspringt Duplikate)", "criticalWarning": "Kritische Warnung", @@ -565,8 +591,6 @@ "passwordLoginDisabledWarning": "Passwort-Login ist deaktiviert. Stellen Sie sicher, dass OIDC ordnungsgemäß konfiguriert ist, sonst können Sie sich nicht bei Termix anmelden.", "oidcRequiredWarning": "KRITISCH: Passwort-Login ist deaktiviert. Wenn Sie OIDC zurücksetzen oder falsch konfigurieren, verlieren Sie den gesamten Zugriff auf Termix und Ihre Instanz wird unbrauchbar. Fahren Sie nur fort, wenn Sie absolut sicher sind.", "confirmDisableOIDCWarning": "WARNUNG: Sie sind dabei, OIDC zu deaktivieren, während auch die Passwort-Anmeldung deaktiviert ist. Dies macht Ihre Termix-Instanz unbrauchbar und Sie verlieren den gesamten Zugriff. Sind Sie absolut sicher, dass Sie fortfahren möchten?", - "allowPasswordLogin": "Benutzername/Passwort-Anmeldung zulassen", - "failedToFetchPasswordLoginStatus": "Abrufen des Passwort-Login-Status fehlgeschlagen", "failedToUpdatePasswordLoginStatus": "Aktualisierung des Passwort-Login-Status fehlgeschlagen" }, "hosts": { @@ -586,7 +610,7 @@ "downloadSample": "Beispiel herunterladen", "formatGuide": "Formatleitfaden", "exportCredentialWarning": "Warnung: Der Host \"{{name}}\" verwendet eine Anmeldeauthentifizierung. Die exportierte Datei enthält keine Anmeldedaten und muss nach dem Import manuell neu konfiguriert werden. Möchten Sie fortfahren?", - "exportSensitiveDataWarning": "Warnung: Der Host \"{{name}}\" enthält vertrauliche Authentifizierungsdaten (Passwort\/SSH-Schlüssel). Die exportierte Datei wird diese Daten im Klartext enthalten. Bitte bewahren Sie die Datei sicher auf und löschen Sie sie nach der Verwendung. Möchten Sie fortfahren?", + "exportSensitiveDataWarning": "Warnung: Der Host \"{{name}}\" enthält vertrauliche Authentifizierungsdaten (Passwort/SSH-Schlüssel). Die exportierte Datei wird diese Daten im Klartext enthalten. Bitte bewahren Sie die Datei sicher auf und löschen Sie sie nach der Verwendung. Möchten Sie fortfahren?", "uncategorized": "Unkategorisiert", "confirmDelete": "Möchten Sie \"{{name}}\" wirklich löschen?", "failedToDeleteHost": "Host konnte nicht gelöscht werden", @@ -605,26 +629,26 @@ "name": "Name", "username": "Benutzername", "folder": "Ordner", - "tags": "Schlagwörter", - "pin": "PIN", - "passwordRequired": "Bei Verwendung der Kennwortauthentifizierung ist ein Kennwort erforderlich", - "sshKeyRequired": "Bei Verwendung der Schlüsselauthentifizierung ist ein privater SSH-Schlüssel erforderlich", - "keyTypeRequired": "Bei Verwendung der Schlüsselauthentifizierung ist der Schlüsseltyp erforderlich", + "tags": "Tags", + "pin": "Anheften", + "passwordRequired": "Bei Verwendung der Passwort-Authentifizierung ist ein Passwort erforderlich", + "sshKeyRequired": "Bei Verwendung der Schlüssel-Authentifizierung ist ein privater SSH-Schlüssel erforderlich", + "keyTypeRequired": "Bei Verwendung der Schlüssel-Authentifizierung ist der Schlüsseltyp erforderlich", "mustSelectValidSshConfig": "Sie müssen eine gültige SSH-Konfiguration aus der Liste auswählen", "addHost": "Host hinzufügen", "editHost": "Host bearbeiten", "cloneHost": "Host klonen", "updateHost": "Host aktualisieren", - "hostUpdatedSuccessfully": "Host „{{name}}“ wurde erfolgreich aktualisiert!", - "hostAddedSuccessfully": "Host „{{name}}“ wurde erfolgreich hinzugefügt!", - "hostDeletedSuccessfully": "Host „{{name}}“ wurde erfolgreich gelöscht!", + "hostUpdatedSuccessfully": "Host \"{{name}}\" wurde erfolgreich aktualisiert!", + "hostAddedSuccessfully": "Host \"{{name}}\" wurde erfolgreich hinzugefügt!", + "hostDeletedSuccessfully": "Host \"{{name}}\" wurde erfolgreich gelöscht!", "failedToSaveHost": "Host konnte nicht gespeichert werden. Bitte versuchen Sie es erneut.", "enableTerminal": "Terminal aktivieren", - "enableTerminalDesc": "Host-Sichtbarkeit im Terminal-Tab aktivieren\/deaktivieren", + "enableTerminalDesc": "Host-Sichtbarkeit im Terminal-Tab aktivieren/deaktivieren", "enableTunnel": "Tunnel aktivieren", - "enableTunnelDesc": "Sichtbarkeit des Hosts im Tab „Tunnel“ aktivieren\/deaktivieren", + "enableTunnelDesc": "Sichtbarkeit des Hosts im Tab \"Tunnel\" aktivieren/deaktivieren", "enableFileManager": "Dateimanager aktivieren", - "enableFileManagerDesc": "Sichtbarkeit des Hosts im Reiter „Dateimanager“ aktivieren\/deaktivieren", + "enableFileManagerDesc": "Sichtbarkeit des Hosts im Reiter \"Dateimanager\" aktivieren/deaktivieren", "defaultPath": "Standard-Pfad", "defaultPathDesc": "Standardverzeichnis beim Öffnen des Dateimanagers für diesen Host", "tunnelConnections": "Tunnel-Verbindungen", @@ -634,7 +658,7 @@ "sourcePortDesc": " (Quelle bezieht sich auf die aktuellen Verbindungsdetails im Reiter Allgemein)", "endpointPort": "Endpunkt-Port", "endpointSshConfig": "SSH-Konfiguration für Endpunkte", - "tunnelForwardDescription": "Dieser Tunnel leitet den Datenverkehr vom Port {{sourcePort}} auf der Quellmaschine (aktuelle Verbindungsdetails auf der Registerkarte „Allgemein“) an den Port {{endpointPort}} auf der Endpunktmaschine weiter.", + "tunnelForwardDescription": "Dieser Tunnel leitet den Datenverkehr vom Port {{sourcePort}} auf der Quellmaschine (aktuelle Verbindungsdetails auf der Registerkarte \"Allgemein\") an den Port {{endpointPort}} auf der Endpunktmaschine weiter.", "maxRetries": "Max. Wiederholungsversuche", "maxRetriesDescription": "Maximale Anzahl der Wiederholungsversuche für die Tunnelverbindung.", "retryInterval": "Wiederholungsintervall (Sekunden)", @@ -643,11 +667,11 @@ "autoStartDesc": "Diesen Tunnel beim Start des Containers automatisch starten", "addConnection": "Tunnelverbindung hinzufügen", "sshpassRequired": "sshpass erforderlich für die Passwort-Authentifizierung", - "sshpassRequiredDesc": "Für die Passwortauthentifizierung in Tunneln muss sshpass auf dem System installiert sein.", + "sshpassRequiredDesc": "Für die Passwort-Authentifizierung in Tunneln muss sshpass auf dem System installiert sein.", "otherInstallMethods": "Andere Installationsmethoden:", - "debianUbuntuEquivalent": "(Debian\/Ubuntu) oder das entsprechende Pendant für Ihr Betriebssystem.", + "debianUbuntuEquivalent": "(Debian/Ubuntu) oder das entsprechende Pendant für Ihr Betriebssystem.", "or": "oder", - "centosRhelFedora": "CentOS\/RHEL\/Fedora", + "centosRhelFedora": "CentOS/RHEL/Fedora", "macos": "macOS", "windows": "Windows", "sshServerConfigRequired": "SSH-Serverkonfiguration erforderlich", @@ -655,7 +679,7 @@ "gatewayPortsYes": "Remote-Ports an alle Schnittstellen binden", "allowTcpForwardingYes": "Portweiterleitung aktivieren", "permitRootLoginYes": "bei Verwendung des Root-Benutzers für das Tunneling", - "editSshConfig": "Bearbeiten Sie \/etc\/ssh\/sshd_config und starten Sie SSH neu: sudo systemctl restart sshd", + "editSshConfig": "Bearbeiten Sie /etc/ssh/sshd_config und starten Sie SSH neu: sudo systemctl restart sshd", "upload": "Hochladen", "authentication": "Authentifizierung", "password": "Passwort", @@ -664,10 +688,10 @@ "none": "Keine", "selectCredential": "Anmeldeinformationen auswählen", "selectCredentialPlaceholder": "Wähle eine Anmeldedaten aus...", - "credentialRequired": "Für die Anmeldeauthentifizierung ist eine Anmeldeinformation erforderlich", + "credentialRequired": "Für die Anmeldeauthentifizierung sind Anmeldeinformationen erforderlich", "credentialDescription": "Durch die Auswahl einer Anmeldeinformation wird der aktuelle Benutzername überschrieben und die Authentifizierungsdetails der Anmeldeinformation verwendet.", "sshPrivateKey": "Privater SSH-Schlüssel", - "keyPassword": "Schlüsselkennwort", + "keyPassword": "Schlüsselpasswort", "keyType": "Schlüsseltyp", "autoDetect": "Automatische Erkennung", "rsa": "RSA", @@ -682,7 +706,7 @@ "pasteKey": "Schlüssel einfügen", "updateKey": "Schlüssel aktualisieren", "existingKey": "Vorhandener Schlüssel (zum Ändern klicken)", - "existingCredential": "Vorhandenes Anmeldedatum (zum Ändern klicken)", + "existingCredential": "Vorhandene Anmeldedaten (zum Ändern klicken)", "addTagsSpaceToAdd": "Tags hinzufügen (Leertaste zum Hinzufügen)", "terminalBadge": "Terminal", "tunnelBadge": "Tunnel", @@ -695,7 +719,7 @@ "confirmRemoveFromFolder": "Möchten Sie \"{{name}}\" wirklich aus dem Ordner \"{{folder}}\" entfernen? Der Host wird in \"Kein Ordner\" verschoben.", "removedFromFolder": "Host \"{{name}}\" erfolgreich aus dem Ordner entfernt", "failedToRemoveFromFolder": "Host konnte nicht aus dem Ordner entfernt werden", - "folderRenamed": "Ordner „ {{oldName}} “ erfolgreich in „ {{newName}} “ umbenannt", + "folderRenamed": "Ordner \"{{oldName}}\" erfolgreich in \"{{newName}}\" umbenannt", "failedToRenameFolder": "Ordner konnte nicht umbenannt werden", "movedToFolder": "Host \"{{name}}\" wurde erfolgreich nach \"{{folder}}\" verschoben", "failedToMoveToFolder": "Host konnte nicht in den Ordner verschoben werden", @@ -720,7 +744,7 @@ "monitoringDisabledBadge": "Überwachung Aus", "statusMonitoring": "Status", "metricsMonitoring": "Metriken", - "terminalCustomizationNotice": "Hinweis: Terminal-Anpassungen funktionieren nur in der Desktop-Website-Version. Mobile und Electron-Apps verwenden die Standard-Terminaleinstellungen des Systems.", + "terminalCustomizationNotice": "Hinweis: Terminal-Anpassungen funktionieren nur in der Desktop-Webversion. Mobile und Electron-Apps verwenden die Standard-Terminaleinstellungen des Systems.", "terminalCustomization": "Terminal-Anpassung", "appearance": "Aussehen", "behavior": "Verhalten", @@ -749,8 +773,8 @@ "chooseCursorAppearance": "Cursor-Erscheinungsbild wählen", "cursorBlink": "Cursor-Blinken", "enableCursorBlink": "Cursor-Blinkanimation aktivieren", - "scrollbackBuffer": "Rückwärts-Puffer", - "scrollbackBufferValue": "Rückwärts-Puffer: {{value}} Zeilen", + "scrollbackBuffer": "Scrollback-Puffer", + "scrollbackBufferValue": "Scrollback-Puffer: {{value}} Zeilen", "scrollbackBufferDesc": "Anzahl der Zeilen im Rückwärtsverlauf", "bellStyle": "Signalton-Stil", "selectBellStyle": "Signalton-Stil auswählen", @@ -784,9 +808,9 @@ "selectSnippet": "Snippet auswählen", "searchSnippets": "Snippets durchsuchen...", "snippetNone": "Keine", - "noneAuthTitle": "Keyboard-Interactive-Authentifizierung", - "noneAuthDescription": "Diese Authentifizierungsmethode verwendet beim Herstellen der Verbindung zum SSH-Server die Keyboard-Interactive-Authentifizierung.", - "noneAuthDetails": "Keyboard-Interactive-Authentifizierung ermöglicht dem Server, Sie während der Verbindung zur Eingabe von Anmeldeinformationen aufzufordern. Dies ist nützlich für Server, die eine Multi-Faktor-Authentifizierung oder eine dynamische Passworteingabe erfordern.", + "noneAuthTitle": "Tastatur-Interaktive Authentifizierung", + "noneAuthDescription": "Diese Authentifizierungsmethode verwendet beim Herstellen der Verbindung zum SSH-Server die Tastatur-Interaktive Authentifizierung.", + "noneAuthDetails": "Tastatur-Interaktive Authentifizierung ermöglicht dem Server, Sie während der Verbindung zur Eingabe von Anmeldeinformationen aufzufordern. Dies ist nützlich für Server, die eine Multi-Faktor-Authentifizierung oder eine dynamische Passworteingabe erfordern.", "forceKeyboardInteractive": "Tastatur-Interaktiv erzwingen", "forceKeyboardInteractiveDesc": "Erzwingt die Verwendung der tastatur-interaktiven Authentifizierung. Dies ist oft für Server erforderlich, die eine Zwei-Faktor-Authentifizierung (TOTP/2FA) verwenden.", "overrideCredentialUsername": "Benutzernamen der Anmeldedaten überschreiben", @@ -799,7 +823,42 @@ "searchServers": "Server durchsuchen...", "noServerFound": "Kein Server gefunden", "jumpHostsOrder": "Verbindungen werden in dieser Reihenfolge hergestellt: Jump-Host 1 → Jump-Host 2 → ... → Ziel-Server", - "advancedAuthSettings": "Erweiterte Authentifizierungseinstellungen" + "advancedAuthSettings": "Erweiterte Authentifizierungseinstellungen", + "addQuickAction": "Schnellaktion hinzufügen", + "allHostsInFolderDeleted": "{{count}} Hosts aus dem Ordner \"{{oderdner}}\" erfolgreich gelöscht", + "confirmDeleteAllHostsInFolder": "Sind Sie sicher, dass Sie alle {{count}} Hosts im Ordner \"{{oderdner}}\" löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", + "customCommands": "Benutzerdefinierte Befehle (Demnächst)", + "customCommandsDesc": "Definieren Sie benutzerdefinierte Befehle zum Herunterfahren und Neustarten für diesen Server", + "deleteAllHostsInFolder": "Alle Hosts im Ordner löschen", + "displayItems": "Elemente anzeigen", + "displayItemsDesc": "Wählen Sie, welche Metriken auf der Server-Statistikseite angezeigt werden sollen", + "editFolderAppearance": "Ordner-Aussehen bearbeiten", + "editFolderAppearanceDesc": "Passen Sie die Farbe und das Symbol für den Ordner an", + "enableCpu": "CPU-Auslastung", + "enableDisk": "Festplatten-Nutzung", + "enableHostname": "Hostname (Demnächst)", + "enableMemory": "Speichernutzung", + "enableNetwork": "Netzwerkstatistiken (Demnächst)", + "enableOs": "Betriebssystem (Demnächst)", + "enableProcesses": "Prozessanzahl (Demnächst)", + "enableServerStats": "Serverstatistiken aktivieren", + "enableServerStatsDesc": "Sammeln von Serverstatistiken für diesen Host aktivieren/deaktivieren", + "enableUptime": "Betriebszeit (Demnächst)", + "failedToDeleteHostsInFolder": "Fehler beim Löschen der Hosts im Ordner", + "failedToUpdateFolderAppearance": "Fehler beim Aktualisieren des Ordner-Aussehens", + "folderAppearanceUpdated": "Ordner-Aussehen erfolgreich aktualisiert", + "folderColor": "Ordnerfarbe", + "folderIcon": "Ordnersymbol", + "noSnippetFound": "Kein Snippet gefunden", + "preview": "Vorschau", + "quickActionName": "Aktionsname", + "quickActions": "Schnellaktionen", + "quickActionsDescription": "Mit Schnellaktionen können Sie benutzerdefinierte Schaltflächen erstellen, die SSH-Snippets auf diesem Server ausführen. Diese Schaltflächen werden oben auf der Server-Statistikseite für schnellen Zugriff angezeigt.", + "quickActionsList": "Liste der Schnellaktionen", + "quickActionsOrder": "Schnellaktions-Schaltflächen werden in der oben aufgeführten Reihenfolge auf der Server-Statistikseite angezeigt", + "rebootCommand": "Neustart-Befehl", + "serverStats": "Server-Statistiken", + "shutdownCommand": "Herunterfahren-Befehl" }, "terminal": { "title": "Terminal", @@ -808,7 +867,7 @@ "clear": "Löschen", "copy": "Kopieren", "paste": "Einfügen", - "find": "Finden", + "find": "Suchen", "fullscreen": "Vollbild", "splitHorizontal": "Horizontal teilen", "splitVertical": "Vertikal teilen", @@ -827,13 +886,17 @@ "messageParseError": "Servernachricht konnte nicht analysiert werden", "websocketError": "WebSocket-Verbindungsfehler", "connecting": "Verbindung wird hergestellt...", - "reconnecting": "Verbindung wird wiederhergestellt... ({{attempt}}\/{{max}})", + "reconnecting": "Verbindung wird wiederhergestellt... ({{attempt}}/{{max}})", "reconnected": "Erfolgreich wiederverbunden", "maxReconnectAttemptsReached": "Maximale Anzahl an Wiederverbindungsversuchen erreicht", "connectionTimeout": "Zeitüberschreitung der Verbindung", "terminalTitle": "Terminal - {{host}}", "terminalWithPath": "Terminal - {{host}} : {{path}}", - "runTitle": "Ausführen von {{command}} – {{host}}" + "runTitle": "Ausführen von {{command}} – {{host}}", + "totpCodeLabel": "Bestätigungscode", + "totpPlaceholder": "000000", + "totpRequired": "Zwei-Faktor-Authentifizierung erforderlich", + "totpVerify": "Bestätigen" }, "fileManager": { "title": "Dateimanager", @@ -856,7 +919,7 @@ "deleteItem": "Element löschen", "currentPath": "Aktueller Pfad", "uploadFileTitle": "Datei hochladen", - "maxFileSize": "Max.: 1 GB (JSON) \/ 5 GB (Binär) – Große Dateien werden unterstützt", + "maxFileSize": "Max.: 1 GB (JSON) / 5 GB (Binär) – Große Dateien werden unterstützt", "removeFile": "Datei entfernen", "clickToSelectFile": "Klicken Sie, um eine Datei auszuwählen", "chooseFile": "Datei auswählen", @@ -940,11 +1003,11 @@ "path": "Pfad", "confirmDelete": "Sind Sie sicher, dass Sie {{name}} löschen möchten?", "uploadSuccess": "Datei erfolgreich hochgeladen", - "uploadFailed": "Datei-Upload fehlgeschlagen", + "uploadFailed": "Datei-Hochladen fehlgeschlagen", "downloadSuccess": "Datei erfolgreich heruntergeladen", - "downloadFailed": "Datei-Download fehlgeschlagen", + "downloadFailed": "Datei-Herunterladen fehlgeschlagen", "permissionDenied": "Zugriff verweigert", - "checkDockerLogs": "Überprüfen Sie die Docker-Logs auf detaillierte Fehlerinformationen", + "checkDockerLogs": "Überprüfen Sie die Docker-Protokolle auf detaillierte Fehlerinformationen", "internalServerError": "Interner Serverfehler aufgetreten", "serverError": "Serverfehler", "error": "Fehler", @@ -961,11 +1024,11 @@ "connectToServer": "Mit einem Server verbinden", "selectServerToEdit": "Wähle einen Server in der Seitenleiste aus, um mit der Bearbeitung von Dateien zu beginnen", "fileOperations": "Dateioperationen", - "confirmDeleteMessage": "Möchten Sie {{name}}<\/strong> wirklich löschen?", - "confirmDeleteSingleItem": "Möchten Sie „ {{name}} “ wirklich dauerhaft löschen?", + "confirmDeleteMessage": "Möchten Sie {{name}} wirklich löschen?", + "confirmDeleteSingleItem": "Möchten Sie \"{{name}}\" wirklich dauerhaft löschen?", "confirmDeleteMultipleItems": "Sind Sie sicher, dass Sie {{name}} löschen möchten?", "confirmDeleteMultipleItemsWithFolders": "Möchten Sie wirklich {{count}} Elemente dauerhaft löschen? Dies schließt Ordner und deren Inhalte ein.", - "confirmDeleteFolder": "Möchten Sie den Ordner „ {{name}} “ und seinen gesamten Inhalt wirklich dauerhaft löschen?", + "confirmDeleteFolder": "Möchten Sie den Ordner \"{{name}}\" und seinen gesamten Inhalt wirklich dauerhaft löschen?", "deleteDirectoryWarning": "Dadurch werden der Ordner und sein gesamter Inhalt gelöscht.", "actionCannotBeUndone": "Diese Aktion kann nicht rückgängig gemacht werden.", "permanentDeleteWarning": "Diese Aktion kann nicht rückgängig gemacht werden. Die Elemente werden dauerhaft vom Server gelöscht.", @@ -998,14 +1061,14 @@ "runningFile": "Wird ausgeführt - {{file}}", "onlyRunExecutableFiles": "Kann nur ausführbare Dateien ausführen", "noHostSelected": "Kein Host ausgewählt", - "starred": "Mit Stern markiert", - "shortcuts": "Kurzbefehle", + "starred": "Favoriten", + "shortcuts": "Verknüpfungen", "directories": "Verzeichnisse", - "removedFromRecentFiles": "„ {{name}} “ aus den zuletzt verwendeten letzten Dateien entfernt", + "removedFromRecentFiles": "\"{{name}}\" aus den zuletzt verwendeten Dateien entfernt", "removeFailed": "Entfernen fehlgeschlagen", - "unpinnedSuccessfully": "\" {{name}} \" erfolgreich gelöst", + "unpinnedSuccessfully": "\"{{name}}\" erfolgreich gelöst", "unpinFailed": "Anheften fehlgeschlagen", - "removedShortcut": "Verknüpfung „ {{name}} “ entfernt", + "removedShortcut": "Verknüpfung \"{{name}}\" entfernt", "removeShortcutFailed": "Verknüpfung entfernen fehlgeschlagen", "clearedAllRecentFiles": "Alle zuletzt verwendeten Dateien gelöscht", "clearFailed": "Löschen fehlgeschlagen", @@ -1015,7 +1078,7 @@ "removeShortcut": "Verknüpfung entfernen", "saveFilesToSystem": "{{count}} Dateien speichern unter...", "pinFile": "Datei anheften", - "addToShortcuts": "Zu Shortcuts hinzufügen", + "addToShortcuts": "Zu Verknüpfungen hinzufügen", "downloadToDefaultLocation": "An Standardort herunterladen", "pasteFailed": "Einfügen fehlgeschlagen", "noUndoableActions": "Keine rückgängig zu machenden Aktionen", @@ -1063,9 +1126,9 @@ "startTyping": "Beginnen Sie mit der Eingabe...", "unknownSize": "Unbekannte Größe", "fileIsEmpty": "Die Datei ist leer", - "largeFileWarning": "Warnung zu großen Dateie(n)", + "largeFileWarning": "Warnung: Große Datei", "largeFileWarningDesc": "Diese Datei hat eine Größe {{size}}, was beim Öffnen als Text zu Leistungsproblemen führen kann.", - "fileNotFoundAndRemoved": "Datei „ {{name}} “ nicht gefunden und aus den letzten\/angehefteten Dateien entfernt", + "fileNotFoundAndRemoved": "Datei \"{{name}}\" nicht gefunden und aus den letzten/angehefteten Dateien entfernt", "failedToLoadFile": "Datei konnte nicht geladen werden: {{error}}", "serverErrorOccurred": "Es ist ein Serverfehler aufgetreten. Bitte versuchen Sie es später noch einmal.", "autoSaveFailed": "Automatisches Speichern fehlgeschlagen", @@ -1077,17 +1140,17 @@ "dragFailed": "Ziehvorgang fehlgeschlagen", "filePinnedSuccessfully": "Datei \"{{name}}\" erfolgreich angeheftet", "pinFileFailed": "Datei konnte nicht angeheftet werden", - "fileUnpinnedSuccessfully": "Datei „ {{name}} “ erfolgreich gelöst", + "fileUnpinnedSuccessfully": "Datei \"{{name}}\" erfolgreich gelöst", "unpinFileFailed": "Fehler beim Lösen der Datei", - "shortcutAddedSuccessfully": "Ordnerverknüpfung „ {{name}} “ erfolgreich hinzugefügt", + "shortcutAddedSuccessfully": "Ordnerverknüpfung \"{{name}}\" erfolgreich hinzugefügt", "addShortcutFailed": "Verknüpfung konnte nicht hinzugefügt werden", "operationCompletedSuccessfully": "{{operation}} {{count}} Element(e) erfolgreich", "operationCompleted": "{{operation}} {{count}} Element(e)", "downloadFileSuccess": "Datei {{name}} erfolgreich heruntergeladen", - "downloadFileFailed": "Download fehlgeschlagen", + "downloadFileFailed": "Herunterladen fehlgeschlagen", "moveTo": "Nach {{name}} verschieben", "diffCompareWith": "Diff-Vergleich mit {{name}}", - "dragOutsideToDownload": "Zum Herunterladen aus dem Fenster ziehen ( {{count}} Dateien)", + "dragOutsideToDownload": "Zum Herunterladen aus dem Fenster ziehen ({{count}} Dateien)", "newFolderDefault": "Neuer Ordner", "newFileDefault": "NeueDatei.txt", "successfullyMovedItems": "{{count}} Elemente erfolgreich nach {{target}} verschoben", @@ -1102,8 +1165,38 @@ "inline": "Inline", "fileComparison": "Dateivergleich: {{file1}} vs {{file2}}", "fileTooLarge": "Datei zu groß: {{error}}", - "sshConnectionFailed": "SSH-Verbindung fehlgeschlagen. Bitte überprüfen Sie Ihre Verbindung zu {{name}} ( {{ip}} : {{port}} )", - "loadFileFailed": "Datei konnte nicht geladen werden: {{error}}" + "sshConnectionFailed": "SSH-Verbindung fehlgeschlagen. Bitte überprüfen Sie Ihre Verbindung zu {{name}} ({{ip}}:{{port}})", + "loadFileFailed": "Datei konnte nicht geladen werden: {{error}}", + "andMoreFiles": "und {{count}} weitere...", + "archiveExtractedSuccessfully": "{{name}} erfolgreich extrahiert", + "archiveName": "Archivname", + "changePermissions": "Berechtigungen ändern", + "changePermissionsDesc": "Dateiberechtigungen ändern für", + "compress": "Komprimieren", + "compressFailed": "Komprimierung fehlgeschlagen", + "compressFile": "Datei komprimieren", + "compressFiles": "Dateien komprimieren", + "compressFilesDesc": "Komprimiere {{count}} Elemente in ein Archiv", + "compressingFiles": "{{count}} Elemente werden in {{name}} komprimiert...", + "compressionFormat": "Komprimierungsformat", + "connectedSuccessfully": "Erfolgreich verbunden", + "currentPermissions": "Aktuelle Berechtigungen", + "enterArchiveName": "Archivnamen eingeben...", + "execute": "Ausführen", + "extractArchive": "Archiv extrahieren", + "extractFailed": "Extrahieren fehlgeschlagen", + "extractingArchive": "Extrahiere {{name}}...", + "failedToChangePermissions": "Berechtigungen konnten nicht geändert werden", + "filesCompressedSuccessfully": "{{name}} erfolgreich erstellt", + "group": "Gruppe", + "newPermissions": "Neue Berechtigungen", + "others": "Andere", + "owner": "Eigentümer", + "permissionsChangedSuccessfully": "Berechtigungen erfolgreich geändert", + "read": "Lesen", + "selectedFiles": "Ausgewählte Dateien", + "totpVerificationFailed": "TOTP-Verifizierung fehlgeschlagen", + "write": "Schreiben" }, "tunnels": { "title": "SSH-Tunnel", @@ -1153,7 +1246,7 @@ "portMapping": "Port {{sourcePort}} → {{endpointHost}} : {{endpointPort}}", "endpointHostNotFound": "Endpunkthost nicht gefunden", "discord": "Discord", - "githubIssue": "GitHub-issue", + "githubIssue": "GitHub-Issue", "forHelp": "für Hilfe" }, "serverStats": { @@ -1178,8 +1271,8 @@ "openFileManager": "Dateimanager öffnen", "cpuCores_one": "{{count}} CPU", "cpuCores_other": "{{count}} CPUs", - "naCpus": "N\/A CPU(s)", - "loadAverageNA": "Durchschnitt: N\/A", + "naCpus": "N/A CPU(s)", + "loadAverageNA": "Durchschnitt: N/A", "cpuUsage": "CPU-Auslastung", "memoryUsage": "Speicherauslastung", "diskUsage": "Festplattennutzung", @@ -1195,7 +1288,6 @@ "serverOffline": "Server offline", "cannotFetchMetrics": "Metriken können nicht vom Offline-Server abgerufen werden", "load": "Last", - "available": "Verfügbar", "editLayout": "Layout anpassen", "cancelEdit": "Abbrechen", "addWidget": "Widget hinzufügen", @@ -1203,7 +1295,7 @@ "unsavedChanges": "Ungespeicherte Änderungen", "layoutSaved": "Layout erfolgreich gespeichert", "failedToSaveLayout": "Speichern des Layout fehlgeschlagen", - "systemInfo": "System Information", + "systemInfo": "Systeminformationen", "hostname": "Hostname", "operatingSystem": "Betriebssystem", "kernel": "Kernel", @@ -1220,7 +1312,15 @@ "recentSuccessfulLogins": "Letzte erfolgreiche Anmeldungen", "recentFailedAttempts": "Letzte fehlgeschlagene Versuche", "noRecentLoginData": "Keine aktuellen Anmeldedaten", - "from": "von" + "from": "von", + "executeQuickAction": "Ausführen {{name}}", + "executingQuickAction": "Führe {{name}} aus...", + "quickActionError": "Fehler beim Ausführen von {{name}}", + "quickActionFailed": "{{name}} fehlgeschlagen", + "quickActionSuccess": "{{name}} erfolgreich abgeschlossen", + "quickActions": "Schnellaktionen", + "totpRequired": "TOTP-Authentifizierung erforderlich", + "totpUnavailable": "Serverstatistiken nicht verfügbar für TOTP-aktivierte Server" }, "auth": { "tagline": "SSH SERVER MANAGER", @@ -1233,7 +1333,7 @@ "loginButton": "Anmelden", "registerButton": "Registrieren", "forgotPassword": "Passwort vergessen?", - "rememberMe": "Erinnere dich an mich", + "rememberMe": "Angemeldet bleiben", "noAccount": "Sie haben noch kein Konto?", "hasAccount": "Sie haben bereits ein Konto?", "loginSuccess": "Anmeldung erfolgreich", @@ -1251,7 +1351,7 @@ "enableTwoFactor": "Zwei-Faktor-Authentifizierung aktivieren", "disableTwoFactor": "Zwei-Faktor-Authentifizierung deaktivieren", "scanQRCode": "Scannen Sie diesen QR-Code mit Ihrer Authentifizierungs-App", - "backupCodes": "Sicherungs-Codes", + "backupCodes": "Backup-Codes", "saveBackupCodes": "Bewahren Sie diese Backup-Codes an einem sicheren Ort auf", "twoFactorEnabledSuccess": "Zwei-Faktor-Authentifizierung erfolgreich aktiviert!", "twoFactorDisabled": "Zwei-Faktor-Authentifizierung deaktiviert", @@ -1302,7 +1402,7 @@ "resetCodeDesc": "Geben Sie Ihren Benutzernamen ein, um einen Code zum Zurücksetzen des Passworts zu erhalten. Der Code wird in den Docker-Container-Protokollen angezeigt.", "resetCode": "Code zurücksetzen", "verifyCodeButton": "Code bestätigen", - "enterResetCode": "Geben Sie den 6-stelligen Code aus den Docker-Container-Protokollen \/ logs für den Benutzer ein:", + "enterResetCode": "Geben Sie den 6-stelligen Code aus den Docker-Container-Protokollen für den Benutzer ein:", "goToLogin": "Zum Login", "newPassword": "Neues Passwort", "confirmNewPassword": "Passwort bestätigen", @@ -1312,15 +1412,25 @@ "signUp": "Registrierung", "dataLossWarning": "Wenn Sie Ihr Passwort auf diese Weise zurücksetzen, werden alle Ihre gespeicherten SSH-Hosts, Anmeldeinformationen und andere verschlüsselte Daten gelöscht. Diese Aktion kann nicht rückgängig gemacht werden. Verwenden Sie diese Option nur, wenn Sie Ihr Passwort vergessen haben und nicht angemeldet sind.", "sshAuthenticationRequired": "SSH-Authentifizierung erforderlich", - "sshNoKeyboardInteractive": "Keyboard-Interactive-Authentifizierung nicht verfügbar", + "sshNoKeyboardInteractive": "Tastatur-Interaktive Authentifizierung nicht verfügbar", "sshAuthenticationFailed": "Authentifizierung fehlgeschlagen", "sshAuthenticationTimeout": "Authentifizierungs-Timeout", - "sshNoKeyboardInteractiveDescription": "Der Server unterstützt keine Keyboard-Interactive-Authentifizierung. Bitte geben Sie Ihr Passwort oder Ihren SSH-Schlüssel ein.", + "sshNoKeyboardInteractiveDescription": "Der Server unterstützt keine Tastatur-Interaktive Authentifizierung. Bitte geben Sie Ihr Passwort oder Ihren SSH-Schlüssel ein.", "sshAuthFailedDescription": "Die angegebenen Anmeldeinformationen waren falsch. Bitte versuchen Sie es erneut mit gültigen Anmeldeinformationen.", "sshTimeoutDescription": "Der Authentifizierungsversuch ist abgelaufen. Bitte versuchen Sie es erneut.", "sshProvideCredentialsDescription": "Bitte geben Sie Ihre SSH-Anmeldeinformationen ein, um eine Verbindung zu diesem Server herzustellen.", "sshPasswordDescription": "Geben Sie das Passwort für diese SSH-Verbindung ein.", - "sshKeyPasswordDescription": "Wenn Ihr SSH-Schlüssel verschlüsselt ist, geben Sie hier die Passphrase ein." + "sshKeyPasswordDescription": "Wenn Ihr SSH-Schlüssel verschlüsselt ist, geben Sie hier die Passphrase ein.", + "authenticating": "Authentifizierung läuft...", + "authenticationDisabled": "Authentifizierung deaktiviert", + "authenticationDisabledDesc": "Alle Authentifizierungsmethoden sind derzeit deaktiviert. Bitte kontaktieren Sie Ihren Administrator.", + "desktopApp": "Desktop App", + "loadingServer": "Lade Server...", + "loggingInToDesktopApp": "Anmelden bei der Desktop-App", + "loggingInToDesktopAppViaWeb": "Anmelden bei der Desktop-App über Webinterface", + "loggingInToMobileApp": "Anmelden bei der Mobile-App", + "mobileApp": "Mobile App", + "redirectingToApp": "Weiterleitung zur App..." }, "errors": { "notFound": "Seite nicht gefunden", @@ -1331,9 +1441,9 @@ "databaseConnection": "Es konnte keine Verbindung zur Datenbank hergestellt werden.", "unknownError": "Unbekannter Fehler", "loginFailed": "Anmeldung fehlgeschlagen", - "failedPasswordReset": "Das Zurücksetzen des Kennworts konnte nicht eingeleitet werden.", + "failedPasswordReset": "Das Zurücksetzen des Passworts konnte nicht eingeleitet werden.", "failedVerifyCode": "Reset-Code konnte nicht verifiziert werden", - "failedCompleteReset": "Das Zurücksetzen des Kennworts konnte nicht abgeschlossen werden.", + "failedCompleteReset": "Das Zurücksetzen des Passworts konnte nicht abgeschlossen werden.", "invalidTotpCode": "Ungültiger TOTP-Code", "failedOidcLogin": "Fehler beim Starten der OIDC-Anmeldung", "failedUserInfo": "Fehler beim Abrufen von Benutzerinformationen nach der OIDC-Anmeldung", @@ -1383,7 +1493,7 @@ "title": "Benutzerprofil", "description": "Verwalten Sie Ihre Kontoeinstellungen und Sicherheit", "security": "Sicherheit", - "changePassword": "Kennwort ändern", + "changePassword": "Passwort ändern", "twoFactorAuth": "Zwei-Faktor-Authentifizierung", "accountInfo": "Kontoinformationen", "role": "Rolle", @@ -1393,13 +1503,16 @@ "local": "Lokal", "external": "Extern (OIDC)", "selectPreferredLanguage": "Wählen Sie Ihre bevorzugte Sprache für die Benutzeroberfläche", - "fileColorCoding": "Dateifarb-Codierung", + "fileColorCoding": "Dateifarbcodierung", "fileColorCodingDesc": "Farbcodierung von Dateien nach Typ: Ordner (rot), Dateien (blau), Symlinks (grün)", "commandAutocomplete": "Befehlsautovervollständigung", "commandAutocompleteDesc": "Tab-Taste Autovervollständigung für Terminal-Befehle basierend auf Ihrem Befehlsverlauf aktivieren", + "defaultSnippetFoldersCollapsed": "Snippet-Ordner standardmäßig einklappen", + "defaultSnippetFoldersCollapsedDesc": "Wenn aktiviert, werden alle Snippet-Ordner beim Öffnen der Snippet-Registerkarte eingeklappt", "currentPassword": "Aktuelles Passwort", "passwordChangedSuccess": "Passwort erfolgreich geändert! Bitte melden Sie sich erneut an.", - "failedToChangePassword": "Passwort konnte nicht geändert werden. Bitte überprüfen Sie Ihr aktuelles Passwort und versuchen Sie es erneut." + "failedToChangePassword": "Passwort konnte nicht geändert werden. Bitte überprüfen Sie Ihr aktuelles Passwort und versuchen Sie es erneut.", + "externalAndLocal": "Dual Auth" }, "user": { "failedToLoadVersionInfo": "Fehler beim Laden der Versionsinformationen" @@ -1415,23 +1528,23 @@ "hostname": "Hostname", "folder": "Ordner", "password": "Passwort", - "keyPassword": "Schlüsselkennwort", + "keyPassword": "Schlüsselpasswort", "pastePrivateKey": "Fügen Sie hier Ihren privaten Schlüssel ein ...", "pastePublicKey": "Fügen Sie hier Ihren öffentlichen Schlüssel ein ...", "credentialName": "Mein SSH-Server", "description": "Beschreibung der SSH-Anmeldeinformationen", "searchCredentials": "Suchen Sie Anmeldeinformationen nach Name, Benutzername oder Tags ...", "sshConfig": "Endpunkt-SSH-Konfiguration", - "homePath": "\/home", + "homePath": "/home", "clientId": "Ihre Client-ID", "clientSecret": "Ihr Client-Geheimnis", - "authUrl": "https:\/\/ihr-anbieter.com\/application\/o\/authorize\/", - "redirectUrl": "https:\/\/ihr-anbieter.com\/application\/o\/termix\/", - "tokenUrl": "https:\/\/ihr-anbieter.com\/application\/o\/token\/", + "authUrl": "https://ihr-anbieter.com/application/o/authorize/", + "redirectUrl": "https://ihr-anbieter.com/application/o/termix/", + "tokenUrl": "https://ihr-anbieter.com/application/o/token/", "userIdField": "sub", "usernameField": "name", "scopes": "openid email profile", - "userinfoUrl": "https:\/\/ihr-anbieter.com\/application\/o\/userinfo\/", + "userinfoUrl": "https://ihr-anbieter.com/application/o/userinfo/", "enterUsername": "Geben Sie den Benutzernamen ein, um zum Administrator zu werden", "searchHosts": "Suchen Sie nach Hosts nach Name, Benutzername, IP, Ordner, Tags usw.", "enterPassword": "Geben Sie Ihr Passwort ein", @@ -1452,7 +1565,7 @@ "failedToDeleteAccount": "Konto konnte nicht gelöscht werden", "failedToMakeUserAdmin": "Fehler beim Festlegen des Benutzers als Administrator", "userIsNowAdmin": "Der Benutzer {{username}} ist jetzt ein Administrator", - "removeAdminConfirm": "Möchten Sie die Administrationsberechtigung von {{username}} wirklich entfernen?", + "removeAdminConfirm": "Möchten Sie die Administratorberechtigung von {{username}} wirklich entfernen?", "deleteUserConfirm": "Möchten Sie den Benutzer {{username}} wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.", "deleteAccount": "Konto löschen", "closeDeleteAccount": "Schließen Konto löschen", @@ -1460,19 +1573,19 @@ "deleteAccountWarningDetails": "Wenn Sie Ihr Konto löschen, werden alle Ihre Daten entfernt, einschließlich SSH-Hosts, Konfigurationen und Einstellungen. Diese Aktion ist nicht rückgängig zu machen.", "deleteAccountWarningShort": "Diese Aktion kann nicht rückgängig gemacht werden und löscht Ihr Konto dauerhaft.", "cannotDeleteAccount": "Konto kann nicht gelöscht werden", - "lastAdminWarning": "Sie sind der letzte Administrator. Sie können Ihr Konto nicht löschen, da das System dann ohne Administratoren wäre. Bitte benennen Sie zunächst einen anderen Benutzer als Administrator oder wenden Sie sich an den Systemsupport.", + "lastAdminWarning": "Sie sind der letzte Administrator. Sie können Ihr Konto nicht löschen, da das System dann ohne Administrator wäre. Bitte benennen Sie zunächst einen anderen Benutzer als Administrator oder wenden Sie sich an den Systemsupport.", "confirmPassword": "Passwort bestätigen", "deleting": "Löschen...", "cancel": "Abbrechen" }, "interface": { "sidebar": "Seitenleiste", - "toggleSidebar": "Seitenleiste ein-\/ausblenden", + "toggleSidebar": "Seitenleiste ein-/ausblenden", "close": "Schließen", "online": "Online", "offline": "Offline", "maintenance": "Wartung", - "degraded": "Herabgestuft", + "degraded": "Eingeschränkt", "noTunnelConnections": "Keine Tunnelverbindungen konfiguriert", "discord": "Discord", "connectToSshForOperations": "Stellen Sie eine SSH-Verbindung her, um Dateivorgänge zu verwenden", @@ -1509,7 +1622,7 @@ "disconnected": "Getrennt", "maxRetriesExhausted": "Maximale Wiederholungsversuche ausgeschöpft", "endpointHostNotFound": "Endpunkthost nicht gefunden", - "administrator": "Admin", + "administrator": "Administrator", "user": "Benutzer", "external": "Extern", "local": "Lokal", @@ -1523,19 +1636,19 @@ "verifyAndEnable": "Überprüfen und Aktivieren", "secretKey": "Geheimer Schlüssel", "totpQrCode": "TOTP-QR-Code", - "passwordRequired": "Bei Verwendung der Kennwortauthentifizierung ist ein Kennwort erforderlich", + "passwordRequired": "Bei Verwendung der Passwort-Authentifizierung ist ein Passwort erforderlich", "sshKeyRequired": "Bei Verwendung der Schlüsselauthentifizierung ist ein privater SSH-Schlüssel erforderlich", "keyTypeRequired": "Bei Verwendung der Schlüsselauthentifizierung ist der Schlüsseltyp erforderlich", "validSshConfigRequired": "Sie müssen eine gültige SSH-Konfiguration aus der Liste auswählen", "updateHost": "Host aktualisieren", "addHost": "Host hinzufügen", "editHost": "Host bearbeiten", - "pinConnection": "Pin-Verbindung", + "pinConnection": "Verbindung anheften", "authentication": "Authentifizierung", "password": "Passwort", "key": "Schlüssel", "sshPrivateKey": "Privater SSH-Schlüssel", - "keyPassword": "Schlüsselkennwort", + "keyPassword": "Schlüsselpasswort", "keyType": "Schlüsseltyp", "enableTerminal": "Terminal aktivieren", "enableTunnel": "Tunnel aktivieren", @@ -1551,9 +1664,9 @@ "developmentFolder": "Entwicklung", "webServerProduction": "Webserver – Produktion", "unknownError": "Unbekannter Fehler", - "failedToInitiatePasswordReset": "Das Zurücksetzen des Kennworts konnte nicht eingeleitet werden.", + "failedToInitiatePasswordReset": "Das Zurücksetzen des Passworts konnte nicht eingeleitet werden.", "failedToVerifyResetCode": "Fehler beim Überprüfen des Reset-Codes", - "failedToCompletePasswordReset": "Das Zurücksetzen des Kennworts konnte nicht abgeschlossen werden.", + "failedToCompletePasswordReset": "Das Zurücksetzen des Passworts konnte nicht abgeschlossen werden.", "invalidTotpCode": "Ungültiger TOTP-Code", "failedToStartOidcLogin": "Fehler beim Starten der OIDC-Anmeldung", "failedToGetUserInfoAfterOidc": "Fehler beim Abrufen von Benutzerinformationen nach der OIDC-Anmeldung", @@ -1588,7 +1701,7 @@ "serverOverview": "Serverübersicht", "version": "Version", "upToDate": "Auf dem neuesten Stand", - "updateAvailable": "Update verfügbar", + "updateAvailable": "Aktualisierung verfügbar", "uptime": "Betriebszeit", "database": "Datenbank", "healthy": "Gesund", @@ -1603,7 +1716,7 @@ "quickActions": "Schnellaktionen", "addHost": "Host hinzufügen", "addCredential": "Anmeldedaten hinzufügen", - "adminSettings": "Admin-Einstellungen", + "adminSettings": "Administratoreinstellungen", "userProfile": "Benutzerprofil", "serverStats": "Serverstatistiken", "loadingServerStats": "Serverstatistiken werden geladen...", @@ -1618,7 +1731,7 @@ "navigation": "Navigation", "addHost": "Host hinzufügen", "addCredential": "Anmeldedaten hinzufügen", - "adminSettings": "Admin-Einstellungen", + "adminSettings": "Administratoreinstellungen", "userProfile": "Benutzerprofil", "updateLog": "Aktualisierungsprotokoll", "hosts": "Hosts", @@ -1634,5 +1747,39 @@ "toToggle": "zum Umschalten", "close": "Schließen", "hostManager": "Host-Manager" + }, + "snippets": { + "content": "Befehl", + "contentPlaceholder": "z.B. sudo systemctl restart nginx", + "contentRequired": "Befehl ist erforderlich", + "copySuccess": "\"{{name}}\" in die Zwischenablage kopiert", + "copyTooltip": "Snippet in die Zwischenablage kopieren", + "create": "Snippet erstellen", + "createDescription": "Erstellen Sie ein neues Befehls-Snippet für die schnelle Ausführung", + "createFailed": "Fehler beim Erstellen des Snippets", + "createSuccess": "Snippet erfolgreich erstellt", + "deleteConfirmDescription": "Sind Sie sicher, dass Sie \"{{name}}\" löschen möchten?", + "deleteConfirmTitle": "Snippet löschen", + "deleteFailed": "Fehler beim Löschen des Snippets", + "deleteSuccess": "Snippet erfolgreich gelöscht", + "deleteTooltip": "Dieses Snippet löschen", + "description": "Beschreibung", + "descriptionPlaceholder": "Optionale Beschreibung", + "edit": "Snippet bearbeiten", + "editDescription": "Dieses Befehls-Snippet bearbeiten", + "editTooltip": "Dieses Snippet bearbeiten", + "empty": "Noch keine Snippets", + "emptyHint": "Erstellen Sie ein Snippet, um häufig verwendete Befehle zu speichern", + "executeSuccess": "Ausführen: {{name}}", + "failedToFetch": "Fehler beim Abrufen der Snippets", + "name": "Name", + "namePlaceholder": "z.B. Restart Nginx", + "nameRequired": "Name ist erforderlich", + "new": "Neues Snippet", + "run": "Ausführen", + "runTooltip": "Dieses Snippet im Terminal ausführen", + "title": "Snippets", + "updateFailed": "Fehler beim Aktualisieren des Snippets", + "updateSuccess": "Snippet erfolgreich aktualisiert" } -} +} \ No newline at end of file diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 72aa1f8f..199fb9b0 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -59,7 +59,6 @@ "keyTypeRSA": "RSA", "keyTypeECDSA": "ECDSA", "keyTypeEd25519": "Ed25519", - "updateCredential": "Update Credential", "basicInfo": "Basic Info", "authentication": "Authentication", "organization": "Organization", @@ -119,7 +118,6 @@ "credentialSecuredDescription": "All sensitive data is encrypted with AES-256", "passwordAuthentication": "Password Authentication", "keyAuthentication": "Key Authentication", - "keyType": "Key Type", "securityReminder": "Security Reminder", "securityReminderText": "Never share your credentials. All data is encrypted at rest.", "hostsUsingCredential": "Hosts Using This Credential", @@ -299,7 +297,7 @@ "warning": "Warning", "info": "Info", "success": "Success", - "loading": "Loading", + "loading": "Loading...", "required": "Required", "optional": "Optional", "connect": "Connect", @@ -315,7 +313,6 @@ "updateAvailable": "Update Available", "sshPath": "SSH Path", "localPath": "Local Path", - "loading": "Loading...", "noAuthCredentials": "No authentication credentials available for this SSH host", "noReleases": "No Releases", "updatesAndReleases": "Updates & Releases", @@ -330,13 +327,10 @@ "resetPassword": "Reset Password", "resetCode": "Reset Code", "newPassword": "New Password", - "sshPath": "SSH Path", - "localPath": "Local Path", "folder": "Folder", "file": "File", "renamedSuccessfully": "renamed successfully", "deletedSuccessfully": "deleted successfully", - "noAuthCredentials": "No authentication credentials available for this SSH host", "noTunnelConnections": "No tunnel connections configured", "sshTools": "SSH Tools", "english": "English", @@ -348,14 +342,12 @@ "login": "Login", "logout": "Logout", "register": "Register", - "username": "Username", "password": "Password", "version": "Version", "confirmPassword": "Confirm Password", "back": "Back", "email": "Email", "submit": "Submit", - "cancel": "Cancel", "change": "Change", "save": "Save", "saving": "Saving...", @@ -363,22 +355,15 @@ "edit": "Edit", "add": "Add", "search": "Search", - "loading": "Loading...", - "error": "Error", - "success": "Success", - "warning": "Warning", - "info": "Info", "confirm": "Confirm", "yes": "Yes", "no": "No", "ok": "OK", - "close": "Close", "enabled": "Enabled", "disabled": "Disabled", "important": "Important", "notEnabled": "Not Enabled", "settingUp": "Setting up...", - "back": "Back", "next": "Next", "previous": "Previous", "refresh": "Refresh", @@ -402,7 +387,11 @@ "documentation": "Documentation", "retry": "Retry", "checking": "Checking...", - "checkingDatabase": "Checking database connection..." + "checkingDatabase": "Checking database connection...", + "actions": "Actions", + "remove": "Remove", + "revoke": "Revoke", + "create": "Create" }, "nav": { "home": "Home", @@ -431,7 +420,7 @@ "userManagement": "User Management", "makeAdmin": "Make Admin", "removeAdmin": "Remove Admin", - "deleteUser": "Delete User", + "deleteUser": "Delete user {{username}}? This cannot be undone.", "allowRegistration": "Allow Registration", "oidcSettings": "OIDC Settings", "clientId": "Client ID", @@ -485,7 +474,6 @@ "removeAdminStatus": "Remove admin status from {{username}}?", "adminStatusRemoved": "Admin status removed from {{username}}", "failedToRemoveAdminStatus": "Failed to remove admin status", - "deleteUser": "Delete user {{username}}? This cannot be undone.", "userDeletedSuccessfully": "User {{username}} deleted successfully", "failedToDeleteUser": "Failed to delete user", "overrideUserInfoUrl": "Override User Info URL (not required)", @@ -563,7 +551,6 @@ "verificationCompleted": "Compatibility verification completed - no data was changed", "verificationInProgress": "Verification completed", "dataMigrationCompleted": "Data migration completed successfully!", - "migrationCompleted": "Migration completed", "verificationFailed": "Compatibility verification failed", "migrationFailed": "Migration failed", "runningVerification": "Running compatibility verification...", @@ -641,7 +628,8 @@ "requiresPasswordLogin": "Requires password login enabled", "passwordLoginDisabledWarning": "Password login is disabled. Ensure OIDC is properly configured or you will not be able to log in to Termix.", "oidcRequiredWarning": "CRITICAL: Password login is disabled. If you reset or misconfigure OIDC, you will lose all access to Termix and brick your instance. Only proceed if you are absolutely certain.", - "confirmDisableOIDCWarning": "WARNING: You are about to disable OIDC while password login is also disabled. This will brick your Termix instance and you will lose all access. Are you absolutely sure you want to proceed?" + "confirmDisableOIDCWarning": "WARNING: You are about to disable OIDC while password login is also disabled. This will brick your Termix instance and you will lose all access. Are you absolutely sure you want to proceed?", + "failedToUpdatePasswordLoginStatus": "Failed to update password login status" }, "hosts": { "title": "Host Manager", @@ -950,7 +938,9 @@ "quickActionName": "Action name", "noSnippetFound": "No snippet found", "quickActionsOrder": "Quick action buttons will appear in the order listed above on the Server Stats page", - "advancedAuthSettings": "Advanced Authentication Settings" + "advancedAuthSettings": "Advanced Authentication Settings", + "sudoPasswordAutoFill": "Sudo Password Auto-Fill", + "sudoPasswordAutoFillDesc": "Automatically offer to insert SSH password when sudo prompts for password" }, "terminal": { "title": "Terminal", @@ -988,7 +978,11 @@ "totpRequired": "Two-Factor Authentication Required", "totpCodeLabel": "Verification Code", "totpPlaceholder": "000000", - "totpVerify": "Verify" + "totpVerify": "Verify", + "sudoPasswordPopupTitle": "Insert Password?", + "sudoPasswordPopupHint": "Press Enter to insert, Esc to dismiss", + "sudoPasswordPopupConfirm": "Insert", + "sudoPasswordPopupDismiss": "Dismiss" }, "fileManager": { "title": "File Manager", @@ -1090,7 +1084,6 @@ "copyPaths": "Copy Paths", "delete": "Delete", "properties": "Properties", - "preview": "Preview", "refresh": "Refresh", "downloadFiles": "Download {{count}} files to Browser", "copyFiles": "Copy {{count}} items", @@ -1105,18 +1098,11 @@ "failedToDeleteItem": "Failed to delete item", "itemRenamedSuccessfully": "{{type}} renamed successfully", "failedToRenameItem": "Failed to rename item", - "upload": "Upload", "download": "Download", - "newFile": "New File", - "newFolder": "New Folder", - "rename": "Rename", - "delete": "Delete", "permissions": "Permissions", "size": "Size", "modified": "Modified", "path": "Path", - "fileName": "File Name", - "folderName": "Folder Name", "confirmDelete": "Are you sure you want to delete {{name}}?", "uploadSuccess": "File uploaded successfully", "uploadFailed": "File upload failed", @@ -1136,10 +1122,7 @@ "fileSavedSuccessfully": "File saved successfully", "saveTimeout": "Save operation timed out. The file may have been saved successfully, but the operation took too long to complete. Check the Docker logs for confirmation.", "failedToSaveFile": "Failed to save file", - "folder": "Folder", - "file": "File", "deletedSuccessfully": "deleted successfully", - "failedToDeleteItem": "Failed to delete item", "connectToServer": "Connect to a Server", "selectServerToEdit": "Select a server from the sidebar to start editing files", "fileOperations": "File Operations", @@ -1196,10 +1179,8 @@ "unpinFile": "Unpin file", "removeShortcut": "Remove shortcut", "saveFilesToSystem": "Save {{count}} files as...", - "saveToSystem": "Save as...", "pinFile": "Pin file", "addToShortcuts": "Add to shortcuts", - "selectLocationToSave": "Select location to save", "downloadToDefaultLocation": "Download to default location", "pasteFailed": "Paste failed", "noUndoableActions": "No undoable actions", @@ -1217,7 +1198,6 @@ "editPath": "Edit path", "confirm": "Confirm", "cancel": "Cancel", - "folderName": "Folder name", "find": "Find...", "replaceWith": "Replace with...", "replace": "Replace", @@ -1243,23 +1223,18 @@ "outdent": "Outdent", "autoComplete": "Auto Complete", "imageLoadError": "Failed to load image", - "zoomIn": "Zoom In", - "zoomOut": "Zoom Out", "rotate": "Rotate", "originalSize": "Original Size", "startTyping": "Start typing...", "unknownSize": "Unknown size", "fileIsEmpty": "File is empty", - "modified": "Modified", "largeFileWarning": "Large File Warning", "largeFileWarningDesc": "This file is {{size}} in size, which may cause performance issues when opened as text.", "fileNotFoundAndRemoved": "File \"{{name}}\" not found and has been removed from recent/pinned files", "failedToLoadFile": "Failed to load file: {{error}}", "serverErrorOccurred": "Server error occurred. Please try again later.", - "fileSavedSuccessfully": "File saved successfully", "autoSaveFailed": "Auto-save failed", "fileAutoSaved": "File auto-saved", - "fileDownloadedSuccessfully": "File downloaded successfully", "moveFileFailed": "Failed to move {{name}}", "moveOperationFailed": "Move operation failed", "canOnlyCompareFiles": "Can only compare two files", @@ -1353,17 +1328,8 @@ "local": "Local", "remote": "Remote", "dynamic": "Dynamic", - "noSshTunnels": "No SSH Tunnels", - "createFirstTunnelMessage": "Create your first SSH tunnel to get started. Use the SSH Manager to add hosts with tunnel connections.", "unknownConnectionStatus": "Unknown", - "connected": "Connected", - "connecting": "Connecting...", - "disconnecting": "Disconnecting...", - "disconnected": "Disconnected", "portMapping": "Port {{sourcePort}} → {{endpointHost}}:{{endpointPort}}", - "disconnect": "Disconnect", - "connect": "Connect", - "canceling": "Canceling...", "endpointHostNotFound": "Endpoint host not found", "discord": "Discord", "githubIssue": "GitHub issue", @@ -1376,7 +1342,7 @@ "disk": "Disk", "network": "Network", "uptime": "Uptime", - "loadAverage": "Load Average", + "loadAverage": "Avg: {{avg1}}, {{avg5}}, {{avg15}}", "processes": "Processes", "connections": "Connections", "usage": "Usage", @@ -1392,7 +1358,6 @@ "cpuCores_one": "{{count}} CPU", "cpuCores_other": "{{count}} CPUs", "naCpus": "N/A CPU(s)", - "loadAverage": "Avg: {{avg1}}, {{avg5}}, {{avg15}}", "loadAverageNA": "Avg: N/A", "cpuUsage": "CPU Usage", "memoryUsage": "Memory Usage", @@ -1411,7 +1376,6 @@ "totpRequired": "TOTP Authentication Required", "totpUnavailable": "Server Stats unavailable for TOTP-enabled servers", "load": "Load", - "available": "Available", "editLayout": "Edit Layout", "cancelEdit": "Cancel", "addWidget": "Add Widget", @@ -1550,7 +1514,9 @@ "authenticating": "Authenticating...", "dataLossWarning": "Resetting your password this way will delete all your saved SSH hosts, credentials, and other encrypted data. This action cannot be undone. Only use this if you have forgotten your password and are not logged in.", "authenticationDisabled": "Authentication Disabled", - "authenticationDisabledDesc": "All authentication methods are currently disabled. Please contact your administrator." + "authenticationDisabledDesc": "All authentication methods are currently disabled. Please contact your administrator.", + "passwordResetSuccess": "Password Reset Successful", + "passwordResetSuccessDesc": "Your password has been reset successfully. You can now log in with your new password." }, "errors": { "notFound": "Page not found", @@ -1628,6 +1594,8 @@ "fileColorCodingDesc": "Color-code files by type: folders (red), files (blue), symlinks (green)", "commandAutocomplete": "Command Autocomplete", "commandAutocompleteDesc": "Enable Tab key autocomplete suggestions for terminal commands based on your command history", + "defaultSnippetFoldersCollapsed": "Collapse Snippet Folders by Default", + "defaultSnippetFoldersCollapsedDesc": "When enabled, all snippet folders will be collapsed when you open the snippets tab", "currentPassword": "Current Password", "passwordChangedSuccess": "Password changed successfully! Please log in again.", "failedToChangePassword": "Failed to change password. Please check your current password and try again." @@ -1719,7 +1687,6 @@ "deleteItem": "Delete Item", "createNewFile": "Create New File", "createNewFolder": "Create New Folder", - "deleteItem": "Delete Item", "renameItem": "Rename Item", "clickToSelectFile": "Click to select a file", "noSshHosts": "No SSH Hosts", @@ -1849,6 +1816,172 @@ "ram": "RAM", "notAvailable": "N/A" }, + "rbac": { + "shareHost": "Share Host", + "shareHostTitle": "Share Host Access", + "shareHostDescription": "Grant temporary or permanent access to this host", + "targetUser": "Target User", + "selectUser": "Select a user to share with", + "duration": "Duration", + "durationHours": "Duration (hours)", + "neverExpires": "Never expires", + "permissionLevel": "Permission Level", + "permissionLevels": { + "readonly": "Read-Only", + "readonlyDesc": "Can view only, no command input", + "restricted": "Restricted", + "restrictedDesc": "Blocks dangerous commands (passwd, rm -rf, etc.)", + "monitored": "Monitored", + "monitoredDesc": "Records all commands but doesn't block (Recommended)", + "full": "Full Access", + "fullDesc": "No restrictions (Not recommended)" + }, + "blockedCommands": "Blocked Commands", + "blockedCommandsPlaceholder": "Enter commands to block, e.g., passwd, rm, dd", + "maxSessionDuration": "Max Session Duration (minutes)", + "createTempUser": "Create Temporary User", + "createTempUserDesc": "Creates a restricted user on the server instead of sharing your credentials. Requires sudo access. Most secure option.", + "expiresAt": "Expires At", + "expiresIn": "Expires in {{hours}} hours", + "expired": "Expired", + "grantedBy": "Granted By", + "accessLevel": "Access Level", + "lastAccessed": "Last Accessed", + "accessCount": "Access Count", + "revokeAccess": "Revoke Access", + "confirmRevokeAccess": "Are you sure you want to revoke access for {{username}}?", + "hostSharedSuccessfully": "Host shared successfully with {{username}}", + "hostAccessUpdated": "Host access updated", + "failedToShareHost": "Failed to share host", + "accessRevokedSuccessfully": "Access revoked successfully", + "failedToRevokeAccess": "Failed to revoke access", + "shared": "Shared", + "sharedHosts": "Shared Hosts", + "sharedWithMe": "Shared With Me", + "noSharedHosts": "No hosts shared with you", + "owner": "Owner", + "viewAccessList": "View Access List", + "accessList": "Access List", + "noAccessGranted": "No access has been granted for this host", + "noAccessGrantedMessage": "No users have been granted access to this host yet", + "manageAccessFor": "Manage access for", + "totalAccessRecords": "{{count}} access record(s)", + "neverAccessed": "Never", + "timesAccessed": "{{count}} time(s)", + "daysRemaining": "{{days}} day(s)", + "hoursRemaining": "{{hours}} hour(s)", + "expired": "Expired", + "failedToFetchAccessList": "Failed to fetch access list", + "currentAccess": "Current Access", + "securityWarning": "Security Warning", + "securityWarningMessage": "Sharing credentials gives the user full access to perform any operations on the server, including changing passwords and deleting files. Only share with trusted users.", + "tempUserRecommended": "We recommend enabling 'Create Temporary User' for better security.", + "roleManagement": "Role Management", + "manageRoles": "Manage Roles", + "manageRolesFor": "Manage roles for {{username}}", + "assignRole": "Assign Role", + "removeRole": "Remove Role", + "userRoles": "User Roles", + "permissions": "Permissions", + "systemRole": "System Role", + "customRole": "Custom Role", + "roleAssignedSuccessfully": "Role assigned to {{username}} successfully", + "failedToAssignRole": "Failed to assign role", + "roleRemovedSuccessfully": "Role removed from {{username}} successfully", + "failedToRemoveRole": "Failed to remove role", + "cannotRemoveSystemRole": "Cannot remove system role", + "cannotShareWithSelf": "Cannot share host with yourself", + "noCustomRolesToAssign": "No custom roles available. System roles are auto-assigned.", + "credentialSharingWarning": "Credential Authentication Not Supported for Sharing", + "credentialSharingWarningDescription": "This host uses credential-based authentication. Shared users will not be able to connect because credentials are encrypted per-user and cannot be shared. Please use password or key-based authentication for hosts you intend to share.", + "auditLogs": "Audit Logs", + "viewAuditLogs": "View Audit Logs", + "action": "Action", + "resourceType": "Resource Type", + "resourceName": "Resource Name", + "timestamp": "Timestamp", + "ipAddress": "IP Address", + "userAgent": "User Agent", + "success": "Success", + "failed": "Failed", + "details": "Details", + "noAuditLogs": "No audit logs available", + "sessionRecordings": "Session Recordings", + "viewRecording": "View Recording", + "downloadRecording": "Download Recording", + "dangerousCommand": "Dangerous Command Detected", + "commandBlocked": "Command Blocked", + "terminateSession": "Terminate Session", + "sessionTerminated": "Session terminated by host owner", + "sharedAccessExpired": "Your shared access to this host has expired", + "sharedAccessExpiresIn": "Shared access expires in {{hours}} hours", + "roles": { + "label": "Roles", + "admin": "Administrator", + "user": "User" + }, + "createRole": "Create Role", + "editRole": "Edit Role", + "roleName": "Role Name", + "displayName": "Display Name", + "description": "Description", + "assignRoles": "Assign Roles", + "userRoleAssignment": "User-Role Assignment", + "selectUserPlaceholder": "Select a user", + "searchUsers": "Search users...", + "noUserFound": "No user found", + "currentRoles": "Current Roles", + "noRolesAssigned": "No roles assigned", + "assignNewRole": "Assign New Role", + "selectRolePlaceholder": "Select a role", + "searchRoles": "Search roles...", + "noRoleFound": "No role found", + "assign": "Assign", + "roleCreatedSuccessfully": "Role created successfully", + "roleUpdatedSuccessfully": "Role updated successfully", + "roleDeletedSuccessfully": "Role deleted successfully", + "failedToLoadRoles": "Failed to load roles", + "failedToSaveRole": "Failed to save role", + "failedToDeleteRole": "Failed to delete role", + "roleDisplayNameRequired": "Role display name is required", + "roleNameRequired": "Role name is required", + "roleNameHint": "Use lowercase letters, numbers, underscores, and hyphens only", + "displayNamePlaceholder": "Developer", + "descriptionPlaceholder": "Software developers and engineers", + "confirmDeleteRole": "Delete Role", + "confirmDeleteRoleDescription": "Are you sure you want to delete the role '{{name}}'? This action cannot be undone.", + "confirmRemoveRole": "Remove Role", + "confirmRemoveRoleDescription": "Are you sure you want to remove this role from the user?", + "editRoleDescription": "Update role information", + "createRoleDescription": "Create a new custom role for grouping users", + "assignRolesDescription": "Manage role assignments for users", + "noRoles": "No roles found", + "selectRole": "Select Role", + "type": "Type", + "user": "User", + "role": "Role", + "saveHostFirst": "Save Host First", + "saveHostFirstDescription": "Please save the host before configuring sharing settings.", + "shareWithUser": "Share with User", + "shareWithRole": "Share with Role", + "share": "Share", + "target": "Target", + "expires": "Expires", + "never": "Never", + "noAccessRecords": "No access records found", + "sharedSuccessfully": "Shared successfully", + "failedToShare": "Failed to share", + "confirmRevokeAccessDescription": "Are you sure you want to revoke this access?", + "hours": "hours", + "sharing": "Sharing", + "selectUserAndRole": "Please select both a user and a role", + "view": "View Only", + "viewDesc": "Can view and connect to the host in read-only mode", + "use": "Use", + "useDesc": "Can use the host normally but cannot modify host configuration", + "manage": "Manage", + "manageDesc": "Full control including modifying host configuration and sharing settings" + }, "commandPalette": { "searchPlaceholder": "Search for hosts or quick actions...", "recentActivity": "Recent Activity", diff --git a/src/locales/fr/translation.json b/src/locales/fr/translation.json index 7f111fb8..60bda5bd 100644 --- a/src/locales/fr/translation.json +++ b/src/locales/fr/translation.json @@ -164,7 +164,8 @@ "generateKeyPairNote": "Générez une nouvelle paire de clés SSH directement. Cela remplacera toute clé existante dans le formulaire.", "invalidKey": "Clé invalide", "detectionError": "Erreur de détection", - "unknown": "Inconnu" + "unknown": "Inconnu", + "credentialId": "ID de l'identifiant" }, "dragIndicator": { "error": "Erreur : {{error}}", @@ -385,7 +386,8 @@ "documentation": "Documentation", "retry": "Réessayer", "checking": "Vérification...", - "checkingDatabase": "Vérification de la connexion à la base de données..." + "checkingDatabase": "Vérification de la connexion à la base de données...", + "saving": "Enregistrement..." }, "nav": { "home": "Accueil", @@ -395,7 +397,7 @@ "tunnels": "Tunnels", "fileManager": "Gestionnaire de fichiers", "serverStats": "Statistiques serveur", - "admin": "Admin", + "admin": "Administrateur", "userProfile": "Profil utilisateur", "tools": "Outils", "snippets": "Extraits", @@ -447,7 +449,7 @@ "makeUserAdmin": "Nommer l'utilisateur administrateur", "adding": "Ajout...", "currentAdmins": "Administrateurs actuels", - "adminBadge": "Admin", + "adminBadge": "Administrateur", "removeAdminButton": "Retirer l'administrateur", "general": "Général", "userRegistration": "Inscription utilisateur", @@ -470,7 +472,7 @@ "failedToRemoveAdminStatus": "Échec du retrait du statut d'administrateur", "userDeletedSuccessfully": "Utilisateur {{username}} supprimé avec succès", "failedToDeleteUser": "Échec de la suppression de l'utilisateur", - "overrideUserInfoUrl": "Remplacer l'URL User Info (optionnel)", + "overrideUserInfoUrl": "Remplacer l'URL Utilisateur Info (optionnel)", "failedToFetchSessions": "Échec de la récupération des sessions", "sessionRevokedSuccessfully": "Session révoquée avec succès", "failedToRevokeSession": "Échec de la révocation de la session", @@ -604,7 +606,26 @@ "requiresPasswordLogin": "Nécessite la connexion par mot de passe activée", "passwordLoginDisabledWarning": "La connexion par mot de passe est désactivée. Vérifiez qu'OIDC est correctement configuré sinon vous ne pourrez plus vous connecter à Termix.", "oidcRequiredWarning": "CRITIQUE : la connexion par mot de passe est désactivée. Si vous réinitialisez ou mal configurez OIDC, vous perdrez tout accès à Termix et bloquerez l'instance. Ne continuez que si vous en êtes absolument certain.", - "confirmDisableOIDCWarning": "AVERTISSEMENT : vous êtes sur le point de désactiver OIDC alors que la connexion par mot de passe est désactivée. Cela bloquera votre instance Termix et vous perdrez tout accès. Êtes-vous vraiment sûr de vouloir continuer ?" + "confirmDisableOIDCWarning": "AVERTISSEMENT : vous êtes sur le point de désactiver OIDC alors que la connexion par mot de passe est désactivée. Cela bloquera votre instance Termix et vous perdrez tout accès. Êtes-vous vraiment sûr de vouloir continuer ?", + "accountsLinkedSuccessfully": "Le compte OIDC {{oidcUsername}} a été lié à {{targetUsername}}", + "failedToLinkAccounts": "Impossible de lier les comptes", + "failedToUnlinkOIDC": "Impossible de délier OIDC", + "linkAccountsButton": "Lier les comptes", + "linkOIDCActionAddCapability": "Ajouter la capacité de connexion OIDC au compte mot de passe cible", + "linkOIDCActionDeleteUser": "Supprimer le compte utilisateur OIDC et toutes ses données", + "linkOIDCActionDualAuth": "Autoriser le compte mot de passe à se connecter avec le mot de passe et OIDC", + "linkOIDCDialogDescription": "Lier {{username}} (utilisateur OIDC) à un compte mot de passe existant. Cela activera la double authentification pour le compte mot de passe.", + "linkOIDCDialogTitle": "Lier Compte OIDC au Compte Mot de Passe", + "linkOIDCWarningTitle": "Avertissement : Les données de l'utilisateur OIDC seront supprimées", + "linkTargetUsernameLabel": "Nom d'utilisateur du compte mot de passe cible", + "linkTargetUsernamePlaceholder": "Entrer le nom d'utilisateur du compte mot de passe", + "linkTargetUsernameRequired": "Le nom d'utilisateur cible est requis", + "linkToPasswordAccount": "Lier au Compte Mot de Passe", + "linkingAccounts": "Liaison en cours...", + "unlinkOIDCDescription": "Retirer l'authentification OIDC de {{username}} ? L'utilisateur ne pourra se connecter qu'avec nom d'utilisateur/mot de passe après cela.", + "unlinkOIDCSuccess": "OIDC délié de {{username}}", + "unlinkOIDCTitle": "Délier l'Authentification OIDC", + "failedToUpdatePasswordLoginStatus": "Impossible de mettre à jour le statut de connexion par mot de passe" }, "hosts": { "title": "Gestionnaire d'hôtes", @@ -720,7 +741,7 @@ "updateKey": "Mettre à jour la clé", "existingKey": "Clé existante (cliquez pour modifier)", "existingCredential": "Identifiant existant (cliquez pour modifier)", - "addTagsSpaceToAdd": "ajouter des labels (espace pour valider)", + "addTagsSpaceToAdd": "Ajouter des labels (espace pour valider)", "terminalBadge": "Terminal", "tunnelBadge": "Tunnel", "fileManagerBadge": "Gestionnaire de fichiers", @@ -790,7 +811,88 @@ "searchServers": "Rechercher des serveurs...", "noServerFound": "Aucun serveur trouvé", "jumpHostsOrder": "Les connexions seront établies dans l'ordre : Serveur de rebond 1 → Serveur de rebond 2 → ... → Serveur cible", - "advancedAuthSettings": "Paramètres d'authentification avancés" + "advancedAuthSettings": "Paramètres d'authentification avancés", + "addQuickAction": "Ajouter une action rapide", + "adjustFontSize": "Ajuster la taille de la police du terminal", + "adjustLetterSpacing": "Ajuster l'espacement des lettres", + "adjustLineHeight": "Ajuster l'espacement des lignes", + "advanced": "Avancé", + "allHostsInFolderDeleted": "{{count}} hôtes supprimés du dossier \"{{folder}}\" avec succès", + "appearance": "Apparence", + "backspaceMode": "Mode retour arrière (Backspace)", + "backspaceModeControlH": "Control-H (^H)", + "backspaceModeDesc": "Comportement de la touche retour arrière pour la compatibilité", + "backspaceModeNormal": "Normal (DEL)", + "behavior": "Comportement", + "bellStyle": "Style de la cloche (Bell)", + "bellStyleBoth": "Les deux", + "bellStyleDesc": "Comment gérer la cloche du terminal (caractère BEL, \\x07). Les programmes déclenchent ceci lors de la fin de tâches, d'erreurs ou pour les notifications. \"Son\" joue un bip audio, \"Visuel\" fait clignoter l'écran brièvement, \"Les deux\" fait les deux, \"Aucun\" désactive les alertes.", + "bellStyleNone": "Aucun", + "bellStyleSound": "Son", + "bellStyleVisual": "Visuel", + "chooseColorTheme": "Choisir un thème de couleur pour le terminal", + "chooseCursorAppearance": "Choisir l'apparence du curseur", + "confirmDeleteAllHostsInFolder": "Êtes-vous sûr de vouloir supprimer les {{count}} hôtes du dossier \"{{folder}}\" ? Cette action ne peut pas être annulée.", + "cursorBlink": "Clignotement du curseur", + "cursorStyle": "Style du curseur", + "cursorStyleBar": "Barre", + "cursorStyleBlock": "Bloc", + "cursorStyleUnderline": "Souligné", + "deleteAllHostsInFolder": "Supprimer tous les hôtes du dossier", + "editFolderAppearance": "Modifier l'apparence du dossier", + "editFolderAppearanceDesc": "Personnalisez la couleur et l'icône du dossier", + "enableCursorBlink": "Activer l'animation de clignotement du curseur", + "failedToDeleteHostsInFolder": "Impossible de supprimer les hôtes du dossier", + "failedToUpdateFolderAppearance": "Impossible de mettre à jour l'apparence du dossier", + "fastScrollModifier": "Touche de défilement rapide", + "fastScrollModifierDesc": "Touche modificatrice pour le défilement rapide", + "fastScrollSensitivity": "Sensibilité du défilement rapide", + "fastScrollSensitivityDesc": "Multiplicateur de vitesse de défilement lorsque la touche est maintenue", + "fastScrollSensitivityValue": "Sensibilité du défilement rapide : {{value}}", + "folderAppearanceUpdated": "Apparence du dossier mise à jour avec succès", + "folderColor": "Couleur du dossier", + "folderIcon": "Icône du dossier", + "fontFamily": "Famille de police", + "fontSize": "Taille de police", + "fontSizeValue": "Taille de police : {{value}}px", + "letterSpacing": "Espacement des lettres", + "letterSpacingValue": "Espacement des lettres : {{value}}px", + "lineHeight": "Hauteur de ligne", + "lineHeightValue": "Hauteur de ligne : {{value}}", + "minimumContrastRatio": "Ratio de contraste minimum", + "minimumContrastRatioDesc": "Ajuster automatiquement les couleurs pour une meilleure lisibilité", + "minimumContrastRatioValue": "Ratio de contraste minimum : {{value}}", + "modifierAlt": "Alt", + "modifierCtrl": "Ctrl", + "modifierShift": "Shift", + "noSnippetFound": "Aucun extrait trouvé", + "preview": "Aperçu", + "quickActionName": "Nom de l'action", + "quickActions": "Actions rapides", + "quickActionsDescription": "Les actions rapides vous permettent de créer des boutons personnalisés qui exécutent des extraits SSH sur ce serveur. Ces boutons apparaîtront en haut de la page Statistiques Serveur pour un accès rapide.", + "quickActionsList": "Liste des actions rapides", + "quickActionsOrder": "Les boutons d'action rapide apparaîtront dans l'ordre indiqué ci-dessus sur la page Statistiques Serveur", + "rightClickSelectsWord": "Clic droit sélectionne le mot", + "rightClickSelectsWordDesc": "Le clic droit sélectionne le mot sous le curseur", + "scrollbackBuffer": "Tampon de défilement", + "scrollbackBufferDesc": "Nombre de lignes à conserver dans l'historique de défilement", + "scrollbackBufferValue": "Tampon de défilement : {{value}} lignes", + "searchSnippets": "Rechercher des extraits...", + "selectBackspaceMode": "Sélectionner le mode retour arrière", + "selectBellStyle": "Sélectionner le style de cloche", + "selectCursorStyle": "Sélectionner le style de curseur", + "selectFont": "Sélectionner la police", + "selectFontDesc": "Sélectionner la police à utiliser dans le terminal", + "selectModifier": "Sélectionner la touche modificatrice", + "selectSnippet": "Sélectionner un extrait", + "selectTheme": "Sélectionner un thème", + "snippetNone": "Aucun", + "sshAgentForwarding": "Transfert d'agent SSH", + "sshAgentForwardingDesc": "Transférer l'agent d'authentification SSH à l'hôte distant", + "startupSnippet": "Extrait au démarrage", + "terminalCustomization": "Personnalisation du terminal", + "theme": "Thème", + "themePreview": "Aperçu du thème" }, "terminal": { "title": "Terminal", @@ -828,7 +930,11 @@ "totpRequired": "Authentification à deux facteurs requise", "totpCodeLabel": "Code de vérification", "totpPlaceholder": "000000", - "totpVerify": "Vérifier" + "totpVerify": "Vérifier", + "sudoPasswordPopupTitle": "Insérer le mot de passe ?", + "sudoPasswordPopupHint": "Appuyez sur Entrée pour insérer, Échap pour annuler", + "sudoPasswordPopupConfirm": "Insérer", + "sudoPasswordPopupDismiss": "Annuler" }, "fileManager": { "title": "Gestionnaire de fichiers", @@ -943,7 +1049,7 @@ "internalServerError": "Une erreur interne du serveur est survenue", "serverError": "Erreur serveur", "error": "Erreur", - "requestFailed": "Réquête échouée avec le code", + "requestFailed": "Requête échouée avec le code", "unknownFileError": "Erreur de fichier inconnue", "cannotReadFile": "Impossible de lire le fichier", "noSshSessionId": "Aucun ID de session SSH", @@ -1100,7 +1206,35 @@ "sshConnectionFailed": "La connexion SSH a échoué. Vérifiez votre connexion à {{name}} ({{ip}}:{{port}})", "loadFileFailed": "Échec du chargement du fichier : {{error}}", "connectedSuccessfully": "Connexion réussie", - "totpVerificationFailed": "Échec de la vérification TOTP" + "totpVerificationFailed": "Échec de la vérification TOTP", + "andMoreFiles": "et {{count}} plus...", + "archiveExtractedSuccessfully": "{{name}} extrait avec succès", + "archiveName": "Nom de l'archive", + "changePermissions": "Modifier les permissions", + "changePermissionsDesc": "Modifier les permissions du fichier pour", + "compress": "Compresser", + "compressFailed": "Échec de la compression", + "compressFile": "Compresser le fichier", + "compressFiles": "Compresser les fichiers", + "compressFilesDesc": "Compresser {{count}} éléments dans une archive", + "compressingFiles": "Compression de {{count}} éléments dans {{name}}...", + "compressionFormat": "Format de compression", + "currentPermissions": "Permissions actuelles", + "enterArchiveName": "Entrer le nom de l'archive...", + "execute": "Exécuter", + "extractArchive": "Extraire l'archive", + "extractFailed": "Échec de l'extraction", + "extractingArchive": "Extraction de {{name}}...", + "failedToChangePermissions": "Impossible de modifier les permissions", + "filesCompressedSuccessfully": "{{name}} créé avec succès", + "group": "Groupe", + "newPermissions": "Nouvelles permissions", + "others": "Autres", + "owner": "Propriétaire", + "permissionsChangedSuccessfully": "Permissions modifiées avec succès", + "read": "Lecture", + "selectedFiles": "Fichiers sélectionnés", + "write": "Écriture" }, "tunnels": { "title": "Tunnels SSH", @@ -1211,7 +1345,20 @@ "noInterfacesFound": "Aucune interface trouvée", "totalProcesses": "Processus totaux", "running": "En cours d'exécution", - "noProcessesFound": "Aucun processus trouvé" + "noProcessesFound": "Aucun processus trouvé", + "executeQuickAction": "Exécuter {{name}}", + "executingQuickAction": "Exécution de {{name}}...", + "from": "de", + "loginStats": "Statistiques de connexion SSH", + "noRecentLoginData": "Aucune donnée de connexion récente", + "quickActionError": "Impossible d'exécuter {{name}}", + "quickActionFailed": "{{name}} a échoué", + "quickActionSuccess": "{{name}} terminé avec succès", + "quickActions": "Actions rapides", + "recentFailedAttempts": "Tentatives échouées récentes", + "recentSuccessfulLogins": "Connexions réussies récentes", + "totalLogins": "Total des connexions", + "uniqueIPs": "IP uniques" }, "auth": { "loginTitle": "Connexion à Termix", @@ -1314,7 +1461,14 @@ "authenticating": "Authentification...", "dataLossWarning": "Réinitialiser votre mot de passe de cette manière supprimera tous vos hôtes, identifiants et autres données chiffrées. Action irréversible. À utiliser uniquement si vous avez oublié votre mot de passe et n'êtes pas connecté.", "authenticationDisabled": "Authentification désactivée", - "authenticationDisabledDesc": "Toutes les méthodes d'authentification sont actuellement désactivées. Contactez votre administrateur." + "authenticationDisabledDesc": "Toutes les méthodes d'authentification sont actuellement désactivées. Contactez votre administrateur.", + "continueExternal": "Continuer avec un fournisseur externe", + "createAccount": "Créer votre compte TERMIX", + "description": "Gestion de connexions SSH sécurisée, puissante et intuitive", + "tagline": "GESTIONNAIRE DE SERVEURS SSH", + "welcomeBack": "Bon retour sur TERMIX", + "passwordResetSuccess": "Réinitialisation du mot de passe réussie", + "passwordResetSuccessDesc": "Votre mot de passe a été réinitialisé avec succès. Vous pouvez maintenant vous connecter avec votre nouveau mot de passe." }, "errors": { "notFound": "Page introuvable", @@ -1391,9 +1545,12 @@ "fileColorCodingDesc": "Codage couleur des fichiers par type : dossiers (rouge), fichiers (bleu), liens symboliques (vert)", "commandAutocomplete": "Autocomplétion des commandes", "commandAutocompleteDesc": "Activer les suggestions d'autocomplétion avec la touche Tab pour les commandes du terminal basées sur votre historique", + "defaultSnippetFoldersCollapsed": "Réduire les dossiers de snippets par défaut", + "defaultSnippetFoldersCollapsedDesc": "Lorsque cette option est activée, tous les dossiers de snippets seront réduits à l'ouverture de l'onglet snippets", "currentPassword": "Mot de passe actuel", "passwordChangedSuccess": "Mot de passe modifié avec succès ! Veuillez vous reconnecter.", - "failedToChangePassword": "Échec de la modification du mot de passe. Vérifiez votre mot de passe actuel et réessayez." + "failedToChangePassword": "Échec de la modification du mot de passe. Vérifiez votre mot de passe actuel et réessayez.", + "externalAndLocal": "Double Auth" }, "user": { "failedToLoadVersionInfo": "Échec du chargement des informations de version" @@ -1405,11 +1562,11 @@ "maxRetries": "3", "retryInterval": "10", "language": "Langue", - "username": "nom d'utilisateur", - "hostname": "nom d'hôte", - "folder": "dossier", - "password": "mot de passe", - "keyPassword": "mot de passe de la clé", + "username": "Nom d'utilisateur", + "hostname": "Nom d'hôte", + "folder": "Dossier", + "password": "Mot de passe", + "keyPassword": "Mot de passe de la clé", "pastePrivateKey": "Collez votre clé privée ici...", "pastePublicKey": "Collez votre clé publique ici...", "credentialName": "Mon serveur SSH", @@ -1605,5 +1762,28 @@ "cpu": "Processeur (CPU)", "ram": "Mémoire (RAM)", "notAvailable": "N/D" + }, + "commandPalette": { + "addCredential": "Ajouter Identifiant", + "addHost": "Ajouter Hôte", + "adminSettings": "Paramètres Administrateur", + "close": "Fermer", + "discord": "Discord", + "donate": "Faire un don", + "edit": "Modifier", + "github": "GitHub", + "hostManager": "Gestionnaire d'hôtes", + "hosts": "Hôtes", + "links": "Liens", + "navigation": "Navigation", + "openFileManager": "Ouvrir Gestionnaire de fichiers", + "openServerDetails": "Ouvrir Détails Serveur", + "press": "Appuyer", + "recentActivity": "Activité Récente", + "searchPlaceholder": "Rechercher hôtes ou actions rapides...", + "support": "Support", + "toToggle": "pour basculer", + "updateLog": "Journal de mise à jour", + "userProfile": "Profil utilisateur" } -} +} \ No newline at end of file diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json new file mode 100644 index 00000000..1924b0f2 --- /dev/null +++ b/src/locales/it/translation.json @@ -0,0 +1,3470 @@ +{ + "credentials": { + "credentialsViewer": "Visualizzatore Credenziali", + "manageYourSSHCredentials": "Gestisci le tue credenziali SSH in modo sicuro", + "addCredential": "Aggiungi Credenziale", + "createCredential": "Crea Credenziale", + "editCredential": "Modifica Credenziale", + "viewCredential": "Visualizza Credenziale", + "duplicateCredential": "Duplica Credenziale", + "deleteCredential": "Elimina Credenziale", + "updateCredential": "Aggiorna Credenziale", + "credentialName": "Nome Credenziale", + "credentialDescription": "Descrizione", + "username": "Nome Utente", + "searchCredentials": "Cerca credenziali...", + "selectFolder": "Seleziona Cartella", + "selectAuthType": "Seleziona Tipo Autenticazione", + "allFolders": "Tutte le Cartelle", + "allAuthTypes": "Tutti i Tipi di Autenticazione", + "uncategorized": "Non Categorizzata", + "totalCredentials": "Totale", + "keyBased": "Basata su Chiave", + "passwordBased": "Basata su Password", + "folders": "Cartelle", + "noCredentialsMatchFilters": "Nessuna credenziale corrisponde ai filtri", + "noCredentialsYet": "Nessuna credenziale creata", + "createFirstCredential": "Crea la tua prima credenziale", + "failedToFetchCredentials": "Impossibile recuperare le credenziali", + "credentialDeletedSuccessfully": "Credenziale eliminata con successo", + "failedToDeleteCredential": "Impossibile eliminare la credenziale", + "confirmDeleteCredential": "Sei sicuro di voler eliminare la credenziale \"{{name}}\"?", + "credentialCreatedSuccessfully": "Credenziale creata con successo", + "credentialUpdatedSuccessfully": "Credenziale aggiornata con successo", + "failedToSaveCredential": "Impossibile salvare la credenziale", + "failedToFetchCredentialDetails": "Impossibile recuperare i dettagli della credenziale", + "failedToFetchHostsUsing": "Impossibile recuperare gli host che usano questa credenziale", + "loadingCredentials": "Caricamento credenziali...", + "retry": "Riprova", + "noCredentials": "Nessuna Credenziale", + "noCredentialsMessage": "Non hai ancora aggiunto credenziali. Clicca \"Aggiungi Credenziale\" per iniziare.", + "sshCredentials": "Credenziali SSH", + "credentialsCount": "{{count}} credenziali", + "refresh": "Aggiorna", + "passwordRequired": "La password è obbligatoria", + "sshKeyRequired": "La chiave SSH è obbligatoria", + "credentialAddedSuccessfully": "Credenziale \"{{name}}\" aggiunta con successo", + "general": "Generale", + "description": "Descrizione", + "folder": "Cartella", + "tags": "Tag", + "addTagsSpaceToAdd": "Aggiungi tag (premi spazio per aggiungere)", + "password": "Password", + "key": "Chiave", + "sshPrivateKey": "Chiave Privata SSH", + "upload": "Carica", + "updateKey": "Aggiorna Chiave", + "keyPassword": "Password Chiave", + "credentialsViewer": "Vèualizzaae Credenziali", + "manageYourSSHCredentials": "Gestèci le tue credenziali SSH in mofare sicuro", + "addCredential": "Aggiungi Credenziale", + "createCredential": "Crea Credenziale", + "editCredential": "Modifica Credenziale", + "viewCredential": "Vèualizza Credenziale", + "duplicateCredential": "Duplica Credenziale", + "deleteCredential": "Elimina Credenziale", + "updateCredential": "Aggèua Credenziale", + "credentialName": "Nome Credenziale", + "credentialDescription": "Descrizèue", + "username": "Nome Utente", + "searchCredentials": "Cerca credenziali...", + "selectFolder": "Selezèua Cartella", + "selectAuthType": "Selezèua Tipo Autenticazèue", + "allFolders": "Tutte le Cartelle", + "allAuthTypes": "Tutti i Tipi di Autenticazèue", + "uncategorized": "Nsu Caegoizzaa", + "totalCredentials": "Totale", + "keyBased": "Bcomeaa su Chiave", + "passwordBased": "Bcomeaa su Pcomeswod", + "folders": "Cartelle", + "noCredentialsMatchFilters": "Nessuna credenziale corèpsude ai filtri", + "noCredentialsYet": "Nessuna credenziale creaa", + "createFirstCredential": "Crea la tua prima credenziale", + "failedToFetchCredentials": "Impossibile recuperssuo le credenziali", + "credentialDeletedSuccessfully": "Credenziale eliminaa csu successoo", + "failedToDeleteCredential": "Impossibile eliminssuo la credenziale", + "confirmDeleteCredential": "Sei sicuro di voler eliminssuo la credenziale \"{{nome}}\"?", + "credentialCreatedSuccessfully": "Credenziale creaa csu successoo", + "credentialUpdatedSuccessfully": "Credenziale aggèuaa csu successoo", + "failedToSaveCredential": "Impossibile salvssuo la credenziale", + "failedToFetchCredentialDetails": "Impossibile recuperssuo i dettagli della credenziale", + "failedToFetchHostsUsing": "Impossibile recuperssuo gli host che usano questa credenziale", + "loadingCredentials": "Caricamena credenziali...", + "retry": "Riprova", + "noCredentials": "Nessuna Credenziale", + "noCredentialsMessage": "Nsu hai ancoa aggiuna credenziali. Clicca \"Aggiungi Credenziale\" per inizèsuo.", + "sshCredentials": "Credenziali SSH", + "credentialsCount": "{{count}} credenziali", + "refresh": "Aggèua", + "passwordRequired": "La pcomeswod è obbligaaia", + "sshKeyRequired": "La chiave SSH è obbligaaia", + "credentialAddedSuccessfully": "Credenziale \"{{nome}}\" aggiunta csu successoo", + "general": "Generalee", + "description": "Descrizèue", + "folder": "Cartella", + "tags": "Tag", + "addTagsSpaceToAdd": "Aggiungi tag (premi spazio per aggiungere)", + "password": "Pcomeswod", + "key": "Chiave", + "sshPrivateKey": "Chiave Privaa SSH", + "upload": "Carica", + "updateKey": "Aggèua Chiave", + "keyPassword": "Pcomeswod Chiave", + "keyType": "Tipo Chiave", + "keyTypeRSA": "RSA", + "keyTypeECDSA": "ECDSA", + "keyTypeEd25519": "Ed25519", + "basicInfo": "Info Base", + "authentication": "Autenticazione", + "organization": "Organizzazione", + "basicInformation": "Informazioni di Base", + "basicInformationDescription": "Inserisci le informazioni di base per questa credenziale", + "authenticationMethod": "Metodo di Autenticazione", + "authenticationMethodDescription": "Scegli come autenticarti con i server SSH", + "organizationDescription": "Organizza le tue credenziali con cartelle e tag", + "enterCredentialName": "Inserisci nome credenziale", + "enterCredentialDescription": "Inserisci descrizione (opzionale)", + "enterUsername": "Inserisci nome utente", + "nameIsRequired": "Il nome della credenziale è obbligatorio", + "usernameIsRequired": "Il nome utente è obbligatorio", + "authenticationType": "Tipo di Autenticazione", + "passwordAuthDescription": "Usa autenticazione con password", + "sshKeyAuthDescription": "Usa autenticazione con chiave SSH", + "passwordIsRequired": "La password è obbligatoria", + "sshKeyIsRequired": "La chiave SSH è obbligatoria", + "sshKeyType": "Tipo Chiave SSH", + "privateKey": "Chiave Privata", + "enterPassword": "Inserisci password", + "enterPrivateKey": "Inserisci chiave privata", + "keyPassphrase": "Passphrase Chiave", + "enterKeyPassphrase": "Inserisci passphrase chiave (opzionale)", + "keyPassphraseOptional": "Opzionale: lascia vuoto se la chiave non ha passphrase", + "leaveEmptyToKeepCurrent": "Lascia vuoto per mantenere il valore attuale", + "uploadKeyFile": "Carica File Chiave", + "generateKeyPairButton": "Genera Coppia di Chiavi", + "generateKeyPair": "Genera Coppia di Chiavi", + "generateKeyPairDescription": "Genera una nuova coppia di chiavi SSH. Se vuoi proteggere la chiave con una passphrase, inseriscila prima nel campo Password Chiave.", + "deploySSHKey": "Distribuisci Chiave SSH", + "deploySSHKeyDescription": "Distribuisci la chiave pubblica sul server di destinazione", + "sourceCredential": "Credenziale Sorgente", + "targetHost": "Host di Destinazione", + "deploymentProcess": "Processo di Distribuzione", + "deploymentProcessDescription": "Questo aggiungerà in modo sicuro la chiave pubblica al file ~/.ssh/authorized_keys dell'host di destinazione senza sovrascrivere le chiavi esistenti. L'operazione è reversibile.", + "chooseHostToDeploy": "Scegli un host su cui distribuire...", + "deploying": "Distribuzione in corso...", + "name": "Nome", + "noHostsAvailable": "Nessun host disponibile", + "noHostsMatchSearch": "Nessun host corrisponde alla ricerca", + "sshKeyGenerationNotImplemented": "Funzione generazione chiave SSH in arrivo", + "connectionTestingNotImplemented": "Funzione test connessione in arrivo", + "testConnection": "Testa Connessione", + "selectOrCreateFolder": "Seleziona o crea cartella", + "noFolder": "Nessuna cartella", + "orCreateNewFolder": "O crea nuova cartella", + "addTag": "Aggiungi tag", + "saving": "Salvataggio...", + "credentialId": "ID Credenziale", + "overview": "Panoramica", + "security": "Sicurezza", + "usage": "Utilizzo", + "securityDetails": "Dettagli Sicurezza", + "securityDetailsDescription": "Visualizza informazioni credenziali criptate", + "credentialSecured": "Credenziale Protetta", + "credentialSecuredDescription": "Tutti i dati sensibili sono criptati con AES-256", + "passwordAuthentication": "Autenticazione Password", + "keyAuthentication": "Autenticazione Chiave", + "securityReminder": "Promemoria Sicurezza", + "securityReminderText": "Non condividere mai le tue credenziali. Tutti i dati sono criptati a riposo.", + "hostsUsingCredential": "Host che Usano Questa Credenziale", + "noHostsUsingCredential": "Nessun host sta usando questa credenziale", + "timesUsed": "Volte Utilizzata", + "lastUsed": "Ultimo Utilizzo", + "connectedHosts": "Host Connessi", + "created": "Creata", + "lastModified": "Ultima Modifica", + "usageStatistics": "Statistiche di Utilizzo", + "copiedToClipboard": "{{field}} copiato negli appunti", + "failedToCopy": "Impossibile copiare negli appunti", + "sshKey": "Chiave SSH", + "createCredentialDescription": "Crea una nuova credenziale SSH per accesso sicuro", + "editCredentialDescription": "Aggiorna le informazioni della credenziale", + "listView": "Lista", + "folderView": "Cartelle", + "unknownCredential": "Sconosciuta", + "confirmRemoveFromFolder": "Sei sicuro di voler rimuovere \"{{name}}\" dalla cartella \"{{folder}}\"? La credenziale sarà spostata in \"Non Categorizzata\".", + "removedFromFolder": "Credenziale \"{{name}}\" rimossa dalla cartella con successo", + "failedToRemoveFromFolder": "Impossibile rimuovere la credenziale dalla cartella", + "folderRenamed": "Cartella \"{{oldName}}\" rinominata in \"{{newName}}\" con successo", + "failedToRenameFolder": "Impossibile rinominare la cartella", + "movedToFolder": "Credenziale \"{{name}}\" spostata in \"{{folder}}\" con successo", + "failedToMoveToFolder": "Impossibile spostare la credenziale nella cartella", + "sshPublicKey": "Chiave Pubblica SSH", + "publicKeyNote": "La chiave pubblica è opzionale ma raccomandata per la validazione", + "publicKeyUploaded": "Chiave Pubblica Caricata", + "uploadPublicKey": "Carica Chiave Pubblica", + "uploadPrivateKeyFile": "Carica File Chiave Privata", + "uploadPublicKeyFile": "Carica File Chiave Pubblica", + "privateKeyRequiredForGeneration": "La chiave privata è necessaria per generare la chiave pubblica", + "failedToGeneratePublicKey": "Impossibile generare la chiave pubblica", + "generatePublicKey": "Genera da Chiave Privata", + "publicKeyGeneratedSuccessfully": "Chiave pubblica generata con successo", + "detectedKeyType": "Tipo chiave rilevato", + "detectingKeyType": "rilevamento...", + "optional": "Opzionale", + "basicInfo": "Info Bcomee", + "authentication": "Autenticazèue", + "organization": "Organizzazèue", + "basicInformation": "Inpermazèui Bcomee", + "basicInformationDescription": "Inserèci le inpermazèui bcomee per questa credenziale", + "authenticationMethod": "Meafare di Autenticazèue", + "authenticationMethodDescription": "Scegli come autenticarti csu i server SSH", + "organizationDescription": "Organizza le tue credenziali csu cartelle e tag", + "enterCredentialName": "Inserèci nome credenziale", + "enterCredentialDescription": "Inserèci descrizèue (opzèuale)", + "enterUsername": "Inserèci nome utente", + "nameIsRequired": "Il nome della credenziale è obbligaaio", + "usernameIsRequired": "Il nome utente è obbligaaio", + "authenticationType": "Tipo di Autenticazèue", + "passwordAuthDescription": "Usa autenticazèue csu pcomeswod", + "sshKeyAuthDescription": "Usa autenticazèue csu chiave SSH", + "passwordIsRequired": "La pcomeswod è obbligaaia", + "sshKeyIsRequired": "La chiave SSH è obbligaaia", + "sshKeyType": "Tipo Chiave SSH", + "privateKey": "Chiave Privaa", + "enterPassword": "Inserèci pcomeswod", + "enterPrivateKey": "Inserèci chiave privaa", + "keyPassphrase": "Pcomesphrcomee Chiave", + "enterKeyPassphrase": "Inserèci pcomesphrcomee chiave (opzèuale)", + "keyPassphraseOptional": "Opzèuale: lcomecia vuoa se la chiave nsu ha pcomesphrcomee", + "leaveEmptyToKeepCurrent": "Lcomecia vuoa per mantenere il valoe atuale", + "uploadKeyFile": "Carica File Chiave", + "generateKeyPairButton": "Genera Coppia di Chiavi", + "generateKeyPair": "Genera Coppia di Chiavi", + "generateKeyPairDescription": "Genera una nuova coppia di chiavi SSH. Se vuoi proteggere la chiave csu una pcomesphrcomee, inserècila prima nel campo Pcomeswod Chiave.", + "deploySSHKey": "Dètribuèci Chiave SSH", + "deploySSHKeyDescription": "Dètribuèci la chiave pubblica sul server di destinazèue", + "sourceCredential": "Credenziale Sogente", + "targetHost": "Host di Destinazèue", + "deploymentProcess": "Processo di Dètribuzèue", + "deploymentProcessDescription": "Quesa aggiungerà in mofare sicuro la chiave pubblica al file ~/.ssh/authoized_chiavi dell'host di destinazèue senza sovrcomecrivere le chiavi esètenti. L'operazèue è reversibile.", + "chooseHostToDeploy": "Scegli un host su cui dètribuire...", + "deploying": "Dètribuzèue in coso...", + "name": "Nome", + "noHostsAvailable": "Nessun host dèpsuibile", + "noHostsMatchSearch": "Nessun host corèpsude tuttia ricerca", + "sshKeyGenerationNotImplemented": "Funzèue generazèue chiave SSH in arrivo", + "connectionTestingNotImplemented": "Funzèue test csunessèue in arrivo", + "testConnection": "Testa Csunessèue", + "selectOrCreateFolder": "Selezèua o crea cartella", + "noFolder": "Nessuna cartella", + "orCreateNewFolder": "O crea nuova cartella", + "addTag": "Aggiungi tag", + "saving": "Salvaaggio...", + "credentialId": "ID Credenziale", + "overview": "Panoamica", + "security": "Sicurezza", + "usage": "Utilizzo", + "securityDetails": "Dettagli Sicurezza", + "securityDetailsDescription": "Vèualizza inpermazèui credenziali criptae", + "credentialSecured": "Credenziale Protetta", + "credentialSecuredDescription": "Tutti i dai sensibili ssuo criptai csu AES-256", + "passwordAuthentication": "Autenticazèue Pcomeswod", + "keyAuthentication": "Autenticazèue Chiave", + "securityReminder": "Promemoia Sicurezza", + "securityReminderText": "Nsu csudividere mai le tue credenziali. Tutti i dai ssuo criptai a riposo.", + "hostsUsingCredential": "Host che Usano Questa Credenziale", + "noHostsUsingCredential": "Nessun host sta useo questa credenziale", + "timesUsed": "Volte Utilizzaa", + "lastUsed": "Ultimo Utilizzo", + "connectedHosts": "Host Csunessi", + "created": "Creaa", + "lastModified": "Ultima Modifica", + "usageStatistics": "Staètiche di Utilizzo", + "copiedToClipboard": "{{field}} copiaa negli appunti", + "failedToCopy": "Impossibile copèsuo negli appunti", + "sshKey": "Chiave SSH", + "createCredentialDescription": "Crea una nuova credenziale SSH per accesso sicuro", + "editCredentialDescription": "Aggèua le inpermazèui della credenziale", + "listView": "Lèta", + "folderView": "Cartelle", + "unknownCredential": "Scsuosciua", + "confirmRemoveFromFolder": "Sei sicuro di voler rimuovere \"{{nome}}\" dtuttia cartella \"{{cartella}}\"? La credenziale sarà spostaa in \"Nsu Caegoizzaa\".", + "removedFromFolder": "Credenziale \"{{nome}}\" rimossa dtuttia cartella csu successoo", + "failedToRemoveFromFolder": "Impossibile rimuovere la credenziale dtuttia cartella", + "folderRenamed": "Cartella \"{{oldNome}}\" rinominaa in \"{{newNome}}\" csu successoo", + "failedToRenameFolder": "Impossibile rinominssuo la cartella", + "movedToFolder": "Credenziale \"{{nome}}\" spostaa in \"{{cartella}}\" csu successoo", + "failedToMoveToFolder": "Impossibile spostssuo la credenziale nella cartella", + "sshPublicKey": "Chiave Pubblica SSH", + "publicKeyNote": "La chiave pubblica è opzèuale ma raccomeaa per la validazèue", + "publicKeyUploaded": "Chiave Pubblica Caricaa", + "uploadPublicKey": "Carica Chiave Pubblica", + "uploadPrivateKeyFile": "Carica File Chiave Privaa", + "uploadPublicKeyFile": "Carica File Chiave Pubblica", + "privateKeyRequiredForGeneration": "La chiave privaa è necessaria per generssuo la chiave pubblica", + "failedToGeneratePublicKey": "Impossibile generssuo la chiave pubblica", + "generatePublicKey": "Genera da Chiave Privaa", + "publicKeyGeneratedSuccessfully": "Chiave pubblica generaa csu successoo", + "detectedKeyType": "Tipo chiave rilevaa", + "detectingKeyType": "rilevamena...", + "optional": "Opzèuale", + "generateKeyPairNew": "Genera Nuova Coppia di Chiavi", + "generateEd25519": "Genera Ed25519", + "generateECDSA": "Genera ECDSA", + "generateRSA": "Genera RSA", + "keyPairGeneratedSuccessfully": "Coppia di chiavi {{keyType}} generata con successo", + "failedToGenerateKeyPair": "Impossibile generare la coppia di chiavi", + "generateKeyPairNote": "Genera una nuova coppia di chiavi SSH direttamente. Questo sostituirà le chiavi esistenti nel modulo.", + "invalidKey": "Chiave Non Valida", + "detectionError": "Errore di Rilevamento", + "unknown": "Sconosciuto" + }, + "dragIndicator": { + "error": "Errore: {{error}}", + "dragging": "Trascinando {{fileName}}", + "preparing": "Preparazione {{fileName}}", + "readySingle": "Pronto per scaricare {{fileName}}", + "readyMultiple": "Pronto per scaricare {{count}} file", + "batchDrag": "Trascina {{count}} file sul desktop", + "dragToDesktop": "Trascina sul desktop", + "canDragAnywhere": "Puoi trascinare i file ovunque sul desktop" + "keyPairGeneratedSuccessfully": "Coppia di chiavi {{chiaveTipo}} generaa csu successoo", + "failedToGenerateKeyPair": "Impossibile generssuo la coppia di chiavi", + "generateKeyPairNote": "Genera una nuova coppia di chiavi SSH direttamente. Quesa sostituirà le chiavi esètenti nel modulo.", + "invalidKey": "Chiave Nsu Valida", + "detectionError": "Erroee di Rilevamena", + "unknown": "Scsuosciua" + }, + "dragIndicator": { + "error": "Erroee: {{erroe}}", + "dragging": "Trcomecinomena {{fileNome}}", + "preparing": "Preparazèue {{fileNome}}", + "readySingle": "Prsua per scaricssuo {{fileNome}}", + "readyMultiple": "Prsua per scaricssuo {{count}} file", + "batchDrag": "Trcomecina {{count}} file sul deskap", + "dragToDesktop": "Trcomecina sul deskap", + "canDragAnywhere": "Puoi trcomecinssuo i file ovunque sul deskap" + }, + "sshTools": { + "title": "Strumenti SSH", + "closeTools": "Chiudi Strumenti SSH", + "keyRecording": "Registrazione Tasti", + "startKeyRecording": "Avvia Registrazione Tasti", + "stopKeyRecording": "Ferma Registrazione Tasti", + "selectTerminals": "Seleziona terminali:", + "typeCommands": "Digita comandi (tutti i tasti supportati):", + "commandsWillBeSent": "I comandi saranno inviati a {{count}} terminale/i selezionato/i.", + "settings": "Impostazioni", + "enableRightClickCopyPaste": "Abilita copia/incolla con tasto destro", + "shareIdeas": "Hai idee per le prossime funzionalità degli strumenti SSH? Condividile su" + "keyRecording": "Regètrazèue Tcometi", + "startKeyRecording": "Avvia Regètrazèue Tcometi", + "stopKeyRecording": "Ferma Regètrazèue Tcometi", + "selectTerminals": "Selezèua terminalei:", + "typeCommands": "Digita comei (tutti i tcometi suppotaai):", + "commandsWillBeSent": "I comei saranno inviai a {{count}} terminalee/i selezèuaa/i.", + "settings": "Impostazèui", + "enableRightClickCopyPaste": "Abilita copia/incolla csu clicca destro", + "shareIdeas": "Hai idee per le prossime funzèualità degli strumenti SSH? Csudividile su" + }, + "snippets": { + "title": "Snippet", + "new": "Nuovo Snippet", + "create": "Crea Snippet", + "edit": "Modifica Snippet", + "run": "Esegui", + "empty": "Nessuno snippet", + "emptyHint": "Crea uno snippet per salvare i comandi usati frequentemente", + "name": "Nome", + "description": "Descrizione", + "content": "Comando", + "namePlaceholder": "es. Riavvia Nginx", + "descriptionPlaceholder": "Descrizione opzionale", + "contentPlaceholder": "es. sudo systemctl restart nginx", + "nameRequired": "Il nome è obbligatorio", + "contentRequired": "Il comando è obbligatorio", + "createDescription": "Crea un nuovo snippet di comando per esecuzione rapida", + "editDescription": "Modifica questo snippet di comando", + "deleteConfirmTitle": "Elimina Snippet", + "deleteConfirmDescription": "Sei sicuro di voler eliminare \"{{name}}\"?", + "createSuccess": "Snippet creato con successo", + "updateSuccess": "Snippet aggiornato con successo", + "deleteSuccess": "Snippet eliminato con successo", + "createFailed": "Impossibile creare lo snippet", + "updateFailed": "Impossibile aggiornare lo snippet", + "deleteFailed": "Impossibile eliminare lo snippet", + "failedToFetch": "Impossibile recuperare gli snippet", + "executeSuccess": "Esecuzione: {{name}}", + "copySuccess": "\"{{name}}\" copiato negli appunti", + "runTooltip": "Esegui questo snippet nel terminale", + "copyTooltip": "Copia snippet negli appunti", + "editTooltip": "Modifica questo snippet", + "deleteTooltip": "Elimina questo snippet" + }, + "commandHistory": { + "title": "Cronologia", + "searchPlaceholder": "Cerca comandi...", + "noTerminal": "Nessun terminale attivo", + "noTerminalHint": "Apri un terminale per vedere la sua cronologia comandi.", + "empty": "Nessun comando nella cronologia", + "emptyHint": "Esegui comandi nel terminale attivo per costruire la cronologia.", + "noResults": "Nessun comando trovato", + "noResultsHint": "Nessun comando corrispondente a \"{{query}}\"", + "deleteSuccess": "Comando eliminato dalla cronologia", + "deleteFailed": "Impossibile eliminare il comando.", + "deleteTooltip": "Elimina comando", + "tabHint": "Usa Tab nel terminale per autocompletare dalla cronologia" + }, + "homepage": { + "loggedInTitle": "Accesso effettuato!", + "loggedInMessage": "Hai effettuato l'accesso! Usa la barra laterale per accedere a tutti gli strumenti disponibili. Per iniziare, crea un Host SSH nella scheda Gestione SSH. Una volta creato, puoi connetterti usando le altre app nella barra laterale.", + "failedToLoadAlerts": "Impossibile caricare gli avvisi", + "failedToDismissAlert": "Impossibile chiudere l'avviso" + }, + "serverConfig": { + "title": "Configurazione Server", + "description": "Configura l'URL del server Termix per connetterti ai servizi backend", + "serverUrl": "URL Server", + "enterServerUrl": "Inserisci un URL del server", + "testConnectionFirst": "Testa prima la connessione", + "connectionSuccess": "Connessione riuscita!", + "connectionFailed": "Connessione fallita", + "connectionError": "Si è verificato un errore di connessione", + "connected": "Connesso", + "disconnected": "Disconnesso", + "configSaved": "Configurazione salvata con successo", + "saveFailed": "Impossibile salvare la configurazione", + "saveError": "Errore nel salvataggio della configurazione", + "saving": "Salvataggio...", + "saveConfig": "Salva Configurazione", + "helpText": "Inserisci l'URL dove il tuo server Termix è in esecuzione (es. http://localhost:30001 o https://tuo-server.com)", + "warning": "Avviso", + "notValidatedWarning": "URL non validata - assicurati che sia corretta", + "changeServer": "Cambia Server", + "mustIncludeProtocol": "L'URL del server deve iniziare con http:// o https://" + }, + "versionCheck": { + "error": "Errore Controllo Versione", + "checkFailed": "Impossibile controllare gli aggiornamenti", + "upToDate": "App Aggiornata", + "currentVersion": "Stai usando la versione {{version}}", + "updateAvailable": "Aggiornamento Disponibile", + "newVersionAvailable": "È disponibile una nuova versione! Stai usando {{current}}, ma {{latest}} è disponibile.", + "releasedOn": "Rilasciata il {{date}}", + "downloadUpdate": "Scarica Aggiornamento", + "dismiss": "Chiudi", + "checking": "Controllo aggiornamenti...", + "checkUpdates": "Controlla Aggiornamenti", + "checkingUpdates": "Controllo aggiornamenti...", + "refresh": "Aggiorna", + "updateRequired": "Aggiornamento Richiesto", + "updateDismissed": "Notifica aggiornamento chiusa", + "noUpdatesFound": "Nessun aggiornamento trovato" + }, + "common": { + "close": "Chiudi", + "minimize": "Riduci a icona", + "online": "Online", + "offline": "Offline", + "continue": "Continua", + "maintenance": "Manutenzione", + "degraded": "Degradato", + "discord": "Discord", + "error": "Errore", + "warning": "Avviso", + "info": "Info", + "success": "Successo", + "loading": "Caricamento...", + "required": "Obbligatorio", + "optional": "Opzionale", + "connect": "Connetti", + "connecting": "Connessione...", + "clear": "Pulisci", + "toggleSidebar": "Mostra/Nascondi Barra Laterale", + "sidebar": "Barra Laterale", + "home": "Home", + "expired": "Scaduto", + "expiresToday": "Scade oggi", + "expiresTomorrow": "Scade domani", + "expiresInDays": "Scade tra {{days}} giorni", + "updateAvailable": "Aggiornamento Disponibile", + "sshPath": "Percorso SSH", + "localPath": "Percorso Locale", + "noAuthCredentials": "Nessuna credenziale di autenticazione disponibile per questo host SSH", + "noReleases": "Nessuna Release", + "updatesAndReleases": "Aggiornamenti e Release", + "newVersionAvailable": "È disponibile una nuova versione ({{version}}).", + "failedToFetchUpdateInfo": "Impossibile recuperare le informazioni sull'aggiornamento", + "preRelease": "Pre-release", + "loginFailed": "Accesso fallito", + "noReleasesFound": "Nessuna release trovata.", + "yourBackupCodes": "I Tuoi Codici di Backup", + "sendResetCode": "Invia Codice di Reset", + "verifyCode": "Verifica Codice", + "resetPassword": "Reimposta Password", + "resetCode": "Codice di Reset", + "newPassword": "Nuova Password", + "folder": "Cartella", + "file": "File", + "renamedSuccessfully": "rinominato con successo", + "deletedSuccessfully": "eliminato con successo", + "noTunnelConnections": "Nessuna connessione tunnel configurata", + "emptyHint": "Crea uno snippet per salvssuo i comei usai frequentemente", + "name": "Nome", + "description": "Descrizèue", + "content": "Comeo", + "namePlaceholder": "es. Riavvia Nginx", + "descriptionPlaceholder": "Descrizèue opzèuale", + "contentPlaceholder": "es. sufare systemctl reavvia nginx", + "nameRequired": "Il nome è obbligaaio", + "contentRequired": "Il comeo è obbligaaio", + "createDescription": "Crea un nuovo snippet di comeo per esecuzèue rapida", + "editDescription": "Modifica quesa snippet di comeo", + "deleteConfirmTitle": "Elimina Snippet", + "deleteConfirmDescription": "Sei sicuro di voler eliminssuo \"{{nome}}\"?", + "createSuccess": "Snippet creaa csu successoo", + "updateSuccess": "Snippet aggèuaa csu successoo", + "deleteSuccess": "Snippet eliminaa csu successoo", + "createFailed": "Impossibile cressuo lo snippet", + "updateFailed": "Impossibile aggèussuo lo snippet", + "deleteFailed": "Impossibile eliminssuo lo snippet", + "failedToFetch": "Impossibile recuperssuo gli snippet", + "executeSuccess": "Esecuzèue: {{nome}}", + "copySuccess": "\"{{nome}}\" copiaa negli appunti", + "runTooltip": "Esegui quesa snippet nel terminalee", + "copyTooltip": "Copia snippet negli appunti", + "editTooltip": "Modifica quesa snippet", + "deleteTooltip": "Elimina quesa snippet" + }, + "commandHistory": { + "title": "Crsuologia", + "searchPlaceholder": "Cerca comei...", + "noTerminal": "Nessun terminalee ativo", + "noTerminalHint": "Apri un terminalee per vedere la sua crsuologia comei.", + "empty": "Nessun comeo nella crsuologia", + "emptyHint": "Esegui comei nel terminalee ativo per costruire la crsuologia.", + "noResults": "Nessun comeo trovaa", + "noResultsHint": "Nessun comeo corèpsudente a \"{{query}}\"", + "deleteSuccess": "Comeo eliminaa dtuttia crsuologia", + "deleteFailed": "Impossibile eliminssuo il comeo.", + "deleteTooltip": "Elimina comeo", + "tabHint": "Usa Tab nel terminalee per auacompletssuo dtuttia crsuologia" + }, + "homepage": { + "loggedInTitle": "Accesso effettuaa!", + "loggedInMessage": "Hai effettuaa l'accesso! Usa la barra laerale per accedere a tutti gli strumenti dèpsuibili. Per inizèsuo, crea un Host SSH nella scheda Gestèue SSH. Una volta creaa, puoi csunetterti useo le altre app nella barra laerale.", + "failedToLoadAlerts": "Impossibile caricssuo gli avvèi", + "failedToDismissAlert": "Impossibile chiudere l'avvèo" + }, + "serverConfig": { + "title": "Csufigurazèue Server", + "description": "Csufigura l'URL del server Termix per csunetterti ai servizi indietroend", + "serverUrl": "URL Server", + "enterServerUrl": "Inserèci un URL del server", + "testConnectionFirst": "Testa prima la csunessèue", + "connectionSuccess": "Csunessèue riuscita!", + "connectionFailed": "Csunessèue ftuttiita", + "connectionError": "Si è verificaa un erroee di csunessèue", + "connected": "Csunesso", + "disconnected": "Dècsunesso", + "configSaved": "Csufigurazèue salvaa csu successoo", + "saveFailed": "Impossibile salvssuo la csufigurazèue", + "saveError": "Erroee nel salvaaggio della csufigurazèue", + "saving": "Salvaaggio...", + "saveConfig": "Salva Csufigurazèue", + "helpText": "Inserèci l'URL fareve il tuo server Termix è in esecuzèue (es. http://localhost:30001 o https://tuo-server.com)", + "warning": "Avvèo", + "notValidatedWarning": "URL nsu validaa - comesicurai che sia coreta", + "changeServer": "Cambia Server", + "mustIncludeProtocol": "L'URL del server deve inizèsuo csu http:// o https://" + }, + "versionCheck": { + "error": "Erroee Csutrollo Versèue", + "checkFailed": "Impossibile csutrollssuo gli aggèuomenti", + "upToDate": "App Aggèuaa", + "currentVersion": "Stai useo la versèue {{versèu}}", + "updateAvailable": "Aggèuomena Dèpsuibile", + "newVersionAvailable": "È dèpsuibile una nuova versèue! Stai useo {{current}}, ma {{laest}} è dèpsuibile.", + "releasedOn": "Rilcomeciaa il {{dae}}", + "downloadUpdate": "Scarica Aggèuomena", + "dismiss": "Chiudi", + "checking": "Csutrollo aggèuomenti...", + "checkUpdates": "Csutrolla Aggèuomenti", + "checkingUpdates": "Csutrollo aggèuomenti...", + "refresh": "Aggèua", + "updateRequired": "Aggèuomena Richiesa", + "updateDismissed": "Notifica aggèuomena chiusa", + "noUpdatesFound": "Nessun aggèuomena trovaa" + }, + "common": { + "close": "Chiudi", + "minimize": "Riduci a icsua", + "online": "Online", + "offline": "Offline", + "continue": "Csutinua", + "maintenance": "Manutenzèue", + "degraded": "Degradaa", + "discord": "Dècod", + "error": "Erroee", + "warning": "Avvèo", + "info": "Info", + "success": "Successoo", + "loading": "Caricamena...", + "required": "Obbligaaio", + "optional": "Opzèuale", + "connect": "Csunetti", + "connecting": "Csunessèue...", + "clear": "Annullala", + "toggleSidebar": "Mostra/Ncomecsudi Barra Laerale", + "sidebar": "Barra Laerale", + "home": "Home", + "expired": "Scadua", + "expiresToday": "Scade oggi", + "expiresTomorrow": "Scade faremani", + "expiresInDays": "Scade tra {{days}} gèui", + "updateAvailable": "Aggèuomena Dèpsuibile", + "sshPath": "Percoso SSH", + "localPath": "Percoso Locale", + "noAuthCredentials": "Nessuna credenziale di autenticazèue dèpsuibile per quesa host SSH", + "noReleases": "Nessuna Relecomee", + "updatesAndReleases": "Aggèuomenti e Relecomee", + "newVersionAvailable": "È dèpsuibile una nuova versèue ({{versèu}}).", + "failedToFetchUpdateInfo": "Impossibile recuperssuo le inpermazèui sull'aggèuomena", + "preRelease": "Pre-relecomee", + "loginFailed": "Accesso ftuttiia", + "noReleasesFound": "Nessuna relecomee trovaa.", + "yourBackupCodes": "I Tuoi Codici di Indietroup", + "sendResetCode": "Invia Codice di Reset", + "verifyCode": "Verifica Codice", + "resetPassword": "Reimposta Pcomeswod", + "resetCode": "Codice di Reset", + "newPassword": "Nuova Pcomeswod", + "folder": "Cartella", + "file": "File", + "renamedSuccessfully": "rinominaa csu successoo", + "deletedSuccessfully": "eliminaa csu successoo", + "noTunnelConnections": "Nessuna csunessèue tunnel csufiguraa", + "sshTools": "Strumenti SSH", + "english": "Inglese", + "chinese": "Cinese", + "german": "Tedesco", + "cancel": "Annulla", + "username": "Nome Utente", + "name": "Nome", + "login": "Accedi", + "logout": "Esci", + "register": "Registrati", + "password": "Password", + "version": "Versione", + "confirmPassword": "Conferma Password", + "register": "Regètrai", + "password": "Pcomeswod", + "version": "Versèue", + "confirmPassword": "Csuferma Pcomeswod", + "back": "Indietro", + "email": "Email", + "submit": "Invia", + "change": "Modifica", + "save": "Salva", + "saving": "Salvataggio...", + "saving": "Salvaaggio...", + "delete": "Elimina", + "edit": "Modifica", + "add": "Aggiungi", + "search": "Cerca", + "confirm": "Conferma", + "yes": "Sì", + "no": "No", + "ok": "OK", + "enabled": "Abilitato", + "disabled": "Disabilitato", + "important": "Importante", + "notEnabled": "Non Abilitato", + "settingUp": "Configurazione...", + "next": "Avanti", + "previous": "Precedente", + "refresh": "Aggiorna", + "settings": "Impostazioni", + "profile": "Profilo", + "help": "Aiuto", + "about": "Informazioni", + "language": "Lingua", + "autoDetect": "Rileva automaticamente", + "changeAccountPassword": "Cambia la password del tuo account", + "passwordResetTitle": "Reimposta Password", + "passwordResetDescription": "Stai per reimpostare la tua password. Questo ti disconnetterà da tutte le sessioni attive.", + "enterSixDigitCode": "Inserisci il codice a 6 cifre dai log del container docker per l'utente:", + "enterNewPassword": "Inserisci la tua nuova password per l'utente:", + "passwordsDoNotMatch": "Le password non corrispondono", + "passwordMinLength": "La password deve avere almeno 6 caratteri", + "passwordResetSuccess": "Password reimpostata con successo! Ora puoi accedere con la nuova password.", + "failedToInitiatePasswordReset": "Impossibile avviare il reset della password", + "failedToVerifyResetCode": "Impossibile verificare il codice di reset", + "failedToCompletePasswordReset": "Impossibile completare il reset della password", + "documentation": "Documentazione", + "retry": "Riprova", + "checking": "Controllo...", + "checkingDatabase": "Controllo connessione database..." + "confirm": "Csuferma", + "yes": "Sì", + "no": "No", + "ok": "OK", + "enabled": "Abilitaa", + "disabled": "Dèabilitaa", + "important": "Impotaaante", + "notEnabled": "Nsu Abilitaa", + "settingUp": "Csufigurazèue...", + "next": "Avanti", + "previous": "Precedente", + "refresh": "Aggèua", + "settings": "Impostazèui", + "profile": "Prdiilo", + "help": "Aiua", + "about": "Inpermazèui", + "language": "Lingua", + "autoDetect": "Rileva auamaicamente", + "changeAccountPassword": "Cambia la pcomeswod del tuo account", + "passwordResetTitle": "Reimposta Pcomeswod", + "passwordResetDescription": "Stai per reimpostssuo la tua pcomeswod. Quesa ti dècsunetterà da tutte le sessèui ative.", + "enterSixDigitCode": "Inserèci il codice a 6 cifre dai log del csutainer farecker per l'utente:", + "enterNewPassword": "Inserèci la tua nuova pcomeswod per l'utente:", + "passwordsDoNotMatch": "Le pcomeswod nsu corèpsudsuo", + "passwordMinLength": "La pcomeswod deve avere almeno 6 carateri", + "passwordResetSuccess": "Pcomeswod reimpostaa csu successoo! Ora puoi accedere csu la nuova pcomeswod.", + "failedToInitiatePasswordReset": "Impossibile avvèsuo il reset della pcomeswod", + "failedToVerifyResetCode": "Impossibile verificssuo il codice di reset", + "failedToCompletePasswordReset": "Impossibile completssuo il reset della pcomeswod", + "documentation": "Documentazèue", + "retry": "Riprova", + "checking": "Csutrollo...", + "checkingDatabase": "Csutrollo csunessèue daabcomee..." + }, + "nav": { + "home": "Home", + "hosts": "Host", + "credentials": "Credenziali", + "terminal": "Terminale", + "tunnels": "Tunnel", + "fileManager": "Gestione File", + "serverStats": "Statistiche Server", + "admin": "Amministrazione", + "userProfile": "Profilo Utente", + "tools": "Strumenti", + "snippets": "Snippet", + "newTab": "Nuova Scheda", + "splitScreen": "Schermo Diviso", + "closeTab": "Chiudi Scheda", + "sshManager": "Gestione SSH", + "hostManager": "Gestione Host", + "cannotSplitTab": "Impossibile dividere questa scheda", + "tabNavigation": "Navigazione Schede" + }, + "admin": { + "title": "Impostazioni Amministratore", + "oidc": "OIDC", + "users": "Utenti", + "userManagement": "Gestione Utenti", + "makeAdmin": "Rendi Amministratore", + "removeAdmin": "Rimuovi Amministratore", + "deleteUser": "Eliminare l'utente {{username}}? Questa operazione non può essere annullata.", + "allowRegistration": "Consenti Registrazione", + "oidcSettings": "Impostazioni OIDC", + "clientId": "ID Client", + "clientSecret": "Segreto Client", + "issuerUrl": "URL Issuer", + "authorizationUrl": "URL Autorizzazione", + "tokenUrl": "URL Token", + "updateSettings": "Aggiorna Impostazioni", + "confirmDelete": "Sei sicuro di voler eliminare questo utente?", + "confirmMakeAdmin": "Sei sicuro di voler rendere questo utente amministratore?", + "confirmRemoveAdmin": "Sei sicuro di voler rimuovere i privilegi di amministratore da questo utente?", + "externalAuthentication": "Autenticazione Esterna (OIDC)", + "configureExternalProvider": "Configura il provider di identità esterno per l'autenticazione OIDC/OAuth2.", + "userIdentifierPath": "Percorso Identificatore Utente", + "displayNamePath": "Percorso Nome Visualizzato", + "scopes": "Scope", + "saving": "Salvataggio...", + "saveConfiguration": "Salva Configurazione", + "reset": "Ripristina", + "success": "Successo", + "loading": "Caricamento...", + "refresh": "Aggiorna", + "loadingUsers": "Caricamento utenti...", + "username": "Nome Utente", + "type": "Tipo", + "actions": "Azioni", + "external": "Esterno", + "local": "Locale", + "adminManagement": "Gestione Amministratori", + "makeUserAdmin": "Rendi Utente Amministratore", + "adding": "Aggiunta...", + "currentAdmins": "Amministratori Attuali", + "adminBadge": "Amministratore", + "removeAdminButton": "Rimuovi Amministratore", + "general": "Generale", + "userRegistration": "Registrazione Utenti", + "allowNewAccountRegistration": "Consenti registrazione nuovi account", + "allowPasswordLogin": "Consenti login con nome utente/password", + "missingRequiredFields": "Campi obbligatori mancanti: {{fields}}", + "oidcConfigurationUpdated": "Configurazione OIDC aggiornata con successo!", + "failedToFetchOidcConfig": "Impossibile recuperare la configurazione OIDC", + "failedToFetchRegistrationStatus": "Impossibile recuperare lo stato della registrazione", + "failedToFetchPasswordLoginStatus": "Impossibile recuperare lo stato del login con password", + "failedToFetchUsers": "Impossibile recuperare gli utenti", + "oidcConfigurationDisabled": "Configurazione OIDC disabilitata con successo!", + "failedToUpdateOidcConfig": "Impossibile aggiornare la configurazione OIDC", + "failedToDisableOidcConfig": "Impossibile disabilitare la configurazione OIDC", + "enterUsernameToMakeAdmin": "Inserisci nome utente da rendere amministratore", + "userIsNowAdmin": "L'utente {{username}} è ora amministratore", + "failedToMakeUserAdmin": "Impossibile rendere l'utente amministratore", + "removeAdminStatus": "Rimuovere lo stato di amministratore da {{username}}?", + "adminStatusRemoved": "Stato di amministratore rimosso da {{username}}", + "failedToRemoveAdminStatus": "Impossibile rimuovere lo stato di amministratore", + "userDeletedSuccessfully": "Utente {{username}} eliminato con successo", + "failedToDeleteUser": "Impossibile eliminare l'utente", + "overrideUserInfoUrl": "Sovrascrivi URL Utente Info (non obbligatorio)", + "failedToFetchSessions": "Impossibile recuperare le sessioni", + "sessionRevokedSuccessfully": "Sessione revocata con successo", + "failedToRevokeSession": "Impossibile revocare la sessione", + "confirmRevokeSession": "Sei sicuro di voler revocare questa sessione?", + "confirmRevokeAllSessions": "Sei sicuro di voler revocare tutte le sessioni per questo utente?", + "failedToRevokeSessions": "Impossibile revocare le sessioni", + "sessionsRevokedSuccessfully": "Sessioni revocate con successo", + "linkToPasswordAccount": "Collega ad Account con Password", + "linkOIDCDialogTitle": "Collega Account OIDC ad Account con Password", + "linkOIDCDialogDescription": "Collega {{username}} (utente OIDC) a un account con password esistente. Questo abiliterà l'autenticazione doppia per l'account con password.", + "linkOIDCWarningTitle": "Attenzione: I Dati dell'Utente OIDC Saranno Eliminati", + "linkOIDCActionDeleteUser": "Elimina l'account utente OIDC e tutti i suoi dati", + "linkOIDCActionAddCapability": "Aggiungi la capacità di login OIDC all'account con password di destinazione", + "linkOIDCActionDualAuth": "Consenti all'account con password di accedere sia con password che con OIDC", + "linkTargetUsernameLabel": "Nome Utente Account Password di Destinazione", + "linkTargetUsernamePlaceholder": "Inserisci nome utente dell'account con password", + "linkAccountsButton": "Collega Account", + "linkingAccounts": "Collegamento...", + "accountsLinkedSuccessfully": "L'utente OIDC {{oidcUsername}} è stato collegato a {{targetUsername}}", + "failedToLinkAccounts": "Impossibile collegare gli account", + "linkTargetUsernameRequired": "Il nome utente di destinazione è obbligatorio", + "unlinkOIDCTitle": "Scollega Autenticazione OIDC", + "unlinkOIDCDescription": "Rimuovere l'autenticazione OIDC da {{username}}? L'utente potrà accedere solo con nome utente/password dopo questa operazione.", + "unlinkOIDCSuccess": "OIDC scollegato da {{username}}", + "failedToUnlinkOIDC": "Impossibile scollegare OIDC", + "databaseSecurity": "Sicurezza Database", + "encryptionStatus": "Stato Crittografia", + "encryptionEnabled": "Crittografia Abilitata", + "enabled": "Abilitato", + "disabled": "Disabilitato", + "keyId": "ID Chiave", + "created": "Creata", + "migrationStatus": "Stato Migrazione", + "migrationCompleted": "Migrazione completata", + "migrationRequired": "Migrazione richiesta", + "deviceProtectedMasterKey": "Chiave Master Protetta dall'Ambiente", + "legacyKeyStorage": "Storage Chiave Legacy", + "masterKeyEncryptedWithDeviceFingerprint": "Chiave master criptata con fingerprint ambiente (protezione KEK attiva)", + "keyNotProtectedByDeviceBinding": "Chiave non protetta dal binding ambiente (aggiornamento consigliato)", + "valid": "Valido", + "initializeDatabaseEncryption": "Inizializza Crittografia Database", + "enableAes256EncryptionWithDeviceBinding": "Abilita crittografia AES-256 con protezione chiave master legata all'ambiente. Questo crea sicurezza di livello enterprise per chiavi SSH, password e token di autenticazione.", + "featuresEnabled": "Funzionalità abilitate:", + "aes256GcmAuthenticatedEncryption": "Crittografia autenticata AES-256-GCM", + "deviceFingerprintMasterKeyProtection": "Protezione chiave master con fingerprint ambiente (KEK)", + "pbkdf2KeyDerivation": "Derivazione chiave PBKDF2 con 100K iterazioni", + "automaticKeyManagement": "Gestione automatica chiavi e rotazione", + "initializing": "Inizializzazione...", + "initializeEnterpriseEncryption": "Inizializza Crittografia Enterprise", + "migrateExistingData": "Migra Dati Esistenti", + "encryptExistingUnprotectedData": "Cripta i dati esistenti non protetti nel tuo database. Questo processo è sicuro e crea backup automatici.", + "testMigrationDryRun": "Verifica Compatibilità Crittografia", + "migrating": "Migrazione...", + "migrateData": "Migra Dati", + "securityInformation": "Informazioni Sicurezza", + "sshPrivateKeysEncryptedWithAes256": "Le chiavi private SSH e le password sono criptate con AES-256-GCM", + "userAuthTokensProtected": "I token di autenticazione utente e i segreti 2FA sono protetti", + "masterKeysProtectedByDeviceFingerprint": "Le chiavi master di crittografia sono protette dal fingerprint del dispositivo (KEK)", + "keysBoundToServerInstance": "Le chiavi sono legate all'ambiente server corrente (migrabile tramite variabili d'ambiente)", + "pbkdf2HkdfKeyDerivation": "Derivazione chiave PBKDF2 + HKDF con 100K iterazioni", + "backwardCompatibleMigration": "Tutti i dati rimangono retrocompatibili durante la migrazione", + "enterpriseGradeSecurityActive": "Sicurezza di Livello Enterprise Attiva", + "masterKeysProtectedByDeviceBinding": "Le tue chiavi master di crittografia sono protette dal fingerprinting dell'ambiente. Questo usa hostname del server, percorsi e altre info ambiente per generare chiavi di protezione. Per migrare i server, imposta la variabile d'ambiente DB_ENCRYPTION_KEY sul nuovo server.", + "important": "Importante", + "keepEncryptionKeysSecure": "Assicura la sicurezza dei dati: esegui regolarmente backup del file database e della configurazione server. Per migrare a un nuovo server, imposta la variabile d'ambiente DB_ENCRYPTION_KEY sul nuovo ambiente, o mantieni lo stesso hostname e struttura directory.", + "loadingEncryptionStatus": "Caricamento stato crittografia...", + "testMigrationDescription": "Verifica che i dati esistenti possano essere migrati in modo sicuro al formato criptato senza modificare effettivamente alcun dato", + "serverMigrationGuide": "Guida Migrazione Server", + "migrationInstructions": "Per migrare dati criptati a un nuovo server: 1) Backup file database, 2) Imposta variabile d'ambiente DB_ENCRYPTION_KEY=\"tua-chiave\" sul nuovo server, 3) Ripristina file database", + "environmentProtection": "Protezione Ambiente", + "environmentProtectionDesc": "Protegge le chiavi di crittografia basandosi sulle info ambiente del server (hostname, percorsi, ecc.), migrabile tramite variabili d'ambiente", + "verificationCompleted": "Verifica compatibilità completata - nessun dato modificato", + "verificationInProgress": "Verifica completata", + "dataMigrationCompleted": "Migrazione dati completata con successo!", + "verificationFailed": "Verifica compatibilità fallita", + "migrationFailed": "Migrazione fallita", + "runningVerification": "Esecuzione verifica compatibilità...", + "startingMigration": "Avvio migrazione...", + "hardwareFingerprintSecurity": "Sicurezza Fingerprint Hardware", + "hardwareBoundEncryption": "Crittografia Legata all'Hardware Attiva", + "masterKeysNowProtectedByHardwareFingerprint": "Le chiavi master sono ora protette dal fingerprinting hardware reale invece delle variabili d'ambiente", + "cpuSerialNumberDetection": "Rilevamento numero seriale CPU", + "motherboardUuidIdentification": "Identificazione UUID scheda madre", + "diskSerialNumberVerification": "Verifica numero seriale disco", + "biosSerialNumberCheck": "Controllo numero seriale BIOS", + "stableMacAddressFiltering": "Filtraggio indirizzo MAC stabile", + "databaseFileEncryption": "Crittografia File Database", + "dualLayerProtection": "Protezione a Doppio Livello Attiva", + "bothFieldAndFileEncryptionActive": "Sia la crittografia a livello di campo che quella a livello di file sono ora attive per la massima sicurezza", + "fieldLevelAes256Encryption": "Crittografia AES-256 a livello di campo per dati sensibili", + "fileLevelDatabaseEncryption": "Crittografia a livello di file del database con binding hardware", + "hardwareBoundFileKeys": "Chiavi di crittografia file legate all'hardware", + "automaticEncryptedBackups": "Creazione automatica backup criptati", + "createEncryptedBackup": "Crea Backup Criptato", + "creatingBackup": "Creazione Backup...", + "backupCreated": "Backup Creato", + "encryptedBackupCreatedSuccessfully": "Backup criptato creato con successo", + "backupCreationFailed": "Creazione backup fallita", + "databaseMigration": "Migrazione Database", + "exportForMigration": "Esporta per Migrazione", + "exportDatabaseForHardwareMigration": "Esporta database come file SQLite con dati decriptati per migrazione a nuovo hardware", + "exportDatabase": "Esporta Database SQLite", + "exporting": "Esportazione...", + "exportCreated": "Esportazione SQLite Creata", + "exportContainsDecryptedData": "L'esportazione SQLite contiene dati decriptati - mantienila sicura!", + "databaseExportedSuccessfully": "Database SQLite esportato con successo", + "databaseExportFailed": "Esportazione database SQLite fallita", + "importFromMigration": "Importa da Migrazione", + "importDatabaseFromAnotherSystem": "Importa database SQLite da un altro sistema o hardware", + "importDatabase": "Importa Database SQLite", + "importing": "Importazione...", + "selectedFile": "File SQLite Selezionato", + "importWillReplaceExistingData": "L'importazione SQLite sostituirà i dati esistenti - backup consigliato!", + "pleaseSelectImportFile": "Seleziona un file di importazione SQLite", + "databaseImportedSuccessfully": "Database SQLite importato con successo", + "databaseImportFailed": "Importazione database SQLite fallita", + "manageEncryptionAndBackups": "Gestisci chiavi di crittografia, sicurezza database e operazioni di backup", + "activeSecurityFeatures": "Misure di sicurezza e protezioni attualmente attive", + "deviceBindingTechnology": "Tecnologia avanzata di protezione chiavi basata su hardware", + "backupAndRecovery": "Creazione backup sicuri e opzioni di recupero database", + "crossSystemDataTransfer": "Esporta e importa database tra sistemi diversi", + "noMigrationNeeded": "Nessuna migrazione necessaria", + "encryptionKey": "Chiave di Crittografia", + "keyProtection": "Protezione Chiave", + "active": "Attivo", + "legacy": "Legacy", + "dataStatus": "Stato Dati", + "encrypted": "Criptato", + "needsMigration": "Richiede Migrazione", + "ready": "Pronto", + "initializeEncryption": "Inizializza Crittografia", + "initialize": "Inizializza", + "test": "Testa", + "migrate": "Migra", + "backup": "Backup", + "createBackup": "Crea Backup", + "exportImport": "Esporta/Importa", + "export": "Esporta", + "import": "Importa", + "passwordRequired": "Password obbligatoria", + "confirmExport": "Conferma Esportazione", + "exportDescription": "Esporta host SSH e credenziali come file SQLite", + "importDescription": "Importa file SQLite con merge incrementale (salta i duplicati)", + "criticalWarning": "Avviso Critico", + "cannotDisablePasswordLoginWithoutOIDC": "Impossibile disabilitare il login con password senza OIDC configurato! Devi configurare l'autenticazione OIDC prima di disabilitare il login con password, altrimenti perderai l'accesso a Termix.", + "confirmDisablePasswordLogin": "Sei sicuro di voler disabilitare il login con password? Assicurati che OIDC sia correttamente configurato e funzionante prima di procedere, altrimenti perderai l'accesso alla tua istanza Termix.", + "passwordLoginDisabled": "Login con password disabilitato con successo", + "passwordLoginAndRegistrationDisabled": "Login con password e registrazione nuovi account disabilitati con successo", + "requiresPasswordLogin": "Richiede login con password abilitato", + "passwordLoginDisabledWarning": "Il login con password è disabilitato. Assicurati che OIDC sia configurato correttamente o non potrai accedere a Termix.", + "oidcRequiredWarning": "CRITICO: Il login con password è disabilitato. Se resetti o configuri male OIDC, perderai tutti gli accessi a Termix e la tua istanza diventerà inaccessibile. Procedi solo se sei assolutamente sicuro.", + "confirmDisableOIDCWarning": "ATTENZIONE: Stai per disabilitare OIDC mentre il login con password è anche disabilitato. Questo renderà inaccessibile la tua istanza Termix e perderai tutti gli accessi. Sei assolutamente sicuro di voler procedere?", + "failedToUpdatePasswordLoginStatus": "Impossibile aggiornare lo stato del login con password" + }, + "hosts": { + "title": "Gestione Host", + "sshHosts": "Host SSH", + "noHosts": "Nessun Host SSH", + "noHostsMessage": "Non hai ancora aggiunto host SSH. Clicca \"Aggiungi Host\" per iniziare.", + "loadingHosts": "Caricamento host...", + "failedToLoadHosts": "Impossibile caricare gli host", + "retry": "Riprova", + "refresh": "Aggiorna", + "hostsCount": "{{count}} host", + "importJson": "Importa JSON", + "importing": "Importazione...", + "importJsonTitle": "Importa Host SSH da JSON", + "importJsonDesc": "Carica un file JSON per importare host SSH in blocco (max 100).", + "downloadSample": "Scarica Esempio", + "formatGuide": "Guida Formato", + "exportCredentialWarning": "Attenzione: L'host \"{{name}}\" usa autenticazione con credenziale. Il file esportato non includerà i dati della credenziale e dovrà essere riconfigurato manualmente dopo l'importazione. Vuoi continuare?", + "exportSensitiveDataWarning": "Attenzione: L'host \"{{name}}\" contiene dati di autenticazione sensibili (password/chiave SSH). Il file esportato includerà questi dati in testo semplice. Mantieni il file sicuro ed eliminalo dopo l'uso. Vuoi continuare?", + "uncategorized": "Non Categorizzata", + "confirmDelete": "Sei sicuro di voler eliminare \"{{name}}\"?", + "failedToDeleteHost": "Impossibile eliminare l'host", + "failedToExportHost": "Impossibile esportare l'host. Assicurati di aver effettuato l'accesso e di avere accesso ai dati dell'host.", + "jsonMustContainHosts": "Il JSON deve contenere un array \"host\" o essere un array di host", + "noHostsInJson": "Nessun host trovato nel file JSON", + "maxHostsAllowed": "Massimo 100 host consentiti per importazione", + "importCompleted": "Importazione completata: {{success}} riusciti, {{failed}} falliti", + "importFailed": "Importazione fallita", + "importError": "Errore di importazione", + "failedToImportJson": "Impossibile importare il file JSON", + "connectionDetails": "Dettagli Connessione", + "organization": "Organizzazione", + "ipAddress": "Indirizzo IP", + "port": "Porta", + "terminal": "Terminalee", + "tunnels": "Tunnel", + "fileManager": "Gestèue File", + "serverStats": "Staètiche Server", + "admin": "Amminètrazèue", + "userProfile": "Prdiilo Utente", + "tools": "Strumenti", + "snippets": "Snippet", + "newTab": "Nuova Scheda", + "splitScreen": "Schermo Divèo", + "closeTab": "Chiudi Scheda", + "sshManager": "Gestèue SSH", + "hostManager": "Gestèue Host", + "cannotSplitTab": "Impossibile dividere questa scheda", + "tabNavigation": "Navigazèue Schede" + }, + "admin": { + "title": "Impostazèui Amminètraae", + "oidc": "OIDC", + "users": "Utenti", + "userManagement": "Gestèue Utenti", + "makeAdmin": "Rendi Amminètraae", + "removeAdmin": "Rimuovi Amminètraae", + "deleteUser": "Eliminssuo l'utente {{nome utente}}? Questa azèue nsu può essere annullaa.", + "allowRegistration": "Csusenti Regètrazèue", + "oidcSettings": "Impostazèui OIDC", + "clientId": "ID Client", + "clientSecret": "Segreto Client", + "issuerUrl": "URL Issuer", + "authorizationUrl": "URL Auaizzazèue", + "tokenUrl": "URL Token", + "updateSettings": "Aggèua Impostazèui", + "confirmDelete": "Sei sicuro di voler eliminssuo quesa utente?", + "confirmMakeAdmin": "Sei sicuro di voler rendere quesa utente amminètraae?", + "confirmRemoveAdmin": "Sei sicuro di voler rimuovere i privilegi di amminètraae da quesa utente?", + "externalAuthentication": "Autenticazèue Esterna (OIDC)", + "configureExternalProvider": "Csufigura il provider di identità esterno per l'autenticazèue OIDC/OAuth2.", + "userIdentifierPath": "Percoso Identificaae Utente", + "displayNamePath": "Percoso Nome Vèualizzaa", + "scopes": "Scope", + "saving": "Salvaaggio...", + "saveConfiguration": "Salva Csufigurazèue", + "reset": "Riprètina", + "success": "Successoo", + "loading": "Caricamena...", + "refresh": "Aggèua", + "loadingUsers": "Caricamena utenti...", + "username": "Nome Utente", + "type": "Tipo", + "actions": "Azèui", + "external": "Esterno", + "local": "Locale", + "adminManagement": "Gestèue Amminètraai", + "makeUserAdmin": "Rendi Utente Amminètraae", + "adding": "Aggiunta...", + "currentAdmins": "Amminètraai Attuali", + "adminBadge": "Amminètraae", + "removeAdminButton": "Rimuovi Amminètraae", + "general": "Generalee", + "userRegistration": "Regètrazèue Utenti", + "allowNewAccountRegistration": "Csusenti regètrazèue nuovi account", + "allowPasswordLogin": "Csusenti login csu nome utente/pcomeswod", + "missingRequiredFields": "Campi obbligaai manpuòti: {{fields}}", + "oidcConfigurationUpdated": "Csufigurazèue OIDC aggèuaa csu successoo!", + "failedToFetchOidcConfig": "Impossibile recuperssuo la csufigurazèue OIDC", + "failedToFetchRegistrationStatus": "Impossibile recuperssuo lo staa della regètrazèue", + "failedToFetchPasswordLoginStatus": "Impossibile recuperssuo lo staa del login csu pcomeswod", + "failedToFetchUsers": "Impossibile recuperssuo gli utenti", + "oidcConfigurationDisabled": "Csufigurazèue OIDC dèabilitaa csu successoo!", + "failedToUpdateOidcConfig": "Impossibile aggèussuo la csufigurazèue OIDC", + "failedToDisableOidcConfig": "Impossibile dèabilitssuo la csufigurazèue OIDC", + "enterUsernameToMakeAdmin": "Inserèci nome utente da rendere amminètraae", + "userIsNowAdmin": "L'utente {{nome utente}} è oa amminètraae", + "failedToMakeUserAdmin": "Impossibile rendere l'utente amminètraae", + "removeAdminStatus": "Rimuovere lo staa di amminètraae da {{nome utente}}?", + "adminStatusRemoved": "Staa di amminètraae rimosso da {{nome utente}}", + "failedToRemoveAdminStatus": "Impossibile rimuovere lo staa di amminètraae", + "userDeletedSuccessfully": "Utente {{nome utente}} eliminaa csu successoo", + "failedToDeleteUser": "Impossibile eliminssuo l'utente", + "overrideUserInfoUrl": "Sovrcomecrivi URL Utente Info (nsu obbligaaio)", + "failedToFetchSessions": "Impossibile recuperssuo le sessèui", + "sessionRevokedSuccessfully": "Sessèue revocaa csu successoo", + "failedToRevokeSession": "Impossibile revocssuo la sessèue", + "confirmRevokeSession": "Sei sicuro di voler revocssuo questa sessèue?", + "confirmRevokeAllSessions": "Sei sicuro di voler revocssuo tutte le sessèui per quesa utente?", + "failedToRevokeSessions": "Impossibile revocssuo le sessèui", + "sessionsRevokedSuccessfully": "Sessèui revocae csu successoo", + "linkToPasswordAccount": "Collega ad Account csu Pcomeswod", + "linkOIDCDialogTitle": "Collega Account OIDC ad Account csu Pcomeswod", + "linkOIDCDialogDescription": "Collega {{nome utente}} (utente OIDC) a un account csu pcomeswod esètente. Quesa abiliterà l'autenticazèue fareppia per l'account csu pcomeswod.", + "linkOIDCWarningTitle": "Attenzèue: I Dai dell'Utente OIDC Saranno Eliminai", + "linkOIDCActionDeleteUser": "Elimina l'account utente OIDC e tutti i suoi dai", + "linkOIDCActionAddCapability": "Aggiungi la capacità di login OIDC tutti'account csu pcomeswod di destinazèue", + "linkOIDCActionDualAuth": "Csusenti tutti'account csu pcomeswod di accedere sia csu pcomeswod che csu OIDC", + "linkTargetUsernameLabel": "Nome Utente Account Pcomeswod di Destinazèue", + "linkTargetUsernamePlaceholder": "Inserèci nome utente dell'account csu pcomeswod", + "linkAccountsButton": "Collega Account", + "linkingAccounts": "Collegamena...", + "accountsLinkedSuccessfully": "L'utente OIDC {{oidcNome utente}} è staa collegaa a {{targetNome utente}}", + "failedToLinkAccounts": "Impossibile collegssuo gli account", + "linkTargetUsernameRequired": "Il nome utente di destinazèue è obbligaaio", + "unlinkOIDCTitle": "Scollega Autenticazèue OIDC", + "unlinkOIDCDescription": "Rimuovere l'autenticazèue OIDC da {{nome utente}}? L'utente potrà accedere solo csu nome utente/pcomeswod farepo questa operazèue.", + "unlinkOIDCSuccess": "OIDC scollegaa da {{nome utente}}", + "failedToUnlinkOIDC": "Impossibile scollegssuo OIDC", + "databaseSecurity": "Sicurezza Daabcomee", + "encryptionStatus": "Staa Critagrafia", + "encryptionEnabled": "Critagrafia Abilitaa", + "enabled": "Abilitaa", + "disabled": "Dèabilitaa", + "keyId": "ID Chiave", + "created": "Creaa", + "migrationStatus": "Staa Migrazèue", + "migrationCompleted": "Migrazèue completaa", + "migrationRequired": "Migrazèue richiesta", + "deviceProtectedMasterKey": "Chiave Mcometer Protetta dtutti'Ambiente", + "legacyKeyStorage": "Saage Chiave Legacy", + "masterKeyEncryptedWithDeviceFingerprint": "Chiave mcometer criptaa csu fingerprint ambiente (protezèue KEK ativa)", + "keyNotProtectedByDeviceBinding": "Chiave nsu protetta dal binding ambiente (aggèuomena csusigliaa)", + "valid": "Valifare", + "initializeDatabaseEncryption": "Inizializza Critagrafia Daabcomee", + "enableAes256EncryptionWithDeviceBinding": "Abilita critagrafia AES-256 csu protezèue chiave mcometer legaa tutti'ambiente. Quesa crea sicurezza di livello inserèciprèe per chiavi SSH, pcomeswod e aken di autenticazèue.", + "featuresEnabled": "Funzèualità abilitae:", + "aes256GcmAuthenticatedEncryption": "Critagrafia autenticaa AES-256-GCM", + "deviceFingerprintMasterKeyProtection": "Protezèue chiave mcometer csu fingerprint ambiente (KEK)", + "pbkdf2KeyDerivation": "Derivazèue chiave PBKDF2 csu 100K iterazèui", + "automaticKeyManagement": "Gestèue auamaica chiavi e rotazèue", + "initializing": "Inizializzazèue...", + "initializeEnterpriseEncryption": "Inizializza Critagrafia Inserèciprèe", + "migrateExistingData": "Migra Dai Esètenti", + "encryptExistingUnprotectedData": "Cripta i dai esètenti nsu protetti nel tuo daabcomee. Quesa processo è sicuro e crea indietroup auamaici.", + "testMigrationDryRun": "Verifica Compaibilità Critagrafia", + "migrating": "Migrazèue...", + "migrateData": "Migra Dai", + "securityInformation": "Inpermazèui Sicurezza", + "sshPrivateKeysEncryptedWithAes256": "Le chiavi privae SSH e le pcomeswod ssuo criptae csu AES-256-GCM", + "userAuthTokensProtected": "I aken di autenticazèue utente e i segreti 2FA ssuo protetti", + "masterKeysProtectedByDeviceFingerprint": "Le chiavi mcometer di critagrafia ssuo protette dal fingerprint del dèpositivo (KEK)", + "keysBoundToServerInstance": "Le chiavi ssuo legae tutti'ambiente server corente (migrabile tramite variabili d'ambiente)", + "pbkdf2HkdfKeyDerivation": "Derivazèue chiave PBKDF2 + HKDF csu 100K iterazèui", + "backwardCompatibleMigration": "Tutti i dai rimangsuo retrocompaibili durante la migrazèue", + "enterpriseGradeSecurityActive": "Sicurezza di Livello Inserèciprèe Attiva", + "masterKeysProtectedByDeviceBinding": "Le tue chiavi mcometer di critagrafia ssuo protette dal fingerprinting dell'ambiente. Quesa usa hostnome del server, percosi e altre info ambiente per generssuo chiavi di protezèue. Per migrssuo i server, imposta la variabile d'ambiente DB_ENCRYPTION_KEY sul nuovo server.", + "important": "Impotaaante", + "keepEncryptionKeysSecure": "Assicura la sicurezza dei dai: esegui regolarmente indietroup del file daabcomee e della csufigurazèue server. Per migrssuo a un nuovo server, imposta la variabile d'ambiente DB_ENCRYPTION_KEY sul nuovo ambiente, o mantieni lo stesso hostnome e struttura direcay.", + "loadingEncryptionStatus": "Caricamena staa critagrafia...", + "testMigrationDescription": "Verifica che i dai esètenti possano essere migrai in mofare sicuro al permaa criptaa senza modificssuo effettivamente alcun daa", + "serverMigrationGuide": "Guida Migrazèue Server", + "migrationInstructions": "Per migrssuo dai criptai a un nuovo server: 1) Indietroup file daabcomee, 2) Imposta variabile d'ambiente DB_ENCRYPTION_KEY=\"tua-chiave\" sul nuovo server, 3) Riprètina file daabcomee", + "environmentProtection": "Protezèue Ambiente", + "environmentProtectionDesc": "Protegge le chiavi di critagrafia bcomeeosi sulle info ambiente del server (hostnome, percosi, ecc.), migrabile tramite variabili d'ambiente", + "verificationCompleted": "Verifica compaibilità completaa - nessun daa modificaa", + "verificationInProgress": "Verifica completaa", + "dataMigrationCompleted": "Migrazèue dai completaa csu successoo!", + "verificationFailed": "Verifica compaibilità ftuttiita", + "migrationFailed": "Migrazèue ftuttiita", + "runningVerification": "Esecuzèue verifica compaibilità...", + "startingMigration": "Avvio migrazèue...", + "hardwareFingerprintSecurity": "Sicurezza Fingerprint Hardwssuo", + "hardwareBoundEncryption": "Critagrafia Legaa tutti'Hardwssuo Attiva", + "masterKeysNowProtectedByHardwareFingerprint": "Le chiavi mcometer ssuo oa protette dal fingerprinting hardwssuo reale invece delle variabili d'ambiente", + "cpuSerialNumberDetection": "Rilevamena numero seriale CPU", + "motherboardUuidIdentification": "Identificazèue UUID scheda madre", + "diskSerialNumberVerification": "Verifica numero seriale dèco", + "biosSerialNumberCheck": "Csutrollo numero seriale BIOS", + "stableMacAddressFiltering": "Filtraggio indirizzo MAC stabile", + "databaseFileEncryption": "Critagrafia File Daabcomee", + "dualLayerProtection": "Protezèue a Doppio Livello Attiva", + "bothFieldAndFileEncryptionActive": "Sia la critagrafia a livello di campo che quella a livello di file ssuo oa ative per la mcomesima sicurezza", + "fieldLevelAes256Encryption": "Critagrafia AES-256 a livello di campo per dai sensibili", + "fileLevelDatabaseEncryption": "Critagrafia a livello di file del daabcomee csu binding hardwssuo", + "hardwareBoundFileKeys": "Chiavi di critagrafia file legae tutti'hardwssuo", + "automaticEncryptedBackups": "Creazèue auamaica indietroup criptai", + "createEncryptedBackup": "Crea Indietroup Criptaa", + "creatingBackup": "Creazèue Indietroup...", + "backupCreated": "Indietroup Creaa", + "encryptedBackupCreatedSuccessfully": "Indietroup criptaa creaa csu successoo", + "backupCreationFailed": "Creazèue indietroup ftuttiita", + "databaseMigration": "Migrazèue Daabcomee", + "exportForMigration": "Espotaa per Migrazèue", + "exportDatabaseForHardwareMigration": "Espotaa daabcomee come file SQLite csu dai decriptai per migrazèue a nuovo hardwssuo", + "exportDatabase": "Espotaa Daabcomee SQLite", + "exporting": "Espotaazèue...", + "exportCreated": "Espotaazèue SQLite Creaa", + "exportContainsDecryptedData": "L'espotaazèue SQLite csutiene dai decriptai - mantienila sicura!", + "databaseExportedSuccessfully": "Daabcomee SQLite espotaaa csu successoo", + "databaseExportFailed": "Espotaazèue daabcomee SQLite ftuttiita", + "importFromMigration": "Impotaaa da Migrazèue", + "importDatabaseFromAnotherSystem": "Impotaaa daabcomee SQLite da un altro sètema o hardwssuo", + "importDatabase": "Impotaaa Daabcomee SQLite", + "importing": "Impotaaazèue...", + "selectedFile": "File SQLite Selezèuaa", + "importWillReplaceExistingData": "L'impotaaazèue SQLite sostituirà i dai esètenti - indietroup csusigliaa!", + "pleaseSelectImportFile": "Selezèua un file di impotaaazèue SQLite", + "databaseImportedSuccessfully": "Daabcomee SQLite impotaaaa csu successoo", + "databaseImportFailed": "Impotaaazèue daabcomee SQLite ftuttiita", + "manageEncryptionAndBackups": "Gestèci chiavi di critagrafia, sicurezza daabcomee e operazèui di indietroup", + "activeSecurityFeatures": "Mèure di sicurezza e protezèui atualmente ative", + "deviceBindingTechnology": "Tecnologia avanzaa di protezèue chiavi bcomeaa su hardwssuo", + "backupAndRecovery": "Creazèue indietroup sicuri e opzèui di recupero daabcomee", + "crossSystemDataTransfer": "Espotaa e impotaaa daabcomee tra sètemi diversi", + "noMigrationNeeded": "Nessuna migrazèue necessaria", + "encryptionKey": "Chiave di Critagrafia", + "keyProtection": "Protezèue Chiave", + "active": "Attivo", + "legacy": "Legacy", + "dataStatus": "Staa Dai", + "encrypted": "Criptaa", + "needsMigration": "Richiede Migrazèue", + "ready": "Prsua", + "initializeEncryption": "Inizializza Critagrafia", + "initialize": "Inizializza", + "test": "Testa", + "migrate": "Migra", + "backup": "Indietroup", + "createBackup": "Crea Indietroup", + "exportImport": "Espotaa/Impotaaa", + "export": "Espotaa", + "import": "Impotaaa", + "passwordRequired": "Pcomeswod obbligaaia", + "confirmExport": "Csuferma Espotaazèue", + "exportDescription": "Espotaa host SSH e credenziali come file SQLite", + "importDescription": "Impotaaa file SQLite csu merge incrementale (salta i duplicai)", + "criticalWarning": "Avvèo Critico", + "cannotDisablePasswordLoginWithoutOIDC": "Impossibile dèabilitssuo il login csu pcomeswod senza OIDC csufiguraa! Devi csufigurssuo l'autenticazèue OIDC prima di dèabilitssuo il login csu pcomeswod, altrimenti perderai l'accesso a Termix.", + "confirmDisablePasswordLogin": "Sei sicuro di voler dèabilitssuo il login csu pcomeswod? Assicurai che OIDC sia corettamente csufiguraa e funzèuante prima di procedere, altrimenti perderai l'accesso tuttia tua ètanza Termix.", + "passwordLoginDisabled": "Login csu pcomeswod dèabilitaa csu successoo", + "passwordLoginAndRegistrationDisabled": "Login csu pcomeswod e regètrazèue nuovi account dèabilitai csu successoo", + "requiresPasswordLogin": "Richiede login csu pcomeswod abilitaa", + "passwordLoginDisabledWarning": "Il login csu pcomeswod è dèabilitaa. Assicurai che OIDC sia csufiguraa corettamente o nsu potrai accedere a Termix.", + "oidcRequiredWarning": "CRITICO: Il login csu pcomeswod è dèabilitaa. Se resetti o csufiguri male OIDC, perderai tutti gli accessi a Termix e la tua ètanza divinserècià inaccessibile. Procedi solo se sei comesolutamente sicuro.", + "confirmDisableOIDCWarning": "ATTENZIONE: Stai per dèabilitssuo OIDC mentre il login csu pcomeswod è anche dèabilitaa. Quesa renderà inaccessibile la tua ètanza Termix e perderai tutti gli accessi. Sei comesolutamente sicuro di voler procedere?", + "failedToUpdatePasswordLoginStatus": "Impossibile aggèussuo lo staa del login csu pcomeswod" + }, + "hosts": { + "title": "Gestèue Host", + "sshHosts": "Host SSH", + "noHosts": "Nessun Host SSH", + "noHostsMessage": "Nsu hai ancoa aggiuna host SSH. Clicca \"Aggiungi Host\" per inizèsuo.", + "loadingHosts": "Caricamena host...", + "failedToLoadHosts": "Impossibile caricssuo gli host", + "retry": "Riprova", + "refresh": "Aggèua", + "hostsCount": "{{count}} host", + "importJson": "Impotaaa JSON", + "importing": "Impotaaazèue...", + "importJsonTitle": "Impotaaa Host SSH da JSON", + "importJsonDesc": "Carica un file JSON per impotacomesuo host SSH in blocco (max 100).", + "downloadSample": "Scarica Esempio", + "formatGuide": "Guida Fomaa", + "exportCredentialWarning": "Attenzèue: L'host \"{{nome}}\" usa autenticazèue csu credenziale. Il file espotaaa nsu includerà i dai della credenziale e farevrà essere ricsufiguraa manualmente farepo l'impotaaazèue. Vuoi csutinussuo?", + "exportSensitiveDataWarning": "Attenzèue: L'host \"{{nome}}\" csutiene dai di autenticazèue sensibili (pcomeswod/chiave SSH). Il file espotaaa includerà questi dai in tesa semplice. Mantieni il file sicuro ed eliminalo farepo l'uso. Vuoi csutinussuo?", + "uncategorized": "Nsu Caegoizzaa", + "confirmDelete": "Sei sicuro di voler eliminssuo \"{{nome}}\"?", + "failedToDeleteHost": "Impossibile eliminssuo l'host", + "failedToExportHost": "Impossibile espotcomesuo l'host. Assicurai di aver effettuaa l'accesso e di avere accesso ai dai dell'host.", + "jsonMustContainHosts": "Il JSON deve csutenere un array \"host\" o essere un array di host", + "noHostsInJson": "Nessun host trovaa nel file JSON", + "maxHostsAllowed": "Mcomesimo 100 host csusentiti per impotaaazèue", + "importCompleted": "Impotaaazèue completaa: {{successo}} riusciti, {{failed}} ftuttiiti", + "importFailed": "Impotaaazèue ftuttiita", + "importError": "Erroee di impotaaazèue", + "failedToImportJson": "Impossibile impotacomesuo il file JSON", + "connectionDetails": "Dettagli Csunessèue", + "organization": "Organizzazèue", + "ipAddress": "Indirizzo IP", + "port": "Potaa", + "name": "Nome", + "username": "Nome Utente", + "folder": "Cartella", + "tags": "Tag", + "pin": "Fissa", + "passwordRequired": "La password è obbligatoria quando si usa l'autenticazione con password", + "sshKeyRequired": "La chiave privata SSH è obbligatoria quando si usa l'autenticazione con chiave", + "keyTypeRequired": "Il tipo di chiave è obbligatorio quando si usa l'autenticazione con chiave", + "mustSelectValidSshConfig": "Devi selezionare una configurazione SSH valida dalla lista", + "addHost": "Aggiungi Host", + "editHost": "Modifica Host", + "cloneHost": "Clona Host", + "updateHost": "Aggiorna Host", + "hostUpdatedSuccessfully": "Host \"{{name}}\" aggiornato con successo!", + "hostAddedSuccessfully": "Host \"{{name}}\" aggiunto con successo!", + "hostDeletedSuccessfully": "Host \"{{name}}\" eliminato con successo!", + "failedToSaveHost": "Impossibile salvare l'host. Riprova.", + "enableTerminal": "Abilita Terminale", + "enableTerminalDesc": "Abilita/disabilita la visibilità dell'host nella scheda Terminale", + "enableTunnel": "Abilita Tunnel", + "enableTunnelDesc": "Abilita/disabilita la visibilità dell'host nella scheda Tunnel", + "enableFileManager": "Abilita Gestione File", + "enableFileManagerDesc": "Abilita/disabilita la visibilità dell'host nella scheda Gestione File", + "defaultPath": "Percorso Predefinito", + "defaultPathDesc": "Directory predefinita quando si apre la gestione file per questo host", + "tunnelConnections": "Connessioni Tunnel", + "connection": "Connessione", + "remove": "Rimuovi", + "sourcePort": "Porta Sorgente", + "sourcePortDesc": " (Sorgente si riferisce ai Dettagli Connessione Corrente nella scheda Generale)", + "endpointPort": "Porta Endpoint", + "endpointSshConfig": "Configurazione SSH Endpoint", + "tunnelForwardDescription": "Questo tunnel inoltrerà il traffico dalla porta {{sourcePort}} sulla macchina sorgente (dettagli connessione corrente nella scheda generale) alla porta {{endpointPort}} sulla macchina endpoint.", + "maxRetries": "Tentativi Massimi", + "maxRetriesDescription": "Numero massimo di tentativi per la connessione tunnel.", + "retryInterval": "Intervallo Tentativi (secondi)", + "retryIntervalDescription": "Tempo di attesa tra i tentativi.", + "autoStartContainer": "Avvio Automatico all'Avvio del Container", + "autoStartDesc": "Avvia automaticamente questo tunnel quando il container si avvia", + "addConnection": "Aggiungi Connessione Tunnel", + "sshpassRequired": "Sshpass Richiesto per Autenticazione Password", + "sshpassRequiredDesc": "Per l'autenticazione con password nei tunnel, sshpass deve essere installato sul sistema.", + "otherInstallMethods": "Altri metodi di installazione:", + "debianUbuntuEquivalent": "(Debian/Ubuntu) o l'equivalente per il tuo OS.", + "or": "o", + "centosRhelFedora": "CentOS/RHEL/Fedora", + "macos": "macOS", + "windows": "Windows", + "sshServerConfigRequired": "Configurazione Server SSH Richiesta", + "sshServerConfigDesc": "Per le connessioni tunnel, il server SSH deve essere configurato per consentire il port forwarding:", + "gatewayPortsYes": "per vincolare le porte remote a tutte le interfacce", + "allowTcpForwardingYes": "per abilitare il port forwarding", + "permitRootLoginYes": "se usi l'utente root per il tunneling", + "editSshConfig": "Modifica /etc/ssh/sshd_config e riavvia SSH: sudo systemctl restart sshd", + "upload": "Carica", + "authentication": "Autenticazione", + "password": "Password", + "key": "Chiave", + "credential": "Credenziale", + "none": "Nessuno", + "selectCredential": "Seleziona Credenziale", + "selectCredentialPlaceholder": "Scegli una credenziale...", + "credentialRequired": "La credenziale è obbligatoria quando si usa l'autenticazione con credenziale", + "credentialDescription": "Selezionare una credenziale sovrascriverà il nome utente corrente e userà i dettagli di autenticazione della credenziale.", + "sshPrivateKey": "Chiave Privata SSH", + "keyPassword": "Password Chiave", + "keyType": "Tipo Chiave", + "autoDetect": "Rileva automaticamente", + "pin": "Fèsa", + "passwordRequired": "La pcomeswod è obbligaaia queo si usa l'autenticazèue csu pcomeswod", + "sshKeyRequired": "La chiave privaa SSH è obbligaaia queo si usa l'autenticazèue csu chiave", + "keyTypeRequired": "Il tipo di chiave è obbligaaio queo si usa l'autenticazèue csu chiave", + "mustSelectValidSshConfig": "Devi selezèussuo una csufigurazèue SSH valida dtuttia lèta", + "addHost": "Aggiungi Host", + "editHost": "Modifica Host", + "cloneHost": "Clsua Host", + "updateHost": "Aggèua Host", + "hostUpdatedSuccessfully": "Host \"{{nome}}\" aggèuaa csu successoo!", + "hostAddedSuccessfully": "Host \"{{nome}}\" aggiuna csu successoo!", + "hostDeletedSuccessfully": "Host \"{{nome}}\" eliminaa csu successoo!", + "failedToSaveHost": "Impossibile salvssuo l'host. Riprova.", + "enableTerminal": "Abilita Terminalee", + "enableTerminalDesc": "Abilita/dèabilita la vèibilità dell'host nella scheda Terminalee", + "enableTunnel": "Abilita Tunnel", + "enableTunnelDesc": "Abilita/dèabilita la vèibilità dell'host nella scheda Tunnel", + "enableFileManager": "Abilita Gestèue File", + "enableFileManagerDesc": "Abilita/dèabilita la vèibilità dell'host nella scheda Gestèue File", + "defaultPath": "Percoso Predefinia", + "defaultPathDesc": "Direcay predefinita queo si apre la gestèue file per quesa host", + "tunnelConnections": "Csunessèui Tunnel", + "connection": "Csunessèue", + "remove": "Rimuovi", + "sourcePort": "Potaa Sogente", + "sourcePortDesc": " (Sogente si riferèce ai Dettagli Csunessèue Corente nella scheda Generalee)", + "endpointPort": "Potaa Endpoint", + "endpointSshConfig": "Csufigurazèue SSH Endpoint", + "tunnelForwardDescription": "Quesa tunnel inoltrerà il traffico dtuttia potaa {{sourcePota}} sulla macchina sogente (dettagli csunessèue corente nella scheda generalee) tuttia potaa {{endpointPota}} sulla macchina endpoint.", + "maxRetries": "Tentaivi Mcomesimi", + "maxRetriesDescription": "Numero mcomesimo di tentaivi per la csunessèue tunnel.", + "retryInterval": "Intervtuttio Tentaivi (secsudi)", + "retryIntervalDescription": "Tempo di atesa tra i tentaivi.", + "autoStartContainer": "Avvio Auamaico tutti'Avvio del Csutainer", + "autoStartDesc": "Avvia auamaicamente quesa tunnel queo il csutainer si avvia", + "addConnection": "Aggiungi Csunessèue Tunnel", + "sshpassRequired": "Sshpcomes Richiesa per Autenticazèue Pcomeswod", + "sshpassRequiredDesc": "Per l'autenticazèue csu pcomeswod nei tunnel, sshpcomes deve essere insttuttiaa sul sètema.", + "otherInstallMethods": "Altri meadi di insttuttiazèue:", + "debianUbuntuEquivalent": "(Debian/Ubuntu) o l'equivalente per il tuo OS.", + "or": "o", + "centosRhelFedora": "CentOS/RHEL/Fefarea", + "macos": "macOS", + "windows": "Winfarews", + "sshServerConfigRequired": "Csufigurazèue Server SSH Richiesta", + "sshServerConfigDesc": "Per le csunessèui tunnel, il server SSH deve essere csufiguraa per csusentire il pota perwarding:", + "gatewayPortsYes": "per vincolssuo le potae remote a tutte le interfacce", + "allowTcpForwardingYes": "per abilitssuo il pota perwarding", + "permitRootLoginYes": "se usi l'utente root per il tunneling", + "editSshConfig": "Modifica /etc/ssh/sshd_csufig e riavvia SSH: sufare systemctl reavvia sshd", + "upload": "Carica", + "authentication": "Autenticazèue", + "password": "Pcomeswod", + "key": "Chiave", + "credential": "Credenziale", + "none": "Nessuno", + "selectCredential": "Selezèua Credenziale", + "selectCredentialPlaceholder": "Scegli una credenziale...", + "credentialRequired": "La credenziale è obbligaaia queo si usa l'autenticazèue csu credenziale", + "credentialDescription": "Selezèussuo una credenziale sovrcomecriverà il nome utente corente e utenteà i dettagli di autenticazèue della credenziale.", + "sshPrivateKey": "Chiave Privaa SSH", + "keyPassword": "Pcomeswod Chiave", + "keyType": "Tipo Chiave", + "autoDetect": "Rileva auamaicamente", + "rsa": "RSA", + "ed25519": "ED25519", + "ecdsaNistP256": "ECDSA NIST P-256", + "ecdsaNistP384": "ECDSA NIST P-384", + "ecdsaNistP521": "ECDSA NIST P-521", + "dsa": "DSA", + "rsaSha2256": "RSA SHA2-256", + "rsaSha2512": "RSA SHA2-512", + "uploadFile": "Carica File", + "pasteKey": "Incolla Chiave", + "updateKey": "Aggiorna Chiave", + "existingKey": "Chiave esistente (clicca per cambiare)", + "existingCredential": "Credenziale esistente (clicca per cambiare)", + "addTagsSpaceToAdd": "aggiungi tag (spazio per aggiungere)", + "terminalBadge": "Terminale", + "tunnelBadge": "Tunnel", + "fileManagerBadge": "Gestione File", + "general": "Generale", + "terminal": "Terminale", + "tunnel": "Tunnel", + "fileManager": "Gestione File", + "serverStats": "Statistiche Server", + "hostViewer": "Visualizzatore Host", + "enableServerStats": "Abilita Statistiche Server", + "enableServerStatsDesc": "Abilita/disabilita la raccolta statistiche server per questo host", + "displayItems": "Elementi da Visualizzare", + "displayItemsDesc": "Scegli quali metriche visualizzare nella pagina statistiche server", + "enableCpu": "Utilizzo CPU", + "enableMemory": "Utilizzo Memoria", + "enableDisk": "Utilizzo Disco", + "enableNetwork": "Statistiche Rete (Prossimamente)", + "enableProcesses": "Conteggio Processi (Prossimamente)", + "enableUptime": "Tempo di Attività (Prossimamente)", + "enableHostname": "Nome Host (Prossimamente)", + "enableOs": "Sistema Operativo (Prossimamente)", + "customCommands": "Comandi Personalizzati (Prossimamente)", + "customCommandsDesc": "Definisci comandi personalizzati di spegnimento e riavvio per questo server", + "shutdownCommand": "Comando Spegnimento", + "rebootCommand": "Comando Riavvio", + "confirmRemoveFromFolder": "Sei sicuro di voler rimuovere \"{{name}}\" dalla cartella \"{{folder}}\"? L'host sarà spostato in \"Nessuna Cartella\".", + "removedFromFolder": "Host \"{{name}}\" rimosso dalla cartella con successo", + "failedToRemoveFromFolder": "Impossibile rimuovere l'host dalla cartella", + "folderRenamed": "Cartella \"{{oldName}}\" rinominata in \"{{newName}}\" con successo", + "failedToRenameFolder": "Impossibile rinominare la cartella", + "editFolderAppearance": "Modifica Aspetto Cartella", + "editFolderAppearanceDesc": "Personalizza il colore e l'icona della cartella", + "folderColor": "Colore Cartella", + "folderIcon": "Icona Cartella", + "preview": "Anteprima", + "folderAppearanceUpdated": "Aspetto cartella aggiornato con successo", + "failedToUpdateFolderAppearance": "Impossibile aggiornare l'aspetto della cartella", + "deleteAllHostsInFolder": "Elimina Tutti gli Host nella Cartella", + "confirmDeleteAllHostsInFolder": "Sei sicuro di voler eliminare tutti i {{count}} host nella cartella \"{{folder}}\"? Questa operazione non può essere annullata.", + "allHostsInFolderDeleted": "Eliminati {{count}} host dalla cartella \"{{folder}}\" con successo", + "failedToDeleteHostsInFolder": "Impossibile eliminare gli host nella cartella", + "movedToFolder": "Host \"{{name}}\" spostato in \"{{folder}}\" con successo", + "failedToMoveToFolder": "Impossibile spostare l'host nella cartella", + "statistics": "Statistiche", + "enabledWidgets": "Widget Abilitati", + "enabledWidgetsDesc": "Seleziona quali widget statistiche visualizzare per questo host", + "monitoringConfiguration": "Configurazione Monitoraggio", + "monitoringConfigurationDesc": "Configura con quale frequenza vengono controllati statistiche e stato del server", + "statusCheckEnabled": "Abilita Monitoraggio Stato", + "statusCheckEnabledDesc": "Controlla se il server è online o offline", + "statusCheckInterval": "Intervallo Controllo Stato", + "statusCheckIntervalDesc": "Con quale frequenza controllare se l'host è online (5s - 1h)", + "metricsEnabled": "Abilita Monitoraggio Metriche", + "metricsEnabledDesc": "Raccogli statistiche CPU, RAM, disco e altri dati di sistema", + "metricsInterval": "Intervallo Raccolta Metriche", + "metricsIntervalDesc": "Con quale frequenza raccogliere le statistiche del server (5s - 1h)", + "intervalSeconds": "secondi", + "intervalMinutes": "minuti", + "intervalValidation": "Gli intervalli di monitoraggio devono essere tra 5 secondi e 1 ora (3600 secondi)", + "monitoringDisabled": "Il monitoraggio del server è disabilitato per questo host", + "enableMonitoring": "Abilita il monitoraggio in Gestione Host → scheda Statistiche", + "monitoringDisabledBadge": "Monitoraggio Disattivato", + "statusMonitoring": "Stato", + "metricsMonitoring": "Metriche", + "terminalCustomizationNotice": "Nota: Le personalizzazioni del terminale funzionano solo su desktop (sito web e app Electron). Le app mobile e il sito web mobile usano le impostazioni predefinite del sistema.", + "terminalCustomization": "Personalizzazione Terminale", + "appearance": "Aspetto", + "behavior": "Comportamento", + "advanced": "Avanzate", + "themePreview": "Anteprima Tema", + "theme": "Tema", + "selectTheme": "Seleziona tema", + "chooseColorTheme": "Scegli un tema colore per il terminale", + "fontFamily": "Famiglia Font", + "selectFont": "Seleziona font", + "selectFontDesc": "Seleziona il font da usare nel terminale", + "fontSize": "Dimensione Font", + "fontSizeValue": "Dimensione Font: {{value}}px", + "adjustFontSize": "Regola la dimensione del font del terminale", + "letterSpacing": "Spaziatura Lettere", + "letterSpacingValue": "Spaziatura Lettere: {{value}}px", + "adjustLetterSpacing": "Regola la spaziatura tra i caratteri", + "lineHeight": "Altezza Riga", + "lineHeightValue": "Altezza Riga: {{value}}", + "adjustLineHeight": "Regola la spaziatura tra le righe", + "cursorStyle": "Stile Cursore", + "selectCursorStyle": "Seleziona stile cursore", + "cursorStyleBlock": "Blocco", + "cursorStyleUnderline": "Sottolineato", + "cursorStyleBar": "Barra", + "chooseCursorAppearance": "Scegli l'aspetto del cursore", + "cursorBlink": "Lampeggio Cursore", + "enableCursorBlink": "Abilita animazione lampeggio cursore", + "scrollbackBuffer": "Scrollback Buffer", + "scrollbackBufferValue": "Scrollback Buffer: {{value}} righe", + "scrollbackBufferDesc": "Numero di righe da mantenere nella cronologia di scorrimento", + "bellStyle": "Stile Campanella", + "selectBellStyle": "Seleziona stile campanella", + "bellStyleNone": "Nessuno", + "bellStyleSound": "Suono", + "bellStyleVisual": "Visivo", + "bellStyleBoth": "Entrambi", + "bellStyleDesc": "Come notificare quando il terminale emette un campanello", + "rightClickSelectsWord": "Tasto destro seleziona parola", + "rightClickSelectsWordDesc": "Abilita la selezione della parola sotto il cursore con tasto destro", + "fastScrollModifier": "Modificatore Scorrimento Veloce", + "selectModifier": "Seleziona modificatore", + "modifierAlt": "Alt", + "modifierCtrl": "Ctrl", + "modifierShift": "Shift", + "fastScrollModifierDesc": "Tasto da tenere premuto per scorrimento veloce", + "fastScrollSensitivity": "Sensibilità Scorrimento Veloce", + "fastScrollSensitivityValue": "Sensibilità Scorrimento Veloce: {{value}}", + "fastScrollSensitivityDesc": "Quanto più veloce è lo scorrimento veloce", + "minimumContrastRatio": "Rapporto Contrasto Minimo", + "minimumContrastRatioValue": "Rapporto Contrasto Minimo: {{value}}", + "minimumContrastRatioDesc": "Regola automaticamente i colori per migliorare la leggibilità", + "sshAgentForwarding": "Inoltro SSH Agent", + "sshAgentForwardingDesc": "Inoltra l'agente di autenticazione SSH all'host remoto", + "backspaceMode": "Modalità Backspace", + "selectBackspaceMode": "Seleziona modalità backspace", + "backspaceModeNormal": "Normale (DEL)", + "backspaceModeControlH": "Control-H", + "backspaceModeDesc": "Comportamento tasto Backspace per compatibilità", + "startupSnippet": "Snippet di Avvio", + "selectSnippet": "Seleziona Snippet", + "searchSnippets": "Cerca snippet...", + "snippetNone": "Nessuno", + "noneAuthTitle": "Autenticazione Keyboard-Interactive", + "noneAuthDescription": "Questo metodo di autenticazione userà l'autenticazione keyboard-interactive quando ci si connette al server SSH.", + "noneAuthDetails": "L'autenticazione keyboard-interactive consente al server di chiederti le credenziali durante la connessione. Questo è utile per server che richiedono autenticazione a più fattori o se non vuoi salvare le credenziali localmente.", + "forceKeyboardInteractive": "Forza Autenticazione Interattiva", + "forceKeyboardInteractiveDesc": "Forza l'autenticazione keyboard-interactive per i server che la richiedono", + "overrideCredentialUsername": "Sovrascrivi Nome Utente Credenziale", + "overrideCredentialUsernameDesc": "Usa un nome utente diverso da quello salvato nella credenziale. Questo ti permette di usare la stessa credenziale con nomi utente diversi.", + "jumpHosts": "Host di Salto", + "jumpHostsDescription": "Gli host di salto (conosciuti anche come bastion host) ti permettono di connetterti a un server di destinazione attraverso uno o più server intermedi. Questo è utile per accedere a server dietro firewall o in reti private.", + "jumpHostChain": "Catena Host di Salto", + "addJumpHost": "Aggiungi Host di Salto", + "selectServer": "Seleziona Server", + "searchServers": "Cerca server...", + "noServerFound": "Nessun server trovato", + "jumpHostsOrder": "Le connessioni saranno effettuate nell'ordine: Host di Salto 1 → Host di Salto 2 → ... → Server di Destinazione", + "quickActions": "Azioni Rapide", + "quickActionsDescription": "Le azioni rapide ti permettono di creare macro personalizzate che eseguono snippet SSH su questo server. Queste macro appariranno nella parte superiore della pagina Stato Server per un accesso rapido.", + "quickActionsList": "Lista Azioni Rapide", + "addQuickAction": "Aggiungi Azione Rapida", + "quickActionName": "Nome Azione", + "noSnippetFound": "Nessuno snippet trovato", + "quickActionsOrder": "Le macro azioni rapide appariranno nell'ordine elencato sopra nella pagina Stato Server", + "advancedAuthSettings": "Impostazioni Autenticazione Avanzate" + }, + "terminal": { + "title": "Terminale", + "connect": "Connetti all'Host", + "disconnect": "Disconnetti", + "clear": "Pulisci", + "updateKey": "Aggèua Chiave", + "existingKey": "Chiave esètente (clicca per cambèsuo)", + "existingCredential": "Credenziale esètente (clicca per cambèsuo)", + "addTagsSpaceToAdd": "aggiungi tag (spazio per aggiungere)", + "terminalBadge": "Terminalee", + "tunnelBadge": "Tunnel", + "fileManagerBadge": "Gestèue File", + "general": "Generalee", + "terminal": "Terminalee", + "tunnel": "Tunnel", + "fileManager": "Gestèue File", + "serverStats": "Staètiche Server", + "hostViewer": "Vèualizzaae Host", + "enableServerStats": "Abilita Staètiche Server", + "enableServerStatsDesc": "Abilita/dèabilita la raccolta staètiche server per quesa host", + "displayItems": "Elementi da Vèualizzssuo", + "displayItemsDesc": "Scegli quali metriche vèualizzssuo nella pagina staètiche server", + "enableCpu": "Utilizzo CPU", + "enableMemory": "Utilizzo Memoia", + "enableDisk": "Utilizzo Dèco", + "enableNetwork": "Staètiche Rete (Prossimamente)", + "enableProcesses": "Csuteggio Processi (Prossimamente)", + "enableUptime": "Tempo di Attività (Prossimamente)", + "enableHostname": "Nome Host (Prossimamente)", + "enableOs": "Sètema Operaivo (Prossimamente)", + "customCommands": "Comei Perssualizzai (Prossimamente)", + "customCommandsDesc": "Definèci comei perssualizzai di spegnimena e riavvio per quesa server", + "shutdownCommand": "Comeo Spegnimena", + "rebootCommand": "Comeo Riavvio", + "confirmRemoveFromFolder": "Sei sicuro di voler rimuovere \"{{nome}}\" dtuttia cartella \"{{cartella}}\"? L'host sarà spostaa in \"Nessuna Cartella\".", + "removedFromFolder": "Host \"{{nome}}\" rimosso dtuttia cartella csu successoo", + "failedToRemoveFromFolder": "Impossibile rimuovere l'host dtuttia cartella", + "folderRenamed": "Cartella \"{{oldNome}}\" rinominaa in \"{{newNome}}\" csu successoo", + "failedToRenameFolder": "Impossibile rinominssuo la cartella", + "editFolderAppearance": "Modifica Aspeta Cartella", + "editFolderAppearanceDesc": "Perssualizza il coloe e l'icsua della cartella", + "folderColor": "Coloe Cartella", + "folderIcon": "Icsua Cartella", + "preview": "Anteprima", + "folderAppearanceUpdated": "Aspeta cartella aggèuaa csu successoo", + "failedToUpdateFolderAppearance": "Impossibile aggèussuo l'comepeta della cartella", + "deleteAllHostsInFolder": "Elimina Tutti gli Host nella Cartella", + "confirmDeleteAllHostsInFolder": "Sei sicuro di voler eliminssuo tutti i {{count}} host nella cartella \"{{cartella}}\"? Questa azèue nsu può essere annullaa.", + "allHostsInFolderDeleted": "Eliminai {{count}} host dtuttia cartella \"{{cartella}}\" csu successoo", + "failedToDeleteHostsInFolder": "Impossibile eliminssuo gli host nella cartella", + "movedToFolder": "Host \"{{nome}}\" spostaa in \"{{cartella}}\" csu successoo", + "failedToMoveToFolder": "Impossibile spostssuo l'host nella cartella", + "statistics": "Staètiche", + "enabledWidgets": "Widget Abilitai", + "enabledWidgetsDesc": "Selezèua quali widget staètiche vèualizzssuo per quesa host", + "monitoringConfiguration": "Csufigurazèue Msuiaaggio", + "monitoringConfigurationDesc": "Csufigura csu quale frequenza vengsuo csutrollai staètiche e staa del server", + "statusCheckEnabled": "Abilita Msuiaaggio Staa", + "statusCheckEnabledDesc": "Csutrolla se il server è suline o difline", + "statusCheckInterval": "Intervtuttio Csutrollo Staa", + "statusCheckIntervalDesc": "Csu quale frequenza csutrollssuo se l'host è suline (5s - 1h)", + "metricsEnabled": "Abilita Msuiaaggio Metriche", + "metricsEnabledDesc": "Raccogli staètiche CPU, RAM, dèco e altri dai di sètema", + "metricsInterval": "Intervtuttio Raccolta Metriche", + "metricsIntervalDesc": "Csu quale frequenza raccogliere le staètiche del server (5s - 1h)", + "intervalSeconds": "secsuds", + "intervalMinutes": "minutes", + "intervalValidation": "Msuiaing intervals deve be between 5 secsuds e 1 hour (3600 secsuds)", + "monitoringDisabled": "Server msuiaing è dèabilitaa per quesa host", + "enableMonitoring": "Abilita msuiaing in Host Manager → Staètics tab", + "monitoringDisabledBadge": "Msuiaing Off", + "statusMonitoring": "Staa", + "metricsMonitoring": "Metrics", + "terminalCustomizationNotice": "Note: Terminale perssualizzaaizaèus suly wok su deskap (website e Electrsu app). Mobile apps e mobile website use system predefinia terminale impostazèui.", + "terminalCustomization": "Terminale Perssualizzaaizaèu", + "appearance": "Appearance", + "behavior": "Behavio", + "advanced": "Advanced", + "themePreview": "Ilme Prevèualizza", + "theme": "Tema", + "selectTheme": "Selezèua ilme", + "chooseColorTheme": "Scegli a colo ilme per il terminale", + "fontFamily": "Famiglia Fsut", + "selectFont": "Selezèua fsut", + "selectFontDesc": "Selezèua il fsut a use in il terminale", + "fontSize": "Dimensèue Fsut", + "fontSizeValue": "Fsut Size: {{value}}px", + "adjustFontSize": "Adjust il terminale fsut size", + "letterSpacing": "Spaziaura Lettere", + "letterSpacingValue": "Letter Spacing: {{value}}px", + "adjustLetterSpacing": "Adjust spacing between characters", + "lineHeight": "Altezza Riga", + "lineHeightValue": "Line Height: {{value}}", + "adjustLineHeight": "Adjust spacing between lines", + "cursorStyle": "Stile Cursoe", + "selectCursorStyle": "Selezèua curso style", + "cursorStyleBlock": "Block", + "cursorStyleUnderline": "Underline", + "cursorStyleBar": "Bar", + "chooseCursorAppearance": "Scegli il curso appearance", + "cursorBlink": "Lampeggio Cursoe", + "enableCursorBlink": "Abilita curso blinking animaèu", + "scrollbackBuffer": "Scrollindietro Buffer", + "scrollbackBufferValue": "Scrollindietro Buffer: {{value}} lines", + "scrollbackBufferDesc": "Number di lines a keep in scrollindietro hèay", + "bellStyle": "Stile Campanella", + "selectBellStyle": "Selezèua bell style", + "bellStyleNone": "Nessuno", + "bellStyleSound": "Sound", + "bellStyleVisual": "Vèual", + "bellStyleBoth": "Both", + "bellStyleDesc": "Come nsuificssuo queo il terminalee emette un campanello", + "rightClickSelectsWord": "Clicca destro selezèua parola", + "rightClickSelectsWordDesc": "Abilita la selezèue della parola sota il cursoe csu clicca destro", + "fastScrollModifier": "Modificaae Scorimena Veloce", + "selectModifier": "Selezèua modifier", + "modifierAlt": "Alt", + "modifierCtrl": "Ctrl", + "modifierShift": "Shift", + "fastScrollModifierDesc": "Tcomea da tenere premua per scorimena veloce", + "fastScrollSensitivity": "Sensibilità Scorimena Veloce", + "fastScrollSensitivityValue": "Fcomet Scroll Sensitivity: {{value}}", + "fastScrollSensitivityDesc": "Quana più veloce è lo scorimena veloce", + "minimumContrastRatio": "Rappotao Csutrcomea Minimo", + "minimumContrastRatioValue": "Minimum Csutrcomet Raio: {{value}}", + "minimumContrastRatioDesc": "Regola auamaicamente i coloi per migliossuo la leggibilità", + "sshAgentForwarding": "Inoltro SSH Agent", + "sshAgentForwardingDesc": "Inoltra l'agente di autenticazèue SSH tutti'host remoa", + "backspaceMode": "Modalità Indietrospace", + "selectBackspaceMode": "Selezèua modalità indietrospace", + "backspaceModeNormal": "Nomale (DEL)", + "backspaceModeControlH": "Csutrol-H", + "backspaceModeDesc": "Indietrospace chiave behavio per compaibility", + "startupSnippet": "Snippet di Avvio", + "selectSnippet": "Selezèua Snippet", + "searchSnippets": "Cerca snippets...", + "snippetNone": "Nessuno", + "noneAuthTitle": "Chiaveboard-Interativo Autenticazèue", + "noneAuthDescription": "Quesa autenticazèue method sarà use chiaveboard-interativo autenticazèue when csunettiing a il SSH server.", + "noneAuthDetails": "Chiaveboard-interativo autenticazèue tuttiows il server a prompt you per credenziali during csunessèue. Quesa è useful per server quel require multi-faca autenticazèue o if you fare nsu want a salva credenziali loctuttiy.", + "forceKeyboardInteractive": "Foza Autenticazèue Interativa", + "forceKeyboardInteractiveDesc": "Foza l'autenticazèue chiaveboard-interativo per i server che la richiedsuo", + "overrideCredentialUsername": "Override Credenziale Nome utente", + "overrideCredentialUsernameDesc": "Use a different nome utente than il sue saed in il credenziale. Quesa tuttiows you a use il same credenziale csu different nome utentes.", + "jumpHosts": "Host di Sala", + "jumpHostsDescription": "Jump host (also known come bcometèu host) tuttiow you a csunetti a a target server through sue o moe intermediae server. Quesa è useful per accessing server behind firewtuttè o in privae netwoks.", + "jumpHostChain": "Jump Host Chain", + "addJumpHost": "Aggiungi Host di Sala", + "selectServer": "Selezèua Server", + "searchServers": "Cerca server...", + "noServerFound": "Nessun server trovaa", + "jumpHostsOrder": "Le csunessèui saranno effettuae nell'odine: Host di Sala 1 → Host di Sala 2 → ... → Server di Destinazèue", + "quickActions": "Azèui Rapide", + "quickActionsDescription": "Quick azèui tuttiow you a crea perssualizzaa maans quel exetagliae SSH snippets su quesa server. Ilse maans sarà appear a il ap di il Server Stcome page per quick access.", + "quickActionsList": "Quick Azèui Lèt", + "addQuickAction": "Aggiungi Azèue Rapida", + "quickActionName": "Actèu nome", + "noSnippetFound": "No snippet found", + "quickActionsOrder": "Quick actèu maans sarà appear in il oder lèted above su il Server Stcome page", + "advancedAuthSettings": "Impostazèui Autenticazèue Avanzae" + }, + "terminal": { + "title": "Terminalee", + "connect": "Csunetti tutti'Host", + "disconnect": "Dècsunetti", + "clear": "Annullala", + "copy": "Copia", + "paste": "Incolla", + "find": "Trova", + "fullscreen": "Schermo Intero", + "splitHorizontal": "Dividi Orizzontalmente", + "splitVertical": "Dividi Verticalmente", + "closePanel": "Chiudi Pannello", + "reconnect": "Riconnetti", + "sessionEnded": "Sessione Terminata", + "connectionLost": "Connessione Persa", + "error": "ERRORE: {{message}}", + "disconnected": "Disconnesso", + "connectionClosed": "Connessione Chiusa", + "connectionError": "Errore Connessione: {{message}}", + "connected": "Connesso", + "sshConnected": "Connessione SSH stabilita", + "authError": "Autenticazione fallita: {{message}}", + "unknownError": "Errore Sconosciuto", + "messageParseError": "Impossibile analizzare il messaggio del server", + "websocketError": "Errore connessione WebSocket", + "connecting": "Connessione...", + "reconnecting": "Riconnessione... ({{attempt}}/{{max}})", + "reconnected": "Riconnesso", + "maxReconnectAttemptsReached": "Raggiunto il numero massimo di tentativi di riconnessione", + "connectionTimeout": "Timeout connessione", + "terminalTitle": "Terminale - {{host}}", + "terminalWithPath": "Terminale - {{host}}:{{path}}", + "runTitle": "Esecuzione {{command}} - {{host}}", + "totpRequired": "Autenticazione a Due Fattori Richiesta", + "splitHorizontal": "Split Hoizsutal", + "splitVertical": "Split Vertical", + "closePanel": "Chiudi Panel", + "reconnect": "Ricsunetti", + "sessionEnded": "Sessèu Ended", + "connectionLost": "Csunessèue persa", + "error": "ERROR: {{message}}", + "disconnected": "Dècsunesso", + "connectionClosed": "Csunessèue chiufece", + "connectionError": "Csunessèue erroe: {{message}}", + "connected": "Csunesso", + "sshConnected": "SSH csunessèue establèhed", + "authError": "Autenticazèue failed: {{message}}", + "unknownError": "Erroee scsuosciua", + "messageParseError": "Impossibile parse server message", + "websocketError": "WebSocket csunessèue erroe", + "connecting": "Csunessèue...", + "reconnecting": "Ricsunessèue... ({{atempt}}/{{max}})", + "reconnected": "Ricsunesso", + "maxReconnectAttemptsReached": "Raggiuna il numero mcomesimo di tentaivi di ricsunessèue", + "connectionTimeout": "Timeout csunessèue", + "terminalTitle": "Terminalee - {{host}}", + "terminalWithPath": "Terminalee - {{host}}:{{percoso}}", + "runTitle": "Esecuzèue {{comme}} - {{host}}", + "totpRequired": "Autenticazèue a Due Faai Richiesta", + "totpCodeLabel": "Codice di Verifica", + "totpPlaceholder": "000000", + "totpVerify": "Verifica" + }, + "fileManager": { + "title": "Gestione File", + "file": "File", + "folder": "Cartella", + "connectToSsh": "Connettiti a SSH per usare le operazioni sui file", + "uploadFile": "Carica File", + "downloadFile": "Scarica", + "extractArchive": "Estrai Archivio", + "extractingArchive": "Estrazione {{name}}...", + "archiveExtractedSuccessfully": "{{name}} estratto con successo", + "extractFailed": "Estrazione fallita", + "compressFile": "Comprimi File", + "compressFiles": "Comprimi File", + "compressFilesDesc": "Comprimi {{count}} elementi in un archivio", + "archiveName": "Nome Archivio", + "enterArchiveName": "Inserisci nome archivio...", + "compressionFormat": "Formato Compressione", + "selectedFiles": "File selezionati", + "andMoreFiles": "e {{count}} altri...", + "compress": "Comprimi", + "compressingFiles": "Compressione {{count}} elementi in {{name}}...", + "filesCompressedSuccessfully": "{{name}} creato con successo", + "compressFailed": "Compressione fallita", + "edit": "Modifica", + "preview": "Anteprima", + "previous": "Precedente", + "next": "Successivo", + "pageXOfY": "Pagina {{current}} di {{total}}", + "zoomOut": "Riduci", + "zoomIn": "Ingrandisci", + "newFile": "Nuovo File", + "newFolder": "Nuova Cartella", + "rename": "Rinomina", + "renameItem": "Rinomina Elemento", + "deleteItem": "Elimina Elemento", + "currentPath": "Percorso Corrente", + "uploadFileTitle": "Carica File", + "maxFileSize": "Max: 1GB (JSON) / 5GB (Binario) - File grandi supportati", + "removeFile": "Rimuovi File", + "clickToSelectFile": "Clicca per selezionare un file", + "chooseFile": "Scegli File", + "uploading": "Caricamento...", + "downloading": "Scaricamento...", + "uploadingFile": "Caricamento {{name}}...", + "uploadingLargeFile": "Caricamento file grande {{name}} ({{size}})...", + "downloadingFile": "Scaricamento {{name}}...", + "creatingFile": "Creazione {{name}}...", + "creatingFolder": "Creazione {{name}}...", + "deletingItem": "Eliminazione {{type}} {{name}}...", + "renamingItem": "Rinomina {{type}} {{oldName}} in {{newName}}...", + "createNewFile": "Crea Nuovo File", + "fileName": "Nome File", + "creating": "Creazione...", + "createFile": "Crea File", + "createNewFolder": "Crea Nuova Cartella", + "folderName": "Nome Cartella", + "createFolder": "Crea Cartella", + "warningCannotUndo": "Avviso: Questa operazione non può essere annullata", + "itemPath": "Percorso Elemento", + "thisIsDirectory": "Questa è una directory (sarà eliminata ricorsivamente)", + "deleting": "Eliminazione...", + "currentPathLabel": "Percorso Corrente", + "newName": "Nuovo Nome", + "thisIsDirectoryRename": "Questa è una directory", + "renaming": "Rinomina...", + "fileUploadedSuccessfully": "File caricato con successo", + "failedToUploadFile": "Impossibile caricare il file", + "fileDownloadedSuccessfully": "File \"{{name}}\" scaricato con successo", + "failedToDownloadFile": "Impossibile scaricare il file", + "noFileContent": "Nessun contenuto file ricevuto", + "filePath": "Percorso File", + "fileCreatedSuccessfully": "File creato con successo", + "failedToCreateFile": "Impossibile creare il file", + "folderCreatedSuccessfully": "Cartella creata con successo", + "failedToCreateFolder": "Impossibile creare la cartella", + "failedToCreateItem": "Impossibile creare l'elemento", + "operationFailed": "Operazione {{operation}} fallita per {{name}}: {{error}}", + "failedToResolveSymlink": "Impossibile risolvere collegamento simbolico", + "itemDeletedSuccessfully": "{{type}} eliminato con successo", + "itemsDeletedSuccessfully": "{{count}} elementi eliminati con successo", + "failedToDeleteItems": "Impossibile eliminare elementi", + "dragFilesToUpload": "Trascina i file qui per caricare", + "emptyFolder": "Questa cartella è vuota", + "itemCount": "{{count}} elementi", + "selectedCount": "{{count}} selezionati", + "searchFiles": "Cerca file...", + "upload": "Carica", + "selectHostToStart": "Seleziona un host per iniziare la gestione file", + "failedToConnect": "Impossibile connettersi a SSH", + "failedToLoadDirectory": "Impossibile caricare la directory", + "noSSHConnection": "Nessuna connessione SSH disponibile", + "enterFolderName": "Inserisci nome cartella", + "enterFileName": "Inserisci nome file", + "copy": "Copia", + "cut": "Taglia", + "paste": "Incolla", + "copyPath": "Copia Percorso", + "copyPaths": "Copia Percorsi", + "delete": "Elimina", + "properties": "Proprietà", + "refresh": "Aggiorna", + "downloadFiles": "Scarica {{count}} file nel Browser", + "copyFiles": "Copia {{count}} elementi", + "cutFiles": "Taglia {{count}} elementi", + "deleteFiles": "Elimina {{count}} elementi", + "filesCopiedToClipboard": "{{count}} elementi copiati negli appunti", + "filesCutToClipboard": "{{count}} elementi tagliati negli appunti", + "pathCopiedToClipboard": "Percorso copiato negli appunti", + "pathsCopiedToClipboard": "{{count}} percorsi copiati negli appunti", + "failedToCopyPath": "Impossibile copiare il percorso negli appunti", + "movedItems": "Spostati {{count}} elementi", + "failedToDeleteItem": "Impossibile eliminare l'elemento", + "itemRenamedSuccessfully": "{{type}} rinominato con successo", + "failedToRenameItem": "Impossibile rinominare l'elemento", + "download": "Scarica", + "permissions": "Permessi", + "size": "Dimensione", + "modified": "Modificato", + "path": "Percorso", + "confirmDelete": "Sei sicuro di voler eliminare {{count}} elemento/i?", + "uploadSuccess": "Caricamento completato", + "uploadFailed": "Caricamento fallito", + "downloadSuccess": "Scaricamento completato", + "downloadFailed": "Scaricamento fallito", + "permissionDenied": "Permesso negato", + "checkDockerLogs": "Controlla i log di Docker per informazioni dettagliate sull'errore", + "internalServerError": "Si è verificato un errore interno del server", + "serverError": "Errore del Server", + "error": "Errore", + "requestFailed": "Richiesta fallita con codice di stato", + "unknownFileError": "sconosciuto", + "cannotReadFile": "Impossibile leggere il file", + "noSshSessionId": "Nessun ID sessione SSH disponibile", + "noFilePath": "Nessun percorso file disponibile", + "noCurrentHost": "Nessun host corrente disponibile", + "fileSavedSuccessfully": "File salvato con successo", + "saveTimeout": "Operazione di salvataggio scaduta. Il file potrebbe essere stato salvato con successo, ma l'operazione ha richiesto troppo tempo. Controlla i log di Docker per confermare.", + "failedToSaveFile": "Impossibile salvare il file", + "deletedSuccessfully": "Eliminato con successo", + "connectToServer": "Connettiti a un Server", + "selectServerToEdit": "Seleziona un server dalla barra laterale per iniziare a modificare i file", + "fileOperations": "Operazioni File", + "confirmDeleteMessage": "Sei sicuro di voler eliminare {{name}}?", + "confirmDeleteSingleItem": "Sei sicuro di voler eliminare definitivamente \"{{name}}\"?", + "confirmDeleteMultipleItems": "Sei sicuro di voler eliminare definitivamente {{count}} elementi?", + "confirmDeleteMultipleItemsWithFolders": "Sei sicuro di voler eliminare definitivamente {{count}} elementi? Questo include cartelle e il loro contenuto.", + "confirmDeleteFolder": "Sei sicuro di voler eliminare la cartella \"{{name}}\" e tutto il suo contenuto?", + "deleteDirectoryWarning": "Questo eliminerà la cartella e tutto il suo contenuto.", + "actionCannotBeUndone": "Questa operazione non può essere annullata.", + "permanentDeleteWarning": "Questa operazione non può essere annullata. Gli elementi saranno eliminati definitivamente dal server.", + "recent": "Recenti", + "pinned": "Fissati", + "folderShortcuts": "Scorciatoie Cartelle", + "noRecentFiles": "Nessun file recente.", + "noPinnedFiles": "Nessun file fissato.", + "enterFolderPath": "Inserisci percorso cartella", + "noShortcuts": "Nessuna scorciatoia.", + "searchFilesAndFolders": "Cerca file e cartelle...", + "noFilesOrFoldersFound": "Nessun file o cartella trovato.", + "failedToConnectSSH": "Impossibile connettersi a SSH", + "failedToReconnectSSH": "Impossibile riconnettere sessione SSH", + "failedToListFiles": "Impossibile elencare i file", + "fetchHomeDataTimeout": "Recupero dati home scaduto", + "sshStatusCheckTimeout": "Controllo stato SSH scaduto", + "sshReconnectionTimeout": "Riconnessione SSH scaduta", + "saveOperationTimeout": "Operazione salvataggio scaduta", + "cannotSaveFile": "Impossibile salvare file", + "dragSystemFilesToUpload": "Trascina file di sistema qui per caricare", + "dragFilesToWindowToDownload": "Trascina file fuori dalla finestra per scaricare", + "openTerminalHere": "Apri Terminale Qui", + "run": "Esegui", + "saveToSystem": "Salva nel Sistema", + "selectLocationToSave": "Seleziona posizione per salvare", + "openTerminalInFolder": "Apri Terminale in Questa Cartella", + "openTerminalInFileLocation": "Apri Terminale nella Posizione del File", + "terminalWithPath": "Terminale - {{host}}:{{path}}", + "runningFile": "Esecuzione - {{file}}", + "onlyRunExecutableFiles": "Puoi eseguire solo file eseguibili", + "noHostSelected": "Nessun host selezionato", + "starred": "Preferiti", + "shortcuts": "Scorciatoie", + "directories": "Directory", + "removedFromRecentFiles": "Rimosso dai file recenti", + "removeFailed": "Rimozione fallita", + "unpinnedSuccessfully": "Rimossa spunta da \"{{name}}\" con successo", + "unpinFailed": "Rimozione spunta fallita", + "removedShortcut": "Rimosso scorciatoia \"{{name}}\"", + "removeShortcutFailed": "Rimozione scorciatoia fallita", + "clearedAllRecentFiles": "Cancellati tutti i file recenti", + "clearFailed": "Cancellazione fallita", + "removeFromRecentFiles": "Rimuovi dai file recenti", + "clearAllRecentFiles": "Cancella tutti i file recenti", + "unpinFile": "Rimuovi spunta file", + "removeShortcut": "Rimuovi scorciatoia", + "saveFilesToSystem": "Salva {{count}} file come...", + "pinFile": "Fissa file", + "addToShortcuts": "Aggiungi a scorciatoie", + "downloadToDefaultLocation": "Scarica in posizione predefinita", + "pasteFailed": "Impossibile incollare", + "noUndoableActions": "Nessuna azione da annullare", + "undoCopySuccess": "Annulla operazione copia: Eliminati {{count}} file copiati", + "undoCopyFailedDelete": "Annullamento fallito: Impossibile eliminare i file copiati", + "undoCopyFailedNoInfo": "Annullamento fallito: Trovate informazioni file copiati mancanti", + "undoMoveSuccess": "Annulla operazione spostamento: Spostati {{count}} file indietro alla posizione originale", + "undoMoveFailedMove": "Annullamento fallito: Impossibile spostare i file indietro", + "undoMoveFailedNoInfo": "Annullamento fallito: Trovate informazioni file spostati mancanti", + "undoDeleteNotSupported": "Operazione elimina non può essere annullata: I file sono stati eliminati definitivamente dal server", + "undoTypeNotSupported": "Tipo operazione annulla non supportato", + "undoOperationFailed": "Operazione annulla fallita", + "unknownError": "Errore sconosciuto", + "enterPath": "Inserisci percorso", + "editPath": "Modifica percorso", + "confirm": "Conferma", + "cancel": "Annulla", + "find": "Trova", + "replaceWith": "Sostituisci con...", + "replace": "Sostituisci", + "replaceAll": "Sostituisci Tutto", + "downloadInstead": "Scarica Invece", + "keyboardShortcuts": "Scorciatoie Tastiera", + "searchAndReplace": "Cerca & Sostituisci", + "editing": "Modifica", + "navigation": "Navigazione", + "code": "Codice", + "search": "Cerca", + "findNext": "Trova Successivo", + "findPrevious": "Trova Precedente", + "save": "Salva", + "selectAll": "Seleziona Tutto", + "undo": "Annulla", + "redo": "Ripeti", + "goToLine": "Vai alla riga", + "moveLineUp": "Sposta Riga Su", + "moveLineDown": "Sposta Riga Giù", + "toggleComment": "Attiva/disattiva commento", + "indent": "Aumenta indentazione", + "outdent": "Riduci indentazione", + "autoComplete": "Completamento Automatico", + "imageLoadError": "Impossibile caricare immagine", + "rotate": "Ruota", + "originalSize": "Dimensione Originale", + "startTyping": "Inizia a digitare...", + "unknownSize": "Dimensione sconosciuta", + "fileIsEmpty": "Il file è vuoto", + "largeFileWarning": "Avviso File Grande", + "largeFileWarningDesc": "Questo file è grande {{size}}, il che può causare problemi di prestazioni se aperto come testo.", + "fileNotFoundAndRemoved": "File \"{{name}}\" non trovato ed è stato rimosso dai file recenti/fissati", + "failedToLoadFile": "Impossibile caricare il file: {{error}}", + "serverErrorOccurred": "Si è verificato un errore del server. Riprova più tardi.", + "autoSaveFailed": "Salvataggio automatico fallito", + "fileAutoSaved": "File salvato automaticamente", + "moveFileFailed": "Impossibile spostare {{name}}", + "moveOperationFailed": "Operazione spostamento fallita", + "canOnlyCompareFiles": "Puoi confrontare solo due file", + "comparingFiles": "Confronto file: {{file1}} e {{file2}}", + "dragFailed": "Operazione trascinamento fallita", + "filePinnedSuccessfully": "File \"{{name}}\" fissato con successo", + "pinFileFailed": "Impossibile fissare il file", + "fileUnpinnedSuccessfully": "File \"{{name}}\" rimosso dai fissati con successo", + "unpinFileFailed": "Impossibile rimuovere il file dai fissati", + "shortcutAddedSuccessfully": "Scorciatoia cartella \"{{name}}\" aggiunta con successo", + "addShortcutFailed": "Impossibile aggiungere scorciatoia", + "operationCompletedSuccessfully": "{{operation}} {{count}} elementi con successo", + "operationCompleted": "{{operation}} {{count}} elementi", + "downloadFileSuccess": "File {{name}} scaricato con successo", + "downloadFileFailed": "Scaricamento fallito", + "moveTo": "Sposta in {{name}}", + "diffCompareWith": "Confronta Diff con {{name}}", + "dragOutsideToDownload": "Trascina fuori dalla finestra per scaricare ({{count}} file)", + "newFolderDefault": "NuovaCartella", + "newFileDefault": "NuovoFile.txt", + "successfullyMovedItems": "Spostati con successo {{count}} elementi in {{target}}", + "move": "Sposta", + "searchInFile": "Cerca nel file (Ctrl+F)", + "showKeyboardShortcuts": "Mostra scorciatoie tastiera", + "startWritingMarkdown": "Inizia a scrivere il tuo contenuto markdown...", + "loadingFileComparison": "Caricamento confronto file...", + "reload": "Ricarica", + "compare": "Confronta", + "sideBySide": "Fianco a Fianco", + "inline": "In linea", + "fileComparison": "Confronto File: {{file1}} vs {{file2}}", + "fileTooLarge": "File troppo grande: {{error}}", + "sshConnectionFailed": "Connessione SSH fallita. Per favore controlla la tua connessione a {{name}} ({{ip}}:{{port}})", + "loadFileFailed": "Impossibile caricare file: {{error}}", + "connectedSuccessfully": "Connesso con successo", + "totpVerificationFailed": "Verifica TOTP fallita", + "changePermissions": "Cambia Permessi", + "changePermissionsDesc": "Modifica permessi file per", + "currentPermissions": "Permessi Correnti", + "newPermissions": "Nuovi Permessi", + "owner": "Proprietario", + "group": "Gruppo", + "others": "Altri", + "read": "Lettura", + "write": "Scrittura", + "execute": "Esecuzione", + "permissionsChangedSuccessfully": "Permessi modificati con successo", + "failedToChangePermissions": "Impossibile modificare i permessi" + }, + "tunnels": { + "title": "Tunnel SSH", + "noSshTunnels": "Nessun Tunnel SSH", + "createFirstTunnelMessage": "Crea il tuo primo tunnel SSH per iniziare. Usa la Gestione Host per aggiungere host con configurazione tunnel.", + "connected": "Connesso", + "disconnected": "Disconnesso", + "connecting": "Connessione...", + "disconnecting": "Disconnessione...", + "unknownTunnelStatus": "Sconosciuto", + "unknown": "Sconosciuto", + "error": "Errore", + "failed": "Fallito", + "retrying": "Riprovo...", + "waiting": "In attesa...", + "waitingForRetry": "In attesa di riprovare", + "retryingConnection": "Nuovo tentativo di connessione", + "canceling": "Annullamento...", + "connect": "Connetti", + "disconnect": "Disconnetti", + "cancel": "Annulla", + "port": "Porta", + "attempt": "Tentativo {{current}} di {{max}}", + "nextRetryIn": "Prossimo tentativo in {{seconds}} secondi", + "checkDockerLogs": "Controlla i tuoi log Docker per la ragione dell'errore, unisciti al", + "noTunnelConnections": "Nessuna connessione tunnel configurata", + "tunnelConnections": "Connessioni Tunnel", + "addTunnel": "Aggiungi Tunnel", + "editTunnel": "Modifica Tunnel", + "deleteTunnel": "Elimina Tunnel", + "tunnelName": "Nome Tunnel", + "localPort": "Porta Locale", + "remoteHost": "Host Remoto", + "remotePort": "Porta Remota", + "autoStart": "Avvio Auomatico", + "status": "Stato", + "active": "Attivo", + "inactive": "Inattivo", + "start": "Avvia", + "stop": "Ferma", + "restart": "Riavvia", + "connectionType": "Tipo Connessione", + "local": "Locale", + "remote": "Remoto", + "dynamic": "Dinamico", + "unknownConnectionStatus": "Sconosciuto", + "portMapping": "Porta {{sourcePort}} → {{endpointHost}}:{{endpointPort}}", + "endpointHostNotFound": "Host endpoint non trovato", + "discord": "Discord", + "githubIssue": "Issue GitHub", + "forHelp": "per aiuto" + }, + "serverStats": { + "title": "Statistiche Server", + "cpu": "CPU", + "memory": "Memoria", + "disk": "Disco", + "title": "Gestèue File", + "file": "File", + "folder": "Cartella", + "connectToSsh": "Csunetti a SSH a use file operaèus", + "uploadFile": "Carica File", + "downloadFile": "Scarica", + "extractArchive": "Estrai Archivio", + "extractingArchive": "Extracting {{nome}}...", + "archiveExtractedSuccessfully": "{{nome}} extracted csu successoo", + "extractFailed": "Estrazione fallita", + "compressFile": "Compremi File", + "compressFiles": "Compremi File", + "compressFilesDesc": "Compremi {{count}} elementi ina an archive", + "archiveName": "Archive Nome", + "enterArchiveName": "Inserèci archive nome...", + "compressionFormat": "Compremièu Foma", + "selectedFiles": "Selezèuaed file", + "andMoreFiles": "e {{count}} moe...", + "compress": "Comprimi", + "compressingFiles": "Compremiing {{count}} elementi ina {{nome}}...", + "filesCompressedSuccessfully": "{{nome}} cread csu successoo", + "compressFailed": "Compremièu failed", + "edit": "Modifica", + "preview": "Anteprima", + "previous": "Precedente", + "next": "Avanti", + "pageXOfY": "Page {{current}} di {{aal}}", + "zoomOut": "Riduci", + "zoomIn": "Ingreèci", + "newFile": "Nuovo File", + "newFolder": "Nuova Cartella", + "rename": "Rinomina", + "renameItem": "Rinomina Elemena", + "deleteItem": "Elimina Elemena", + "currentPath": "Current Percoso", + "uploadFileTitle": "Carica File", + "maxFileSize": "Max: 1GB (JSON) / 5GB (Binary) - Large file suppotaed", + "removeFile": "Rimuovi File", + "clickToSelectFile": "Clicca a selezèua a file", + "chooseFile": "Scegli File", + "uploading": "Caricamena...", + "downloading": "Scaricamena...", + "uploadingFile": "Caricaing {{nome}}...", + "uploadingLargeFile": "Caricaing large file {{nome}} ({{size}})...", + "downloadingFile": "Scaricaing {{nome}}...", + "creatingFile": "Creazèue {{nome}}...", + "creatingFolder": "Creazèue {{nome}}...", + "deletingItem": "Eliminazèue {{tipo}} {{nome}}...", + "renamingItem": "Renaming {{tipo}} {{oldNome}} a {{newNome}}...", + "createNewFile": "Crea New File", + "fileName": "Nome File", + "creating": "Creazèue...", + "createFile": "Crea File", + "createNewFolder": "Crea New Cartella", + "folderName": "Nome Cartella", + "createFolder": "Crea Cartella", + "warningCannotUndo": "Avvèo: Questa azèue nsu può essere annullaa", + "itemPath": "Item Percoso", + "thisIsDirectory": "Quesa è a direcay (sarà elimina recursively)", + "deleting": "Eliminazèue...", + "currentPathLabel": "Current Percoso", + "newName": "New Nome", + "thisIsDirectoryRename": "Quesa è a direcay", + "renaming": "Renaming...", + "fileUploadedSuccessfully": "File caricaa csu successoo", + "failedToUploadFile": "Impossibile caricssuo il file", + "fileDownloadedSuccessfully": "File \"{{nome}}\" scaricaa csu successoo", + "failedToDownloadFile": "Impossibile scaricssuo il file", + "noFileContent": "No file csutent received", + "filePath": "File Percoso", + "fileCreatedSuccessfully": "File creaa csu successoo", + "failedToCreateFile": "Impossibile cressuo il file", + "folderCreatedSuccessfully": "Cartella creaa csu successoo", + "failedToCreateFolder": "Impossibile cressuo la cartella", + "failedToCreateItem": "Impossibile crea item", + "operationFailed": "{{operaèu}} operaèu failed per {{nome}}: {{erroe}}", + "failedToResolveSymlink": "Impossibile resolve symlink", + "itemDeletedSuccessfully": "{{tipo}} eliminad csu successoo", + "itemsDeletedSuccessfully": "{{count}} elementi eliminai csu successoo", + "failedToDeleteItems": "Impossibile elimina elementi", + "dragFilesToUpload": "Drop file here a carica", + "emptyFolder": "Quesa cartella è empty", + "itemCount": "{{count}} elementi", + "selectedCount": "{{count}} selezèuaed", + "searchFiles": "Cerca file...", + "upload": "Carica", + "selectHostToStart": "Selezèua un host per inizèsuo la gestèue file", + "failedToConnect": "Impossibile csunetti a SSH", + "failedToLoadDirectory": "Impossibile caricssuo la direcay", + "noSSHConnection": "No SSH csunessèue available", + "enterFolderName": "Inserèci nome cartella", + "enterFileName": "Inserèci nome file", + "copy": "Copia", + "cut": "Taglia", + "paste": "Incolla", + "copyPath": "Copia Percoso", + "copyPaths": "Copia Percosos", + "delete": "Elimina", + "properties": "Proprietà", + "refresh": "Aggèua", + "downloadFiles": "Scarica {{count}} file a Browser", + "copyFiles": "Copia {{count}} elementi", + "cutFiles": "Taglia {{count}} elementi", + "deleteFiles": "Elimina {{count}} elementi", + "filesCopiedToClipboard": "{{count}} elementi copied a clipboard", + "filesCutToClipboard": "{{count}} elementi taglia a clipboard", + "pathCopiedToClipboard": "Percoso copied a clipboard", + "pathsCopiedToClipboard": "{{count}} percosos copied a clipboard", + "failedToCopyPath": "Impossibile copia percoso a clipboard", + "movedItems": "Spostato {{count}} elementi", + "failedToDeleteItem": "Impossibile eliminssuo l'elemena", + "itemRenamedSuccessfully": "{{tipo}} renomed csu successoo", + "failedToRenameItem": "Impossibile rinominssuo l'elemena", + "download": "Scarica", + "permissions": "Permessi", + "size": "Dimensèue", + "modified": "Modificaa", + "path": "Percoso", + "confirmDelete": "Sei sicuro di voler eliminssuo {{count}} elemena/i?", + "uploadSuccess": "Caricamena completaa", + "uploadFailed": "Caricamena ftuttiia", + "downloadSuccess": "Scaricamena completaa", + "downloadFailed": "Scaricamena ftuttiia", + "permissionDenied": "Permèsèu denied", + "checkDockerLogs": "Check il Docker logs per detailed erroe inpermazèui", + "internalServerError": "Internal server erroe occurred", + "serverError": "Erroee del server", + "error": "Erroee", + "requestFailed": "Request failed csu staa code", + "unknownFileError": "unknown", + "cannotReadFile": "Cannsu read file", + "noSshSessionId": "No SSH sessèu ID available", + "noFilePath": "No file percoso available", + "noCurrentHost": "Nessun host corrente disponibile", + "fileSavedSuccessfully": "File salvaa csu successoo", + "saveTimeout": "Salva operaèu timed out. Il file può ssuo stai salvad csu successoo, ma il operaèu aok ao lsug a completaa. Check il Docker logs per csufermaaèu.", + "failedToSaveFile": "Impossibile salva file", + "deletedSuccessfully": "Eliminaa csu successoo", + "connectToServer": "Csunetti a a Server", + "selectServerToEdit": "Selezèua a server da il sidebar a avvia modificaing file", + "fileOperations": "File Operaèus", + "confirmDeleteMessage": "Sei sicuro you want a elimina {{nome}}?", + "confirmDeleteSingleItem": "Sei sicuro you want a permanently elimina \"{{nome}}\"?", + "confirmDeleteMultipleItems": "Sei sicuro you want a permanently elimina {{count}} elementi?", + "confirmDeleteMultipleItemsWithFolders": "Sei sicuro you want a permanently elimina {{count}} elementi? Quesa includes cartelle e ilir csutents.", + "confirmDeleteFolder": "Sei sicuro di voler eliminssuo la cartella \"{{nome}}\" e tuta il suo csutenua?", + "deleteDirectoryWarning": "Quesa sarà elimina il cartella e tutti its csutents.", + "actionCannotBeUndone": "Questa azèue nsu può essere annullaa.", + "permanentDeleteWarning": "Questa azèue nsu può essere annullaa. Il item(s) sarà permanently eliminad da il server.", + "recent": "Recenti", + "pinned": "Fissato", + "folderShortcuts": "Cartella Shottaglicome", + "noRecentFiles": "No recent file.", + "noPinnedFiles": "No pinned file.", + "enterFolderPath": "Inserèci cartella percoso", + "noShortcuts": "No shottaglicome.", + "searchFilesAndFolders": "Cerca file e cartelle...", + "noFilesOrFoldersFound": "No file o cartelle found.", + "failedToConnectSSH": "Impossibile csunetti a SSH", + "failedToReconnectSSH": "Impossibile recsunetti SSH sessèu", + "failedToListFiles": "Impossibile lèt file", + "fetchHomeDataTimeout": "Fetch home daa timed out", + "sshStatusCheckTimeout": "SSH staa check timed out", + "sshReconnectionTimeout": "SSH recsunessèue timed out", + "saveOperationTimeout": "Salva operaèu timed out", + "cannotSaveFile": "Cannsu salva file", + "dragSystemFilesToUpload": "Drag system file here a carica", + "dragFilesToWindowToDownload": "Drag file outside winfarew a scarica", + "openTerminalHere": "Apri Terminale Here", + "run": "Esegui", + "saveToSystem": "Salva nel Sètema", + "selectLocationToSave": "Selezèua posizèue per salvssuo", + "openTerminalInFolder": "Apri Terminale in Quesa Cartella", + "openTerminalInFileLocation": "Apri Terminale a File Locaèu", + "terminalWithPath": "Terminalee - {{host}}:{{percoso}}", + "runningFile": "Running - {{file}}", + "onlyRunExecutableFiles": "Can suly run exetagliaable file", + "noHostSelected": "Nessun host selezèuaa", + "starred": "Starred", + "shortcuts": "Shottaglicome", + "directories": "Direcaies", + "removedFromRecentFiles": "Rimuovid \"{{nome}}\" da recent file", + "removeFailed": "Rimuovi failed", + "unpinnedSuccessfully": "Unpinned \"{{nome}}\" csu successoo", + "unpinFailed": "Unpin failed", + "removedShortcut": "Rimuovid shottaglia \"{{nome}}\"", + "removeShortcutFailed": "Rimuovi shottaglia failed", + "clearedAllRecentFiles": "Clessuod tutti recent file", + "clearFailed": "Cancellazione fallita", + "removeFromRecentFiles": "Rimuovi da recent file", + "clearAllRecentFiles": "Clear tutti recent file", + "unpinFile": "Unpin file", + "removeShortcut": "Rimuovi shottaglia", + "saveFilesToSystem": "Salva {{count}} file come...", + "pinFile": "Fissa file", + "addToShortcuts": "Aggiungi a shottaglicome", + "downloadToDefaultLocation": "Scarica a predefinia locaèu", + "pasteFailed": "Impossibile incollssuo", + "noUndoableActions": "No unfareable azèui", + "undoCopySuccess": "Unfece copia operaèu: Eliminad {{count}} copied file", + "undoCopyFailedDelete": "Unfare failed: Could nsu elimina any copied file", + "undoCopyFailedNoInfo": "Unfare failed: Could nsu find copied file inpermazèui", + "undoMoveSuccess": "Unfece move operaèu: Spostato {{count}} file indietro a oiginal locaèu", + "undoMoveFailedMove": "Unfare failed: Could nsu move any file indietro", + "undoMoveFailedNoInfo": "Unfare failed: Could nsu find moved file inpermazèui", + "undoDeleteNotSupported": "Elimina operaèu nsu può essere annullaa: File ssuo stai permanently eliminad da server", + "undoTypeNotSupported": "Unsuppotaed unfare operaèu tipo", + "undoOperationFailed": "Unfare operaèu failed", + "unknownError": "Erroee scsuosciua", + "enterPath": "Inserèci percoso", + "editPath": "Modifica percoso", + "confirm": "Csuferma", + "cancel": "Annulla", + "find": "Trova", + "replaceWith": "Replace csu...", + "replace": "Sostituèci", + "replaceAll": "Sostituèci tuta", + "downloadInstead": "Scarica Instead", + "keyboardShortcuts": "Chiaveboard Shottaglicome", + "searchAndReplace": "Cerca & Replace", + "editing": "Modificaing", + "navigation": "Navigaèu", + "code": "Code", + "search": "Cerca", + "findNext": "Find Avanti", + "findPrevious": "Find Precedente", + "save": "Salva", + "selectAll": "Selezèua Tuta", + "undo": "Unfare", + "redo": "Refare", + "goToLine": "Vai tuttia riga", + "moveLineUp": "Sposta Riga Su", + "moveLineDown": "Sposta Riga Giù", + "toggleComment": "Attiva/dèativa commena", + "indent": "Aumenta indentazione", + "outdent": "Riduci indentazione", + "autoComplete": "Aua Completaa", + "imageLoadError": "Impossibile load image", + "rotate": "Rotae", + "originalSize": "Dimensèue Originale", + "startTyping": "Avvia typing...", + "unknownSize": "Unknown size", + "fileIsEmpty": "File è empty", + "largeFileWarning": "Large File Avvèo", + "largeFileWarningDesc": "Quesa file è {{size}} in size, which può cause perpermance èsues when apried come text.", + "fileNotFoundAndRemoved": "File \"{{nome}}\" nsu trovaa e è staa rimuovid da recent/pinned file", + "failedToLoadFile": "Impossibile load file: {{erroe}}", + "serverErrorOccurred": "Server erroe occurred. Per favoe try again laer.", + "autoSaveFailed": "Aua-salva failed", + "fileAutoSaved": "File aua-salvad", + "moveFileFailed": "Impossibile move {{nome}}", + "moveOperationFailed": "Move operaèu failed", + "canOnlyCompareFiles": "Can suly compssuo two file", + "comparingFiles": "Comparing file: {{file1}} e {{file2}}", + "dragFailed": "Drag operaèu failed", + "filePinnedSuccessfully": "File \"{{nome}}\" pinned csu successoo", + "pinFileFailed": "Impossibile pin file", + "fileUnpinnedSuccessfully": "File \"{{nome}}\" unpinned csu successoo", + "unpinFileFailed": "Impossibile unpin file", + "shortcutAddedSuccessfully": "Cartella shottaglia \"{{nome}}\" aggiungied csu successoo", + "addShortcutFailed": "Impossibile aggiungi shottaglia", + "operationCompletedSuccessfully": "{{operaèu}} {{count}} elementi csu successoo", + "operationCompleted": "{{operaèu}} {{count}} elementi", + "downloadFileSuccess": "File {{nome}} scaricaed csu successoo", + "downloadFileFailed": "Scarica failed", + "moveTo": "Move a {{nome}}", + "diffCompareWith": "Diff compssuo csu {{nome}}", + "dragOutsideToDownload": "Drag outside winfarew a scarica ({{count}} file)", + "newFolderDefault": "NewCartella", + "newFileDefault": "NewFile.txt", + "successfullyMovedItems": "csu successoo moved {{count}} elementi a {{target}}", + "move": "Move", + "searchInFile": "Cerca in file (Ctrl+F)", + "showKeyboardShortcuts": "Mostra chiaveboard shottaglicome", + "startWritingMarkdown": "Avvia writing tuo markfarewn csutent...", + "loadingFileComparison": "Caricamena file comparèsu...", + "reload": "Reload", + "compare": "Compssuo", + "sideBySide": "Side da Side", + "inline": "In linea", + "fileComparison": "File Comparèsu: {{file1}} vs {{file2}}", + "fileTooLarge": "File ao large: {{erroe}}", + "sshConnectionFailed": "SSH csunessèue failed. Per favoe check tuo csunessèue a {{nome}} ({{ip}}:{{pota}})", + "loadFileFailed": "Impossibile load file: {{erroe}}", + "connectedSuccessfully": "Csunesso csu successoo", + "totpVerificationFailed": "TOTP verificaèu failed", + "changePermissions": "Change Permèsèus", + "changePermissionsDesc": "Modify file permèsèus per", + "currentPermissions": "Current Permèsèus", + "newPermissions": "New Permèsèus", + "owner": "Proprietario", + "group": "Gruppo", + "others": "Oilrs", + "read": "Read", + "write": "Write", + "execute": "Exetagliae", + "permissionsChangedSuccessfully": "Permèsèus changed csu successoo", + "failedToChangePermissions": "Impossibile change permèsèus" + }, + "tunnels": { + "title": "Tunnel SSH", + "noSshTunnels": "No SSH Tunnel", + "createFirstTunnelMessage": "Crea il tuo primo tunnel SSH per inizèsuo. Usa il Gesae SSH per aggiungere host csu csufigurazèue tunnel.", + "connected": "Csunesso", + "disconnected": "Dècsunesso", + "connecting": "Csunessèue...", + "disconnecting": "Dècsunessèue...", + "unknownTunnelStatus": "Unknown", + "unknown": "Scsuosciua", + "error": "Erroe", + "failed": "Failed", + "retrying": "Nuovo tentaivo...", + "waiting": "Attendiing", + "waitingForRetry": "In atesa di riprovssuo", + "retryingConnection": "Nuovo tentaivo di csunessèue", + "canceling": "Annullamena...", + "connect": "Csunetti", + "disconnect": "Dècsunetti", + "cancel": "Annulla", + "port": "Pota", + "attempt": "Attempt {{current}} di {{max}}", + "nextRetryIn": "Avanti riprova in {{secsuds}} secsuds", + "checkDockerLogs": "Check tuo Docker logs per il erroe recomesu, join il", + "noTunnelConnections": "Nessuna csunessèue tunnel csufiguraa", + "tunnelConnections": "Csunessèui Tunnel", + "addTunnel": "Aggiungi Tunnel", + "editTunnel": "Modifica Tunnel", + "deleteTunnel": "Elimina Tunnel", + "tunnelName": "Tunnel Nome", + "localPort": "Potaa Locale", + "remoteHost": "Host Remoa", + "remotePort": "Potaa Remota", + "autoStart": "Aua Avvia", + "status": "Staa", + "active": "Attivo", + "inactive": "Inativo", + "start": "Avvia", + "stop": "Ferma", + "restart": "Reavvia", + "connectionType": "Csunessèue Tipo", + "local": "Local", + "remote": "Remote", + "dynamic": "Dynamic", + "unknownConnectionStatus": "Unknown", + "portMapping": "Pota {{sourcePota}} → {{endpointHost}}:{{endpointPota}}", + "endpointHostNotFound": "Endpoint host nsu trovaa", + "discord": "Dècod", + "githubIssue": "GitHub èsue", + "forHelp": "per help" + }, + "serverStats": { + "title": "Staètiche Server", + "cpu": "CPU", + "memory": "Memoia", + "disk": "Dèco", + "network": "Rete", + "uptime": "Tempo di Attività", + "loadAverage": "Carico Medio", + "processes": "Processi", + "connections": "Connessioni", + "usage": "Utilizzo", + "available": "Disponibile", + "total": "Totale", + "free": "Libero", + "used": "Usata", + "percentage": "Percentuale", + "refreshStatusAndMetrics": "Aggiorna stato e metriche", + "refreshStatus": "Aggiorna Stato", + "fileManagerAlreadyOpen": "Gestione File già aperta per questo host", + "openFileManager": "Apri Gestione File", + "cpuCores_one": "{{count}} CPU", + "cpuCores_other": "{{count}} CPU", + "naCpus": "N/D CPU", + "loadAverageNA": "Media: N/D", + "cpuUsage": "Uso CPU", + "memoryUsage": "Uso Memoria", + "diskUsage": "Uso Disco", + "rootStorageSpace": "Spazio di Archiviazione Root", + "of": "di", + "feedbackMessage": "Hai idee per cosa dovrebbe arrivare dopo per la gestione server? Condividile su", + "failedToFetchHostConfig": "Impossibile recuperare configurazione host", + "failedToFetchStatus": "Impossibile recuperare stato server", + "failedToFetchMetrics": "Impossibile recuperare metriche server", + "failedToFetchHomeData": "Impossibile recuperare dati home", + "loadingMetrics": "Caricamento metriche...", + "refreshing": "Aggiornamento...", + "serverOffline": "Server Offline", + "cannotFetchMetrics": "Impossibile recuperare metriche da server offline", + "totpRequired": "Autenticazione TOTP Richiesta", + "totpUnavailable": "Stato Server non disponibile per server con TOTP abilitato", + "load": "Carico", + "connections": "Csunessèui", + "usage": "Utilizzo", + "available": "Dèpsuibile", + "total": "Totale", + "free": "Libero", + "used": "Usaa", + "percentage": "Percentage", + "refreshStatusAndMetrics": "Aggèua staa e metrics", + "refreshStatus": "Aggèua Staa", + "fileManagerAlreadyOpen": "File Manager already apri per quesa host", + "openFileManager": "Apri File Manager", + "cpuCores_one": "{{count}} CPU", + "cpuCores_other": "{{count}} CPUs", + "naCpus": "N/A CPU(s)", + "loadAverageNA": "Avg: N/A", + "cpuUsage": "Uso CPU", + "memoryUsage": "Uso Memoia", + "diskUsage": "Dèk Usage", + "rootStorageSpace": "Root Saage Space", + "of": "di", + "feedbackMessage": "Have idecome per wha farevrebbe come avanti per server management? Shssuo ilm su", + "failedToFetchHostConfig": "Impossibile fetch host csufigurazèue", + "failedToFetchStatus": "Impossibile fetch server staa", + "failedToFetchMetrics": "Impossibile fetch server metrics", + "failedToFetchHomeData": "Impossibile fetch home daa", + "loadingMetrics": "Caricamena metrics...", + "refreshing": "Aggèuomena...", + "serverOffline": "Server Offline", + "cannotFetchMetrics": "Cannsu fetch metrics da difline server", + "totpRequired": "Autenticazèue TOTP Richiesta", + "totpUnavailable": "Server Stcome unavailable per TOTP-abilitaa server", + "load": "Load", + "editLayout": "Modifica Layout", + "cancelEdit": "Annulla", + "addWidget": "Aggiungi Widget", + "saveLayout": "Salva Layout", + "unsavedChanges": "Modifiche non salvate", + "layoutSaved": "Layout salvato con successo", + "failedToSaveLayout": "Impossibile salvare il layout", + "systemInfo": "Informazioni Sistema", + "hostname": "Nome Host", + "operatingSystem": "Sistema Operativo", + "kernel": "Kernel", + "totalUptime": "Tempo di Attività Totale", + "seconds": "secondi", + "networkInterfaces": "Interfacce di Rete", + "noInterfacesFound": "Nessuna interfaccia di rete trovata", + "totalProcesses": "Processi Totali", + "running": "In esecuzione", + "noProcessesFound": "Nessun processo trovato", + "loginStats": "Statistiche Accessi", + "totalLogins": "Accessi Totali", + "uniqueIPs": "IP Unici", + "recentSuccessfulLogins": "Accessi Riusciti Recenti", + "recentFailedAttempts": "Tentativi Falliti Recenti", + "noRecentLoginData": "Nessun dato di accesso recente", + "from": "da", + "quickActions": "Azioni Rapide", + "executeQuickAction": "Esegui {{name}}", + "executingQuickAction": "Esecuzione {{name}}...", + "quickActionSuccess": "{{name}} completata con successo", + "quickActionFailed": "{{name}} fallita", + "quickActionError": "Impossibile eseguire {{name}}" + }, + "auth": { + "tagline": "GESTORE SERVER SSH", + "description": "Gestione connessioni SSH sicura, potente e intuitiva", + "welcomeBack": "Bentornato su TERMIX", + "createAccount": "Crea il tuo account TERMIX", + "continueExternal": "Continua con provider esterno", + "loginTitle": "Accedi a Termix", + "registerTitle": "Crea Account", + "loginButton": "Accedi", + "registerButton": "Registrati", + "forgotPassword": "Password Dimenticata?", + "rememberMe": "Ricordami", + "noAccount": "Non hai un account?", + "hasAccount": "Hai già un account?", + "loginSuccess": "Accesso effettuato con successo", + "loginFailed": "Accesso fallito", + "registerSuccess": "Registrazione completata con successo", + "registerFailed": "Registrazione fallita", + "logoutSuccess": "Disconnessione effettuata con successo", + "invalidCredentials": "Credenziali non valide", + "accountCreated": "Account creato con successo", + "passwordReset": "Link reimpostazione password inviato", + "twoFactorAuth": "Autenticazione a Due Fattori", + "enterCode": "Inserisci codice", + "backupCode": "O usa codice di backup", + "verifyCode": "Verifica Codice", + "redirectingToApp": "Reindirizzamento all'app...", + "enableTwoFactor": "Abilita Autenticazione a Due Fattori", + "disableTwoFactor": "Disabilita Autenticazione a Due Fattori", + "scanQRCode": "Scansiona il codice QR", + "backupCodes": "Codici di Backup", + "saveBackupCodes": "Salva questi codici di backup in un luogo sicuro", + "twoFactorEnabledSuccess": "Autenticazione a due fattori abilitata con successo!", + "twoFactorDisabled": "Autenticazione a due fattori disabilitata", + "newBackupCodesGenerated": "Nuovi codici di backup generati", + "backupCodesDownloaded": "Codici di backup scaricati", + "pleaseEnterSixDigitCode": "Per favore inserisci un codice a 6 cifre", + "invalidVerificationCode": "Codice di verifica non valido", + "failedToDisableTotp": "Impossibile disabilitare TOTP", + "failedToGenerateBackupCodes": "Impossibile generare codici di backup", + "enterPassword": "Inserisci password", + "lockedOidcAuth": "Bloccato (Auth OIDC)", + "twoFactorTitle": "Autenticazione a Due Fattori", + "twoFactorProtected": "Il tuo account è protetto con autenticazione a due fattori", + "twoFactorActive": "L'autenticazione a due fattori è attualmente attiva sul tuo account", + "disable2FA": "Disabilita 2FA", + "disableTwoFactorWarning": "Disabilitare l'autenticazione a due fattori renderà il tuo account meno sicuro", + "passwordOrTotpCode": "Password o Codice TOTP", + "or": "O", + "generateNewBackupCodesText": "Genera nuovi codici di backup se hai perso quelli esistenti", + "generateNewBackupCodes": "Genera Nuovi Codici di Backup", + "yourBackupCodes": "I Tuoi Codici di Backup", + "download": "Scarica", + "setupTwoFactorTitle": "Configura Autenticazione a Due Fattori", + "sshAuthenticationRequired": "Autenticazione SSH Obbligatoria", + "sshNoKeyboardInteractive": "Autenticazione Keyboard-Interactive Non Disponibile", + "sshAuthenticationFailed": "Autenticazione Fallita", + "sshAuthenticationTimeout": "Timeout Autenticazione", + "sshNoKeyboardInteractiveDescription": "Il server non supporta l'autenticazione keyboard-interactive. Per favore fornisci la tua password o chiave SSH.", + "sshAuthFailedDescription": "Le credenziali fornite non erano corrette. Riprova con credenziali valide.", + "sshTimeoutDescription": "Il tentativo di autenticazione è scaduto. Riprova.", + "sshProvideCredentialsDescription": "Per favore fornisci le tue credenziali SSH per connetterti a questo server.", + "sshPasswordDescription": "Inserisci la password per questa connessione SSH.", + "sshKeyPasswordDescription": "Se la tua chiave SSH è criptata, inserisci la passphrase qui.", + "step1ScanQR": "Passo 1: Scansiona il codice QR con la tua app authenticator", + "manualEntryCode": "Inserimento Manuale Codice", + "cannotScanQRText": "Se non puoi scansionare il codice QR, inserisci questo codice manualmente nella tua app authenticator", + "nextVerifyCode": "Avanti: Verifica Codice", + "verifyAuthenticator": "Verifica il tuo Authenticator", + "step2EnterCode": "Passo 2: Inserisci il codice a 6 cifre dalla tua app authenticator", + "verificationCode": "Codice Verifica", + "back": "Indietro", + "verifyAndEnable": "Verifica e Abilita", + "saveBackupCodesTitle": "Salva i Tuoi Codici di Backup", + "step3StoreCodesSecurely": "Passo 3: Salva questi codici in un luogo sicuro", + "importantBackupCodesText": "Salva questi codici di backup in una posizione sicura. Puoi usarli per accedere al tuo account se perdi il tuo dispositivo authenticator.", + "completeSetup": "Completa Configurazione", + "notEnabledText": "L'autenticazione a due fattori aggiunge un livello extra di sicurezza richiedendo un codice dalla tua app authenticator durante l'accesso.", + "enableTwoFactorButton": "Abilita Autenticazione a Due Fattori", + "addExtraSecurityLayer": "Aggiungi un livello extra di sicurezza al tuo account", + "firstUser": "Primo Utente", + "firstUserMessage": "Sei il primo utente e sarai reso amministratore. Puoi visualizzare le impostazioni amministratore nel menu utente della barra laterale. Se pensi che questo sia un errore, controlla i log del container docker, o crea una GitHub issue.", + "external": "Esterno", + "loginWithExternal": "Accedi con Provider Esterno", + "loginWithExternalDesc": "Accedi usando il tuo provider di identità esterno configurato", + "externalNotSupportedInElectron": "L'autenticazione esterna non è ancora supportata nell'app Electron. Per favore usa la versione web per il login OIDC.", + "resetPasswordButton": "Reimposta Password", + "sendResetCode": "Invia Codice di Reset", + "resetCodeDesc": "Inserisci il tuo nome utente per ricevere un codice di reset password. Il codice sarà registrato nei log del container docker.", + "resetCode": "Codice di Reset", + "verifyCodeButton": "Verifica Codice", + "enterResetCode": "Inserisci il codice a 6 cifre dai log del container docker per l'utente:", + "goToLogin": "Vai al Login", + "newPassword": "Nuova Password", + "confirmNewPassword": "Conferma Password", + "enterNewPassword": "Inserisci nuova password", + "signUp": "Registrati", + "mobileApp": "App Mobile", + "loggingInToMobileApp": "Accesso all'app mobile", + "desktopApp": "App Desktop", + "loggingInToDesktopApp": "Accesso all'app desktop", + "loggingInToDesktopAppViaWeb": "Accesso all'app desktop via interfaccia web", + "loadingServer": "Caricamento server...", + "authenticating": "Autenticazione...", + "dataLossWarning": "Reimpostare la password in questo modo eliminerà tutti i tuoi host SSH salvati, credenziali, e altri dati criptati. Questa operazione non può essere annullata. Usa questa opzione solo se hai dimenticato la password e non sei loggato.", + "authenticationDisabled": "Autenticazione Disabilitata", + "authenticationDisabledDesc": "Tutti i metodi di autenticazione sono attualmente disabilitati. Per favore contatta il tuo amministratore.", + "passwordResetSuccess": "Password reimpostata con successo", + "passwordResetSuccessDesc": "La tua password è stata reimpostata con successo. Ora puoi accedere con la nuova password." + }, + "errors": { + "notFound": "Non trovato", + "unauthorized": "Non autorizzato", + "forbidden": "Accesso negato", + "serverError": "Errore del server", + "networkError": "Errore di rete", + "databaseConnection": "Impossibile connettersi al database", + "unknownError": "Errore sconosciuto", + "loginFailed": "Accesso fallito", + "failedPasswordReset": "Impossibile avviare il reset della password", + "failedVerifyCode": "Impossibile verificare il codice di reset", + "failedCompleteReset": "Impossibile completare il reset della password", + "invalidTotpCode": "Codice TOTP non valido", + "failedOidcLogin": "Impossibile avviare login OIDC", + "failedUserInfo": "Impossibile ottenere info utente dopo login OIDC", + "oidcAuthFailed": "Autenticazione OIDC fallita", + "noTokenReceived": "Nessun token ricevuto dal login", + "invalidAuthUrl": "URL autorizzazione non valido ricevuto dal backend", + "invalidInput": "Input non valido", + "requiredField": "Campo obbligatorio", + "minLength": "Lunghezza minima è {{min}}", + "maxLength": "Lunghezza massima è {{max}}", + "invalidEmail": "Indirizzo email non valido", + "passwordMismatch": "Le password non corrispondono", + "passwordLoginDisabled": "Il login nome utente/password è attualmente disabilitato", + "weakPassword": "Password troppo debole", + "usernameExists": "Il nome utente esiste già", + "emailExists": "L'email esiste già", + "loadFailed": "Impossibile caricare i dati", + "saveError": "Impossibile salvare", + "sessionExpired": "Sessione scaduta" + }, + "messages": { + "saveSuccess": "Salvato con successo", + "saveError": "Impossibile salvare", + "deleteSuccess": "Eliminato con successo", + "deleteError": "Impossibile eliminare", + "updateSuccess": "Aggiornato con successo", + "updateError": "Impossibile aggiornare", + "copySuccess": "Copiato negli appunti", + "copyError": "Impossibile copiare", + "copiedToClipboard": "Copiato negli appunti", + "connectionEstablished": "Connessione stabilita", + "connectionClosed": "Connessione chiusa", + "reconnecting": "Riconnessione...", + "processing": "Elaborazione...", + "pleaseWait": "Attendere...", + "registrationDisabled": "La registrazione di nuovi account è attualmente disabilitata da un amministratore. Per favore accedi o contatta un amministratore.", + "databaseConnected": "Database connesso con successo", + "databaseConnectionFailed": "Impossibile connettersi al server database", + "checkServerConnection": "Per favore controlla la connessione al server e riprova", + "resetCodeSent": "Codice di reset inviato ai log Docker", + "codeVerified": "Codice verificato con successo", + "passwordResetSuccess": "Password reimpostata con successo", + "loginSuccess": "Accesso effettuato con successo", + "registrationSuccess": "Registrazione completata con successo" + }, + "profile": { + "title": "Profilo Utente", + "description": "Gestisci le impostazioni del tuo account e la sicurezza", + "security": "Sicurezza", + "changePassword": "Cambia Password", + "twoFactorAuth": "Autenticazione a Due Fattori", + "accountInfo": "Informazioni Account", + "role": "Ruolo", + "admin": "Amministratore", + "user": "Utente", + "authMethod": "Metodo Autenticazione", + "local": "Locale", + "external": "Esterno (OIDC)", + "externalAndLocal": "Doppia Autenticazione", + "selectPreferredLanguage": "Seleziona la tua lingua preferita per l'interfaccia", + "fileColorCoding": "Colorazione File", + "fileColorCodingDesc": "Colora file per tipo: cartelle (rosso), file (blu), link simbolici (verde)", + "commandAutocomplete": "Autocompletamento Comandi", + "commandAutocompleteDesc": "Abilita suggerimenti autocompletamento tasto Tab per i comandi terminale basati sulla tua cronologia comandi", + "currentPassword": "Password Attuale", + "passwordChangedSuccess": "Password cambiata con successo! Per favore accedi di nuovo.", + "failedToChangePassword": "Impossibile cambiare la password. Per favore controlla la tua password attuale e riprova." + }, + "user": { + "failedToLoadVersionInfo": "Impossibile caricare informazioni versione" + "unsavedChanges": "Unsalvad changes", + "layoutSaved": "Layout salvad csu successoo", + "failedToSaveLayout": "Impossibile salva layout", + "systemInfo": "System Inpermazèui", + "hostname": "Nome Host", + "operatingSystem": "Operaing System", + "kernel": "Kernel", + "totalUptime": "Total Uptime", + "seconds": "secsudi", + "networkInterfaces": "Netwok Interfaces", + "noInterfacesFound": "No netwok interfaces found", + "totalProcesses": "Total Processes", + "running": "Running", + "noProcessesFound": "No processes found", + "loginStats": "Staètiche Accessi", + "totalLogins": "Total Logins", + "uniqueIPs": "Unique IPs", + "recentSuccessfulLogins": "Recenti Successdiul Logins", + "recentFailedAttempts": "Recenti Failed Attempts", + "noRecentLoginData": "No recent login daa", + "from": "da", + "quickActions": "Azèui Rapide", + "executeQuickAction": "Exetagliae {{nome}}", + "executingQuickAction": "Exetagliaing {{nome}}...", + "quickActionSuccess": "{{nome}} completaad csu successoo", + "quickActionFailed": "{{nome}} failed", + "quickActionError": "Impossibile exetagliae {{nome}}" + }, + "auth": { + "tagline": "GESTORE SERVER SSH", + "description": "Secure, powerful, e intuitive SSH csunessèue management", + "welcomeBack": "Welcome indietro a TERMIX", + "createAccount": "Crea tuo TERMIX account", + "continueExternal": "Csutinue csu external provider", + "loginTitle": "Login a Termix", + "registerTitle": "Crea Account", + "loginButton": "Login", + "registerButton": "Regèter", + "forgotPassword": "Pcomeswod Dimenticaa?", + "rememberMe": "Ricodami", + "noAccount": "Dsu't hanno an account?", + "hasAccount": "Already hanno an account?", + "loginSuccess": "Login successdiul", + "loginFailed": "Accesso ftuttiia", + "registerSuccess": "Regètrazèue completaa csu successoo", + "registerFailed": "Regètrazèue ftuttiita", + "logoutSuccess": "Logged out csu successoo", + "invalidCredentials": "Credenziali nsu valide", + "accountCreated": "Account cread csu successoo", + "passwordReset": "Pcomeswod reset link sent", + "twoFactorAuth": "Autenticazèue a Due Faai", + "enterCode": "Inserèci codice", + "backupCode": "Or use indietroup code", + "verifyCode": "Verifica Codice", + "redirectingToApp": "Redirecting a app...", + "enableTwoFactor": "Abilita Two-Faca Autenticazèue", + "disableTwoFactor": "Dèabilita Two-Faca Autenticazèue", + "scanQRCode": "Spuòsèua il codice QR", + "backupCodes": "Codici di Indietroup", + "saveBackupCodes": "Salva ilse indietroup codes in a safe place", + "twoFactorEnabledSuccess": "Two-faca autenticazèue abilitaa csu successoo!", + "twoFactorDisabled": "Two-faca autenticazèue dèabilitaa", + "newBackupCodesGenerated": "New indietroup codes generaed", + "backupCodesDownloaded": "Indietroup codes scaricaed", + "pleaseEnterSixDigitCode": "Per favoe inserèci a 6-digit code", + "invalidVerificationCode": "Invalid verificaèu code", + "failedToDisableTotp": "Impossibile dèabilitssuo TOTP", + "failedToGenerateBackupCodes": "Impossibile generae indietroup codes", + "enterPassword": "Inserèci pcomeswod", + "lockedOidcAuth": "Bloccato (Auth OIDC)", + "twoFactorTitle": "Two-Faca Autenticazèue", + "twoFactorProtected": "Il tuo account è protected csu two-faca autenticazèue", + "twoFactorActive": "Two-faca autenticazèue è currently ativo su tuo account", + "disable2FA": "Dèabilita 2FA", + "disableTwoFactorWarning": "Dèabling two-faca autenticazèue sarà make tuo account less secure", + "passwordOrTotpCode": "Pcomeswod o TOTP Code", + "or": "Or", + "generateNewBackupCodesText": "Generae new indietroup codes if you've lost tuo exèting sues", + "generateNewBackupCodes": "Generae New Indietroup Codes", + "yourBackupCodes": "I Tuoi Codici di Indietroup", + "download": "Scarica", + "setupTwoFactorTitle": "Set Up Two-Faca Autenticazèue", + "sshAuthenticationRequired": "SSH Autenticazèue Obbligaaio", + "sshNoKeyboardInteractive": "Chiaveboard-Interativo Autenticazèue Unavailable", + "sshAuthenticationFailed": "Autenticazèue Failed", + "sshAuthenticationTimeout": "Autenticazèue Timeout", + "sshNoKeyboardInteractiveDescription": "Il server fa nsu suppota chiaveboard-interativo autenticazèue. Per favoe provide tuo pcomeswod o SSH chiave.", + "sshAuthFailedDescription": "Il provided credenziali erano incorect. Per favoe try again csu valid credenziali.", + "sshTimeoutDescription": "Il autenticazèue atempt timed out. Per favoe try again.", + "sshProvideCredentialsDescription": "Per favoe provide tuo SSH credenziali a csunetti a quesa server.", + "sshPasswordDescription": "Inserèci il pcomeswod per quesa SSH csunessèue.", + "sshKeyPasswordDescription": "If tuo SSH chiave è encrypted, inserèci il pcomesphrcomee here.", + "step1ScanQR": "Step 1: Spuò il QR code csu tuo auilnticaa app", + "manualEntryCode": "Codice Inserimento Manuale", + "cannotScanQRText": "If you può't spuò il QR code, inserèci quesa code manututtiy in tuo auilnticaa app", + "nextVerifyCode": "Avanti: Verifica Codice", + "verifyAuthenticator": "Verify Il tuo Auilnticaa", + "step2EnterCode": "Step 2: Inserèci il 6-digit code da tuo auilnticaa app", + "verificationCode": "Verificaèu Code", + "back": "Indietro", + "verifyAndEnable": "Verifica e Abilita", + "saveBackupCodesTitle": "Salva Il tuo Indietroup Codes", + "step3StoreCodesSecurely": "Step 3: Sae ilse codes in a safe place", + "importantBackupCodesText": "Salva ilse indietroup codes in a secure locaèu. You può use ilm a access tuo account if you lose tuo auilnticaa device.", + "completeSetup": "Completaa Setup", + "notEnabledText": "Two-faca autenticazèue aggiungè an extra layer di sicurezza da requiring a code da tuo auilnticaa app when signing in.", + "enableTwoFactorButton": "Abilita Two-Faca Autenticazèue", + "addExtraSecurityLayer": "Aggiungi an extra layer di sicurezza a tuo account", + "firstUser": "First Utente", + "firstUserMessage": "You ssuo il first utente e sarà made an amminètraae. You può vèualizza amminètraae impostazèui in il sidebar utente dropfarewn. If you think quesa è a mètake, check il farecker logs, o crea a GitHub èsue.", + "external": "Esterno", + "loginWithExternal": "Login csu Esterno Provider", + "loginWithExternalDesc": "Login using tuo csufigured external identity provider", + "externalNotSupportedInElectron": "Esterno autenticazèue è nsu suppotaaa in il Electrsu app yet. Per favoe use il web versèu per OIDC login.", + "resetPasswordButton": "Reset Pcomeswod", + "sendResetCode": "Invia Codice di Reset", + "resetCodeDesc": "Inserèci tuo nome utente a receive a pcomeswod reset code. Il code sarà logged in il farecker csutainer logs.", + "resetCode": "Codice di Reset", + "verifyCodeButton": "Verifica Codice", + "enterResetCode": "Inserèci il 6-digit code da il farecker csutainer logs per utente:", + "goToLogin": "Go a Login", + "newPassword": "Nuova Pcomeswod", + "confirmNewPassword": "Csuferma Pcomeswod", + "enterNewPassword": "Inserèci nuova pcomeswod", + "signUp": "Regètrai", + "mobileApp": "App Mobile", + "loggingInToMobileApp": "Logging in a il mobile app", + "desktopApp": "Deskap App", + "loggingInToDesktopApp": "Logging in a il deskap app", + "loggingInToDesktopAppViaWeb": "Logging in a il deskap app via web interface", + "loadingServer": "Caricamena server...", + "authenticating": "Auilnticaing...", + "dataLossWarning": "Resetting tuo pcomeswod quesa way sarà elimina tutti tuo salvad SSH host, credenziali, e oilr encrypted daa. Questa azèue nsu può essere annullaa. Only use quesa if you hanno pergotten tuo pcomeswod e ssuo nsu logged in.", + "authenticationDisabled": "Autenticazèue Dèabilitaa", + "authenticationDisabledDesc": "Tutti autenticazèue methods ssuo currently dèabilitaa. Per favoe csutact tuo amminètraaeètraa.", + "passwordResetSuccess": "Pcomeswod reimpostaa csu successoo", + "passwordResetSuccessDesc": "La tua pcomeswod è staa reimpostaa csu successoo. Ora puoi accedere csu la nuova pcomeswod." + }, + "errors": { + "notFound": "Nsu trovaa", + "unauthorized": "Nsu auaizzaa", + "forbidden": "Accesso negaa", + "serverError": "Erroee del server", + "networkError": "Erroee di rete", + "databaseConnection": "Could nsu csunetti a il daabcomee", + "unknownError": "Erroee scsuosciua", + "loginFailed": "Accesso fallito", + "failedPasswordReset": "Impossibile initiae pcomeswod reset", + "failedVerifyCode": "Impossibile verify reset code", + "failedCompleteReset": "Impossibile completaa pcomeswod reset", + "invalidTotpCode": "Codice TOTP non valido", + "failedOidcLogin": "Impossibile avvia OIDC login", + "failedUserInfo": "Impossibile get utente info after OIDC login", + "oidcAuthFailed": "OIDC autenticazèue failed", + "noTokenReceived": "No aken received da login", + "invalidAuthUrl": "Invalid authoizaèu URL received da indietroend", + "invalidInput": "Input nsu valifare", + "requiredField": "Campo obbligaaio", + "minLength": "Minimum length è {{min}}", + "maxLength": "Maximum length è {{max}}", + "invalidEmail": "Invalid email indirizzo", + "passwordMismatch": "Pcomeswods fare nsu mach", + "passwordLoginDisabled": "Nome utente/pcomeswod login è currently dèabilitaa", + "weakPassword": "Pcomeswod è ao weak", + "usernameExists": "Nome utente esète già", + "emailExists": "Email esète già", + "loadFailed": "Impossibile load daa", + "saveError": "Impossibile salvssuo", + "sessionExpired": "Sessèue scaduta" + }, + "messages": { + "saveSuccess": "Salvad csu successoo", + "saveError": "Impossibile salvssuo", + "deleteSuccess": "Eliminad csu successoo", + "deleteError": "Impossibile elimina", + "updateSuccess": "Aggèuaa csu successoo", + "updateError": "Impossibile aggèua", + "copySuccess": "Copied a clipboard", + "copyError": "Impossibile copia", + "copiedToClipboard": "Copiaa negli appunti", + "connectionEstablished": "Csunessèue establèhed", + "connectionClosed": "Csunessèue chiufece", + "reconnecting": "Ricsunessèue...", + "processing": "Processing...", + "pleaseWait": "Attendere...", + "registrationDisabled": "New account regètraèu è currently dèabilitaa da an amminètraae. Per favoe log in o csutact an amminètraaeètraa.", + "databaseConnected": "Daabcomee csunesso csu successoo", + "databaseConnectionFailed": "Impossibile csunetti a il daabcomee server", + "checkServerConnection": "Per favoe check tuo server csunessèue e try again", + "resetCodeSent": "Reset code sent a Docker logs", + "codeVerified": "Code verified csu successoo", + "passwordResetSuccess": "Pcomeswod reset csu successoo", + "loginSuccess": "Login successdiul", + "registrationSuccess": "Regètraèu successdiul" + }, + "profile": { + "title": "Prdiilo Utente", + "description": "Manage tuo account impostazèui e sicurezza", + "security": "Sicurezza", + "changePassword": "Cambia Pcomeswod", + "twoFactorAuth": "Autenticazèue a Due Faai", + "accountInfo": "Account Inpermazèui", + "role": "Role", + "admin": "Amminètraaeètraa", + "user": "Utente", + "authMethod": "Autenticazèue Method", + "local": "Local", + "external": "Esterno (OIDC)", + "externalAndLocal": "Dual Auth", + "selectPreferredLanguage": "Selezèua tuo preferred language per il interface", + "fileColorCoding": "File Colo Coding", + "fileColorCodingDesc": "Colo-code file da tipo: cartelle (red), file (blue), symlinks (green)", + "commandAutocomplete": "Comme Auacompletaa", + "commandAutocompleteDesc": "Abilita Tab chiave auacompletaa suggestèus per terminale commes bcomeed su tuo comme hèay", + "currentPassword": "Pcomeswod Attuale", + "passwordChangedSuccess": "Pcomeswod changed csu successoo! Per favoe log in again.", + "failedToChangePassword": "Impossibile change pcomeswod. Per favoe check tuo current pcomeswod e try again." + }, + "user": { + "failedToLoadVersionInfo": "Impossibile load versèu inpermazèui" + }, + "placeholders": { + "enterCode": "000000", + "ipAddress": "192.168.1.1", + "port": "22", + "maxRetries": "3", + "retryInterval": "10", + "language": "Lingua", + "username": "nome utente", + "hostname": "nome host", + "folder": "Cartella", + "password": "password", + "keyPassword": "password chiave", + "pastePrivateKey": "Incolla la tua chiave privata qui...", + "pastePublicKey": "Incolla la tua chiave pubblica qui...", + "credentialName": "Mio Server SSH", + "description": "Descrizione (opzionale)", + "searchCredentials": "Cerca credenziali per nome, utente o tag...", + "sshConfig": "configurazione ssh endpoint", + "homePath": "/home", + "clientId": "client-id", + "clientSecret": "client-secret", + "authUrl": "https://tuo-provider.com/applicazione/o/authorize/", + "redirectUrl": "https://tuo-provider.com/applicazione/o/termix/", + "tokenUrl": "https://tuo-provider.com/applicazione/o/token/", + "userIdField": "sub", + "usernameField": "name", + "scopes": "openid email profile", + "userinfoUrl": "https://tuo-provider.com/applicazione/o/userinfo/", + "enterUsername": "Inserisci nome utente da rendere amministratore", + "searchHosts": "Cerca host per nome, nome utente, IP, cartella, tag...", + "enterPassword": "Inserisci la tua password", + "totpCode": "Codice TOTP a 6 cifre", + "searchHostsAny": "Cerca host per qualsiasi info...", + "confirmPassword": "Inserisci la tua password per conferma", + "typeHere": "Scrivi qui", + "fileName": "Inserisci nome file (es. esempio.txt)", + "folderName": "Inserisci nome cartella", + "fullPath": "Inserisci percorso completo", + "currentPath": "Inserisci percorso corrente", + "newName": "Inserisci nuovo nome" + }, + "leftSidebar": { + "failedToLoadHosts": "Impossibile caricare gli host", + "noFolder": "Nessuna Cartella", + "passwordRequired": "La password è obbligatoria", + "failedToDeleteAccount": "Impossibile eliminare l'account", + "failedToMakeUserAdmin": "Impossibile rendere l'utente amministratore", + "userIsNowAdmin": "L'utente {{username}} è ora amministratore", + "removeAdminConfirm": "Sei sicuro di voler rimuovere lo stato di amministratore da {{username}}?", + "deleteUserConfirm": "Sei sicuro di voler eliminare l'utente {{username}}? Questa operazione non può essere annullata.", + "deleteAccount": "Elimina Account", + "closeDeleteAccount": "Chiudi Elimina Account", + "deleteAccountWarning": "Questa operazione non può essere annullata. Questo eliminerà permanentemente il tuo account e tutti i dati associati.", + "deleteAccountWarningDetails": "Eliminare il tuo account rimuoverà tutti i tuoi dati inclusi host SSH, configurazioni e impostazioni. Questa operazione è irreversibile.", + "deleteAccountWarningShort": "Questa operazione non è reversibile e eliminerà permanentemente il tuo account.", + "cannotDeleteAccount": "Impossibile Eliminare Account", + "lastAdminWarning": "Sei l'ultimo utente amministratore. Non puoi eliminare il tuo account perché questo lascerebbe il sistema senza amministratori. Rendi prima un altro utente amministratore, o contatta il supporto di sistema.", + "confirmPassword": "Conferma Password", + "deleting": "Eliminazione...", + "cancel": "Annulla" + }, + "interface": { + "sidebar": "Barra Laterale", + "toggleSidebar": "Mostra/Nascondi Barra Laterale", + "close": "Chiudi", + "online": "Online", + "offline": "Offline", + "maintenance": "Manutenzione", + "degraded": "Degradato", + "noTunnelConnections": "Nessuna connessione tunnel configurata", + "discord": "Discord", + "connectToSshForOperations": "Connettiti a SSH per usare le operazioni file", + "language": "Language", + "username": "nome utente", + "hostname": "host nome", + "folder": "Cartella", + "password": "pcomeswod", + "keyPassword": "chiave pcomeswod", + "pastePrivateKey": "Incolla tuo privae chiave here...", + "pastePublicKey": "Incolla tuo public chiave here...", + "credentialName": "My SSH Server", + "description": "Descrizèue (opzèuale)", + "searchCredentials": "Cerca credenziali per nome, utente o tag...", + "sshConfig": "endpoint ssh csufigurazèue", + "homePath": "/home", + "clientId": "client-id", + "clientSecret": "client-secret", + "authUrl": "https://tuo-provider.com/applicaèu/o/authoize/", + "redirectUrl": "https://tuo-provider.com/applicaèu/o/termix/", + "tokenUrl": "https://tuo-provider.com/applicaèu/o/aken/", + "userIdField": "sub", + "usernameField": "nome", + "scopes": "apriid email prdiile", + "userinfoUrl": "https://tuo-provider.com/applicaèu/o/utenteinfo/", + "enterUsername": "Inserèci nome utente a make amminètraae", + "searchHosts": "Cerca host da nome, nome utente, IP, cartella, tags...", + "enterPassword": "Inserèci tuo pcomeswod", + "totpCode": "6-digit TOTP code", + "searchHostsAny": "Cerca host da any info...", + "confirmPassword": "Inserèci tuo pcomeswod a csuferma", + "typeHere": "Tipo here", + "fileName": "Inserèci file nome (e.g., example.txt)", + "folderName": "Inserèci cartella nome", + "fullPath": "Inserèci percoso complea", + "currentPath": "Inserèci percoso corente", + "newName": "Inserèci nuovo nome" + }, + "leftSidebar": { + "failedToLoadHosts": "Impossibile caricssuo gli host", + "noFolder": "Nessuna Cartella", + "passwordRequired": "La pcomeswod è obbligaaia", + "failedToDeleteAccount": "Impossibile eliminssuo l'account", + "failedToMakeUserAdmin": "Impossibile rendere l'utente amminètraae", + "userIsNowAdmin": "L'utente {{nome utente}} è oa amminètraae", + "removeAdminConfirm": "Sei sicuro di voler rimuovere lo staa di amminètraae da {{nome utente}}?", + "deleteUserConfirm": "Sei sicuro di voler eliminssuo l'utente {{nome utente}}? Questa azèue nsu può essere annullaa.", + "deleteAccount": "Elimina Account", + "closeDeleteAccount": "Chiudi Elimina Account", + "deleteAccountWarning": "Questa azèue nsu può essere annullaa. Quesa eliminerà permanentemente il tuo account e tutti i dai comesociai.", + "deleteAccountWarningDetails": "Eliminssuo il tuo account rimuoverà tutti i tuoi dai inclusi host SSH, csufigurazèui e impostazèui. Questa azèue è irreversibile.", + "deleteAccountWarningShort": "Questa azèue nsu è reversibile e eliminerà permanentemente il tuo account.", + "cannotDeleteAccount": "Impossibile Eliminssuo Account", + "lastAdminWarning": "Sei l'ultimo utente amminètraae. Nsu puoi eliminssuo il tuo account perché quesa lcomecerebbe il sètema senza amminètraai. Rendi prima un altro utente amminètraae, o csutata il suppotao di sètema.", + "confirmPassword": "Csuferma Pcomeswod", + "deleting": "Eliminazèue...", + "cancel": "Annulla" + }, + "interface": { + "sidebar": "Barra Laerale", + "toggleSidebar": "Mostra/Ncomecsudi Barra Laerale", + "close": "Chiudi", + "online": "Online", + "offline": "Offline", + "maintenance": "Manutenzèue", + "degraded": "Degradaa", + "noTunnelConnections": "Nessuna csunessèue tunnel csufiguraa", + "discord": "Dècod", + "connectToSshForOperations": "Csunettiti a SSH per usssuo le operazèui file", + "uploadFile": "Carica File", + "newFile": "Nuovo File", + "newFolder": "Nuova Cartella", + "rename": "Rinomina", + "deleteItem": "Elimina Elemento", + "createNewFile": "Crea Nuovo File", + "createNewFolder": "Crea Nuova Cartella", + "renameItem": "Rinomina Elemento", + "clickToSelectFile": "Clicca per selezionare un file", + "noSshHosts": "Nessun Host SSH", + "sshHosts": "Host SSH", + "importSshHosts": "Importa Host SSH da JSON", + "clientId": "ID Client", + "clientSecret": "Segreto Client", + "error": "Errore", + "warning": "Avviso", + "deleteAccount": "Elimina Account", + "closeDeleteAccount": "Chiudi Elimina Account", + "cannotDeleteAccount": "Impossibile Eliminare Account", + "confirmPassword": "Conferma Password", + "deleting": "Eliminazione...", + "externalAuth": "Autenticazione Esterna (OIDC)", + "configureExternalProvider": "Configura provider identità esterno per", + "waitingForRetry": "In attesa di riprovare", + "retryingConnection": "Nuovo tentativo di connessione", + "resetSplitSizes": "Ripristina dimensioni divisione", + "sshManagerAlreadyOpen": "Gestore SSH già aperto", + "disabledDuringSplitScreen": "Disabilitato durante schermo diviso", + "unknown": "Sconosciuto", + "connected": "Connesso", + "disconnected": "Disconnesso", + "maxRetriesExhausted": "Tentativi massimi esauriti", + "endpointHostNotFound": "Host endpoint non trovato", + "administrator": "Amministratore", + "user": "Utente", + "external": "Esterno", + "local": "Locale", + "saving": "Salvataggio...", + "saveConfiguration": "Salva Configurazione", + "loading": "Caricamento...", + "refresh": "Aggiorna", + "adding": "Aggiunta...", + "makeAdmin": "Rendi Amministratore", + "deleteItem": "Elimina Elemena", + "createNewFile": "Crea Nuovo File", + "createNewFolder": "Crea Nuova Cartella", + "renameItem": "Rinomina Elemena", + "clickToSelectFile": "Clicca per selezèussuo un file", + "noSshHosts": "Nessun Host SSH", + "sshHosts": "Host SSH", + "importSshHosts": "Impotaaa Host SSH da JSON", + "clientId": "ID Client", + "clientSecret": "Segreto Client", + "error": "Erroee", + "warning": "Avvèo", + "deleteAccount": "Elimina Account", + "closeDeleteAccount": "Chiudi Elimina Account", + "cannotDeleteAccount": "Impossibile Eliminssuo Account", + "confirmPassword": "Csuferma Pcomeswod", + "deleting": "Eliminazèue...", + "externalAuth": "Autenticazèue Esterna (OIDC)", + "configureExternalProvider": "Csufigura provider identità esterno per", + "waitingForRetry": "In atesa di riprovssuo", + "retryingConnection": "Nuovo tentaivo di csunessèue", + "resetSplitSizes": "Riprètina dimensèui divèèue", + "sshManagerAlreadyOpen": "Gesae SSH già apera", + "disabledDuringSplitScreen": "Dèabilitaa durante schermo divèo", + "unknown": "Scsuosciua", + "connected": "Csunesso", + "disconnected": "Dècsunesso", + "maxRetriesExhausted": "Tentaivi mcomesimi esauriti", + "endpointHostNotFound": "Host endpoint nsu trovaa", + "administrator": "Amminètraae", + "user": "Utente", + "external": "Esterno", + "local": "Locale", + "saving": "Salvaaggio...", + "saveConfiguration": "Salva Csufigurazèue", + "loading": "Caricamena...", + "refresh": "Aggèua", + "adding": "Aggiunta...", + "makeAdmin": "Rendi Amminètraae", + "verifying": "Verifica...", + "verifyAndEnable": "Verifica e Abilita", + "secretKey": "Chiave segreta", + "totpQrCode": "Codice QR TOTP", + "passwordRequired": "La password è obbligatoria quando si usa l'autenticazione con password", + "sshKeyRequired": "La chiave privata SSH è obbligatoria quando si usa l'autenticazione con chiave", + "keyTypeRequired": "Il tipo di chiave è obbligatorio quando si usa l'autenticazione con chiave", + "validSshConfigRequired": "Devi selezionare una configurazione SSH valida dalla lista", + "updateHost": "Aggiorna Host", + "addHost": "Aggiungi Host", + "editHost": "Modifica Host", + "pinConnection": "Fissa Connessione", + "authentication": "Autenticazione", + "password": "Password", + "key": "Chiave", + "sshPrivateKey": "Chiave Privata SSH", + "keyPassword": "Password Chiave", + "keyType": "Tipo Chiave", + "enableTerminal": "Abilita Terminale", + "enableTunnel": "Abilita Tunnel", + "enableFileManager": "Abilita Gestione File", + "defaultPath": "Percorso Predefinito", + "tunnelConnections": "Connessioni Tunnel", + "maxRetries": "Tentativi Massimi", + "upload": "Carica", + "updateKey": "Aggiorna Chiave", + "productionFolder": "Produzione", + "databaseServer": "Server Database", + "developmentServer": "Server Sviluppo", + "developmentFolder": "Sviluppo", + "webServerProduction": "Web Server - Produzione", + "unknownError": "Errore sconosciuto", + "failedToInitiatePasswordReset": "Impossibile avviare il reset della password", + "failedToVerifyResetCode": "Impossibile verificare il codice di reset", + "failedToCompletePasswordReset": "Impossibile completare il reset della password", + "invalidTotpCode": "Codice TOTP non valido", + "failedToStartOidcLogin": "Impossibile avviare il login OIDC", + "failedToGetUserInfoAfterOidc": "Impossibile ottenere info utente dopo login OIDC", + "loginWithExternalProvider": "Accedi con provider esterno", + "loginWithExternal": "Accedi con Provider Esterno", + "sendResetCode": "Invia Codice di Reset", + "verifyCode": "Verifica Codice", + "resetPassword": "Reimposta Password", + "login": "Accedi", + "signUp": "Registrati", + "failedToUpdateOidcConfig": "Impossibile aggiornare la configurazione OIDC", + "failedToMakeUserAdmin": "Impossibile rendere l'utente amministratore", + "failedToStartTotpSetup": "Impossibile avviare la configurazione TOTP", + "invalidVerificationCode": "Codice di verifica non valido", + "failedToDisableTotp": "Impossibile disabilitare TOTP", + "failedToGenerateBackupCodes": "Impossibile generare i codici di backup" + }, + "mobile": { + "selectHostToStart": "Seleziona un host per iniziare la sessione terminale", + "limitedSupportMessage": "Il supporto mobile per il sito web è ancora in sviluppo. Usa l'app mobile per un'esperienza migliore.", + "mobileAppInProgress": "App mobile in sviluppo", + "mobileAppInProgressDesc": "Stiamo lavorando a un'app mobile dedicata per fornire un'esperienza migliore sui dispositivi mobili.", + "viewMobileAppDocs": "Installa App Mobile", + "mobileAppDocumentation": "Documentazione App Mobile" + }, + "dashboard": { + "title": "Dashboard", + "github": "GitHub", + "support": "Supporto", + "discord": "Discord", + "donate": "Dona", + "serverOverview": "Panoramica Server", + "version": "Versione", + "upToDate": "Aggiornato", + "updateAvailable": "Aggiornamento Disponibile", + "uptime": "Tempo di Attività", + "database": "Database", + "healthy": "Sano", + "error": "Errore", + "totalServers": "Server Totali", + "totalTunnels": "Tunnel Totali", + "totalCredentials": "Credenziali Totali", + "recentActivity": "Attività Recenti", + "reset": "Ripristina", + "loadingRecentActivity": "Caricamento attività recenti...", + "noRecentActivity": "Nessuna attività recente", + "quickActions": "Azioni Rapide", + "addHost": "Aggiungi Host", + "addCredential": "Aggiungi Credenziale", + "adminSettings": "Impostazioni Amministratore", + "userProfile": "Profilo Utente", + "serverStats": "Statistiche Server", + "loadingServerStats": "Caricamento statistiche server...", + "noServerData": "Nessun dato server disponibile", + "passwordRequired": "La pcomeswod è obbligaaia queo si usa l'autenticazèue csu pcomeswod", + "sshKeyRequired": "La chiave privaa SSH è obbligaaia queo si usa l'autenticazèue csu chiave", + "keyTypeRequired": "Il tipo di chiave è obbligaaio queo si usa l'autenticazèue csu chiave", + "validSshConfigRequired": "Devi selezèussuo una csufigurazèue SSH valida dtuttia lèta", + "updateHost": "Aggèua Host", + "addHost": "Aggiungi Host", + "editHost": "Modifica Host", + "pinConnection": "Fèsa Csunessèue", + "authentication": "Autenticazèue", + "password": "Pcomeswod", + "key": "Chiave", + "sshPrivateKey": "Chiave Privaa SSH", + "keyPassword": "Pcomeswod Chiave", + "keyType": "Tipo Chiave", + "enableTerminal": "Abilita Terminalee", + "enableTunnel": "Abilita Tunnel", + "enableFileManager": "Abilita Gestèue File", + "defaultPath": "Percoso Predefinia", + "tunnelConnections": "Csunessèui Tunnel", + "maxRetries": "Tentaivi Mcomesimi", + "upload": "Carica", + "updateKey": "Aggèua Chiave", + "productionFolder": "Produzèue", + "databaseServer": "Server Daabcomee", + "developmentServer": "Server Sviluppo", + "developmentFolder": "Sviluppo", + "webServerProduction": "Web Server - Produzèue", + "unknownError": "Erroee scsuosciua", + "failedToInitiatePasswordReset": "Impossibile avvèsuo il reset della pcomeswod", + "failedToVerifyResetCode": "Impossibile verificssuo il codice di reset", + "failedToCompletePasswordReset": "Impossibile completssuo il reset della pcomeswod", + "invalidTotpCode": "Codice TOTP nsu valifare", + "failedToStartOidcLogin": "Impossibile avvèsuo il login OIDC", + "failedToGetUserInfoAfterOidc": "Impossibile ottenere info utente farepo login OIDC", + "loginWithExternalProvider": "Accedi csu provider esterno", + "loginWithExternal": "Accedi csu Provider Esterno", + "sendResetCode": "Invia Codice di Reset", + "verifyCode": "Verifica Codice", + "resetPassword": "Reimposta Pcomeswod", + "login": "Accedi", + "signUp": "Regètrai", + "failedToUpdateOidcConfig": "Impossibile aggèussuo la csufigurazèue OIDC", + "failedToMakeUserAdmin": "Impossibile rendere l'utente amminètraae", + "failedToStartTotpSetup": "Impossibile avvèsuo la csufigurazèue TOTP", + "invalidVerificationCode": "Codice di verifica nsu valifare", + "failedToDisableTotp": "Impossibile dèabilitssuo TOTP", + "failedToGenerateBackupCodes": "Impossibile generssuo i codici di indietroup" + }, + "mobile": { + "selectHostToStart": "Selezèua un host per inizèsuo la sessèue terminalee", + "limitedSupportMessage": "Il suppotao mobile per il sia web è ancoa in sviluppo. Usa l'app mobile per un'esperienza miglioe.", + "mobileAppInProgress": "App mobile in sviluppo", + "mobileAppInProgressDesc": "Stiamo lavoeo a un'app mobile dedicaa per pernire un'esperienza miglioe sui dèpositivi mobili.", + "viewMobileAppDocs": "Insttuttia App Mobile", + "mobileAppDocumentation": "Documentazèue App Mobile" + }, + "dashboard": { + "title": "Dcomehboard", + "github": "GitHub", + "support": "Suppotao", + "discord": "Dècod", + "donate": "Dsua", + "serverOverview": "Panoamica Server", + "version": "Versèue", + "upToDate": "Aggèuaa", + "updateAvailable": "Aggèuomena Dèpsuibile", + "uptime": "Tempo di Attività", + "database": "Daabcomee", + "healthy": "Sano", + "error": "Erroee", + "totalServers": "Server Totali", + "totalTunnels": "Tunnel Totali", + "totalCredentials": "Credenziali Totali", + "recentActivity": "Attività Recentii", + "reset": "Riprètina", + "loadingRecentActivity": "Caricamena atività recenti...", + "noRecentActivity": "Nessuna atività recente", + "quickActions": "Azèui Rapide", + "addHost": "Aggiungi Host", + "addCredential": "Aggiungi Credenziale", + "adminSettings": "Impostazèui Amminètraae", + "userProfile": "Prdiilo Utente", + "serverStats": "Staètiche Server", + "loadingServerStats": "Caricamena staètiche server...", + "noServerData": "Nessun daa server dèpsuibile", + "cpu": "CPU", + "ram": "RAM", + "notAvailable": "N/D" + }, + "commandPalette": { + "searchPlaceholder": "Cerca host o azioni rapide...", + "recentActivity": "Attività Recenti", + "navigation": "Navigazione", + "addHost": "Aggiungi Host", + "addCredential": "Aggiungi Credenziale", + "adminSettings": "Impostazioni Amministratore", + "userProfile": "Profilo Utente", + "updateLog": "Log Aggiornamenti", + "hosts": "Host", + "openServerDetails": "Apri Dettagli Server", + "openFileManager": "Apri Gestione File", + "edit": "Modifica", + "links": "Link", + "github": "GitHub", + "support": "Supporto", + "discord": "Discord", + "donate": "Dona", + "press": "Premi", + "toToggle": "per aprire/chiudere", + "close": "Chiudi", + "hostManager": "Gestione Host" + } +} + "searchPlaceholder": "Cerca host o azèui rapide...", + "recentActivity": "Attività Recentii", + "navigation": "Navigazèue", + "addHost": "Aggiungi Host", + "addCredential": "Aggiungi Credenziale", + "adminSettings": "Impostazèui Amminètraae", + "userProfile": "Prdiilo Utente", + "updateLog": "Log Aggèuomenti", + "hosts": "Host", + "openServerDetails": "Apri Dettagli Server", + "openFileManager": "Apri Gestèue File", + "edit": "Modifica", + "links": "Link", + "github": "GitHub", + "support": "Suppotao", + "discord": "Dècod", + "donate": "Dsua", + "press": "Premi", + "toToggle": "per aprire/chiudere", + "close": "Chiudi", + "hostManager": "Gestèue Host" + } +} diff --git a/src/locales/ko/translation.json b/src/locales/ko/translation.json new file mode 100644 index 00000000..ff1dea4c --- /dev/null +++ b/src/locales/ko/translation.json @@ -0,0 +1,1815 @@ +{ + "credentials": { + "credentialsViewer": "자격 증명 뷰어", + "manageYourSSHCredentials": "SSH 자격 증명을 안전하게 관리하세요", + "addCredential": "자격 증명 추가", + "createCredential": "자격 증명 생성", + "editCredential": "자격 증명 편집", + "viewCredential": "자격 증명 보기", + "duplicateCredential": "자격 증명 복제", + "deleteCredential": "자격 증명 삭제", + "updateCredential": "자격 증명 업데이트", + "credentialName": "자격 증명 이름", + "credentialDescription": "설명", + "username": "사용자 이름", + "searchCredentials": "자격 증명 검색...", + "selectFolder": "폴더 선택", + "selectAuthType": "인증 유형 선택", + "allFolders": "모든 폴더", + "allAuthTypes": "모든 인증 유형", + "uncategorized": "미분류", + "totalCredentials": "전체", + "keyBased": "키 기반", + "passwordBased": "비밀번호 기반", + "folders": "폴더", + "noCredentialsMatchFilters": "필터와 일치하는 자격 증명이 없습니다", + "noCredentialsYet": "생성된 자격 증명이 없습니다", + "createFirstCredential": "첫 번째 자격 증명을 생성하세요", + "failedToFetchCredentials": "자격 증명을 가져오는 데 실패했습니다", + "credentialDeletedSuccessfully": "자격 증명이 성공적으로 삭제되었습니다", + "failedToDeleteCredential": "자격 증명 삭제에 실패했습니다", + "confirmDeleteCredential": "자격 증명 \"{{name}}\"을(를) 삭제하시겠습니까?", + "credentialCreatedSuccessfully": "자격 증명이 성공적으로 생성되었습니다", + "credentialUpdatedSuccessfully": "자격 증명이 성공적으로 업데이트되었습니다", + "failedToSaveCredential": "자격 증명 저장에 실패했습니다", + "failedToFetchCredentialDetails": "자격 증명 세부 정보를 가져오는 데 실패했습니다", + "failedToFetchHostsUsing": "이 자격 증명을 사용하는 호스트를 가져오는 데 실패했습니다", + "loadingCredentials": "자격 증명 로드 중...", + "retry": "재시도", + "noCredentials": "자격 증명 없음", + "noCredentialsMessage": "아직 자격 증명을 추가하지 않았습니다. \"자격 증명 추가\"를 클릭하여 시작하세요.", + "sshCredentials": "SSH 자격 증명", + "credentialsCount": "{{count}}개 자격 증명", + "refresh": "새로 고침", + "passwordRequired": "비밀번호가 필요합니다", + "sshKeyRequired": "SSH 키가 필요합니다", + "credentialAddedSuccessfully": "자격 증명 \"{{name}}\"이(가) 성공적으로 추가되었습니다", + "general": "일반", + "description": "설명", + "folder": "폴더", + "tags": "태그", + "addTagsSpaceToAdd": "태그 추가 (스페이스바로 추가)", + "password": "비밀번호", + "key": "키", + "sshPrivateKey": "SSH 개인 키", + "upload": "업로드", + "updateKey": "키 업데이트", + "keyPassword": "키 비밀번호", + "keyType": "키 유형", + "keyTypeRSA": "RSA", + "keyTypeECDSA": "ECDSA", + "keyTypeEd25519": "Ed25519", + "updateCredential": "자격 증명 업데이트", + "basicInfo": "기본 정보", + "authentication": "인증", + "organization": "구성", + "basicInformation": "기본 정보", + "basicInformationDescription": "이 자격 증명의 기본 정보를 입력하세요", + "authenticationMethod": "인증 방법", + "authenticationMethodDescription": "SSH 서버 인증 방법을 선택하세요", + "organizationDescription": "폴더와 태그로 자격 증명을 정리하세요", + "enterCredentialName": "자격 증명 이름 입력", + "enterCredentialDescription": "설명 입력 (선택사항)", + "enterUsername": "사용자 이름 입력", + "nameIsRequired": "자격 증명 이름이 필요합니다", + "usernameIsRequired": "사용자 이름이 필요합니다", + "authenticationType": "인증 유형", + "passwordAuthDescription": "비밀번호 인증 사용", + "sshKeyAuthDescription": "SSH 키 인증 사용", + "passwordIsRequired": "비밀번호가 필요합니다", + "sshKeyIsRequired": "SSH 키가 필요합니다", + "sshKeyType": "SSH 키 유형", + "privateKey": "개인 키", + "enterPassword": "비밀번호 입력", + "enterPrivateKey": "개인 키 입력", + "keyPassphrase": "키 암호구문", + "enterKeyPassphrase": "키 암호구문 입력 (선택사항)", + "keyPassphraseOptional": "선택사항: 키에 암호구문이 없으면 비워두세요", + "leaveEmptyToKeepCurrent": "현재 값을 유지하려면 비워두세요", + "uploadKeyFile": "키 파일 업로드", + "generateKeyPairButton": "키 쌍 생성", + "generateKeyPair": "키 쌍 생성", + "generateKeyPairDescription": "새 SSH 키 쌍을 생성합니다. 암호구문으로 키를 보호하려면 먼저 아래 키 비밀번호 필드에 입력하세요.", + "deploySSHKey": "SSH 키 배포", + "deploySSHKeyDescription": "대상 서버에 공개 키 배포", + "sourceCredential": "소스 자격 증명", + "targetHost": "대상 호스트", + "deploymentProcess": "배포 프로세스", + "deploymentProcessDescription": "대상 호스트의 ~/.ssh/authorized_keys 파일에 기존 키를 덮어쓰지 않고 공개 키를 안전하게 추가합니다. 이 작업은 되돌릴 수 있습니다.", + "chooseHostToDeploy": "배포할 호스트 선택...", + "deploying": "배포 중...", + "name": "이름", + "noHostsAvailable": "사용 가능한 호스트 없음", + "noHostsMatchSearch": "검색과 일치하는 호스트 없음", + "sshKeyGenerationNotImplemented": "SSH 키 생성 기능이 곧 제공됩니다", + "connectionTestingNotImplemented": "연결 테스트 기능이 곧 제공됩니다", + "testConnection": "연결 테스트", + "selectOrCreateFolder": "폴더 선택 또는 생성", + "noFolder": "폴더 없음", + "orCreateNewFolder": "또는 새 폴더 생성", + "addTag": "태그 추가", + "saving": "저장 중...", + "credentialId": "자격 증명 ID", + "overview": "개요", + "security": "보안", + "usage": "사용량", + "securityDetails": "보안 세부 정보", + "securityDetailsDescription": "암호화된 자격 증명 정보 보기", + "credentialSecured": "자격 증명 보안 처리됨", + "credentialSecuredDescription": "모든 민감한 데이터는 AES-256으로 암호화됩니다", + "passwordAuthentication": "비밀번호 인증", + "keyAuthentication": "키 인증", + "keyType": "키 유형", + "securityReminder": "보안 알림", + "securityReminderText": "자격 증명을 절대 공유하지 마세요. 모든 데이터는 저장 시 암호화됩니다.", + "hostsUsingCredential": "이 자격 증명을 사용하는 호스트", + "noHostsUsingCredential": "현재 이 자격 증명을 사용하는 호스트가 없습니다", + "timesUsed": "사용 횟수", + "lastUsed": "마지막 사용", + "connectedHosts": "연결된 호스트", + "created": "생성됨", + "lastModified": "마지막 수정", + "usageStatistics": "사용 통계", + "copiedToClipboard": "{{field}}이(가) 클립보드에 복사되었습니다", + "failedToCopy": "클립보드에 복사 실패", + "sshKey": "SSH 키", + "createCredentialDescription": "보안 액세스를 위한 새 SSH 자격 증명 생성", + "editCredentialDescription": "자격 증명 정보 업데이트", + "listView": "목록", + "folderView": "폴더", + "unknownCredential": "알 수 없음", + "confirmRemoveFromFolder": "폴더 \"{{folder}}\"에서 \"{{name}}\"을(를) 제거하시겠습니까? 자격 증명은 \"미분류\"로 이동됩니다.", + "removedFromFolder": "자격 증명 \"{{name}}\"이(가) 폴더에서 성공적으로 제거되었습니다", + "failedToRemoveFromFolder": "폴더에서 자격 증명 제거에 실패했습니다", + "folderRenamed": "폴더 \"{{oldName}}\"이(가) \"{{newName}}\"(으)로 성공적으로 이름이 변경되었습니다", + "failedToRenameFolder": "폴더 이름 변경에 실패했습니다", + "movedToFolder": "자격 증명 \"{{name}}\"이(가) \"{{folder}}\"(으)로 성공적으로 이동되었습니다", + "failedToMoveToFolder": "자격 증명 이동에 실패했습니다", + "sshPublicKey": "SSH 공개 키", + "publicKeyNote": "공개 키는 선택사항이지만 키 유효성 검사를 위해 권장됩니다", + "publicKeyUploaded": "공개 키 업로드됨", + "uploadPublicKey": "공개 키 업로드", + "uploadPrivateKeyFile": "개인 키 파일 업로드", + "uploadPublicKeyFile": "공개 키 파일 업로드", + "privateKeyRequiredForGeneration": "공개 키 생성을 위해서는 개인 키가 필요합니다", + "failedToGeneratePublicKey": "공개 키 생성에 실패했습니다", + "generatePublicKey": "개인 키에서 생성", + "publicKeyGeneratedSuccessfully": "공개 키가 성공적으로 생성되었습니다", + "detectedKeyType": "감지된 키 유형", + "detectingKeyType": "감지 중...", + "optional": "선택사항", + "generateKeyPairNew": "새 키 쌍 생성", + "generateEd25519": "Ed25519 생성", + "generateECDSA": "ECDSA 생성", + "generateRSA": "RSA 생성", + "keyPairGeneratedSuccessfully": "{{keyType}} 키 쌍이 성공적으로 생성되었습니다", + "failedToGenerateKeyPair": "키 쌍 생성에 실패했습니다", + "generateKeyPairNote": "새 SSH 키 쌍을 직접 생성합니다. 이는 양식의 기존 키를 대체합니다.", + "invalidKey": "잘못된 키", + "detectionError": "감지 오류", + "unknown": "알 수 없음" + }, + "dragIndicator": { + "error": "오류: {{error}}", + "dragging": "{{fileName}} 드래그 중", + "preparing": "{{fileName}} 준비 중", + "readySingle": "{{fileName}} 다운로드 준비 완료", + "readyMultiple": "{{count}}개 파일 다운로드 준비 완료", + "batchDrag": "{{count}}개 파일을 데스크톱으로 드래그", + "dragToDesktop": "데스크톱으로 드래그", + "canDragAnywhere": "데스크톱 어디든 파일을 드래그할 수 있습니다" + }, + "sshTools": { + "title": "SSH 도구", + "closeTools": "SSH 도구 닫기", + "keyRecording": "키 녹화", + "startKeyRecording": "키 녹화 시작", + "stopKeyRecording": "키 녹화 중지", + "selectTerminals": "터미널 선택:", + "typeCommands": "명령어 입력 (모든 키 지원):", + "commandsWillBeSent": "명령어가 {{count}}개의 선택된 터미널로 전송됩니다.", + "settings": "설정", + "enableRightClickCopyPaste": "우클릭 복사/붙여넣기 활성화", + "shareIdeas": "SSH 도구의 다음 기능에 대한 아이디어가 있으신가요? 공유해주세요" + }, + "snippets": { + "title": "스니펫", + "new": "새 스니펫", + "create": "스니펫 생성", + "edit": "스니펫 편집", + "run": "실행", + "empty": "스니펫이 없습니다", + "emptyHint": "자주 사용하는 명령어를 저장하기 위해 스니펫을 생성하세요", + "name": "이름", + "description": "설명", + "content": "명령어", + "namePlaceholder": "예: Nginx 재시작", + "descriptionPlaceholder": "선택적 설명", + "contentPlaceholder": "예: sudo systemctl restart nginx", + "nameRequired": "이름이 필요합니다", + "contentRequired": "명령어가 필요합니다", + "createDescription": "빠른 실행을 위한 새 명령어 스니펫 생성", + "editDescription": "이 명령어 스니펫 편집", + "deleteConfirmTitle": "스니펫 삭제", + "deleteConfirmDescription": "\"{{name}}\"을(를) 삭제하시겠습니까?", + "createSuccess": "스니펫이 성공적으로 생성되었습니다", + "updateSuccess": "스니펫이 성공적으로 업데이트되었습니다", + "deleteSuccess": "스니펫이 성공적으로 삭제되었습니다", + "createFailed": "스니펫 생성에 실패했습니다", + "updateFailed": "스니펫 업데이트에 실패했습니다", + "deleteFailed": "스니펫 삭제에 실패했습니다", + "failedToFetch": "스니펫을 가져오는 데 실패했습니다", + "executeSuccess": "실행 중: {{name}}", + "copySuccess": "\"{{name}}\"이(가) 클립보드에 복사되었습니다", + "runTooltip": "터미널에서 이 스니펫 실행", + "copyTooltip": "스니펫을 클립보드에 복사", + "editTooltip": "이 스니펫 편집", + "deleteTooltip": "이 스니펫 삭제" + }, + "commandHistory": { + "title": "기록", + "searchPlaceholder": "명령어 검색...", + "noTerminal": "활성 터미널 없음", + "noTerminalHint": "터미널을 열어 명령어 기록을 확인하세요.", + "empty": "명령어 기록이 없습니다", + "emptyHint": "활성 터미널에서 명령어를 실행하여 기록을 만드세요.", + "noResults": "명령어를 찾을 수 없습니다", + "noResultsHint": "\"{{query}}\"와(과) 일치하는 명령어 없음", + "deleteSuccess": "기록에서 명령어가 삭제되었습니다", + "deleteFailed": "명령어 삭제에 실패했습니다.", + "deleteTooltip": "명령어 삭제", + "tabHint": "터미널에서 Tab 키를 사용하여 명령어 기록에서 자동 완성" + }, + "homepage": { + "loggedInTitle": "로그인되었습니다!", + "loggedInMessage": "로그인되었습니다! 사이드바를 사용하여 사용 가능한 모든 도구에 액세스하세요. 시작하려면 SSH 관리자 탭에서 SSH 호스트를 생성하세요. 생성 후 사이드바의 다른 앱을 사용하여 해당 호스트에 연결할 수 있습니다.", + "failedToLoadAlerts": "알림을 로드하는 데 실패했습니다", + "failedToDismissAlert": "알림 해제에 실패했습니다" + }, + "serverConfig": { + "title": "서버 구성", + "description": "백엔드 서비스에 연결하기 위한 Termix 서버 URL 구성", + "serverUrl": "서버 URL", + "enterServerUrl": "서버 URL을 입력하세요", + "testConnectionFirst": "먼저 연결을 테스트하세요", + "connectionSuccess": "연결 성공!", + "connectionFailed": "연결 실패", + "connectionError": "연결 오류 발생", + "connected": "연결됨", + "disconnected": "연결 끊김", + "configSaved": "구성이 성공적으로 저장되었습니다", + "saveFailed": "구성 저장에 실패했습니다", + "saveError": "구성 저장 오류", + "saving": "저장 중...", + "saveConfig": "구성 저장", + "helpText": "Termix 서버가 실행 중인 URL을 입력하세요 (예: http://localhost:30001 또는 https://your-server.com)", + "warning": "경고", + "notValidatedWarning": "URL이 검증되지 않았습니다 - 올바른지 확인하세요", + "changeServer": "서버 변경", + "mustIncludeProtocol": "서버 URL은 http:// 또는 https://로 시작해야 합니다" + }, + "versionCheck": { + "error": "버전 확인 오류", + "checkFailed": "업데이트 확인에 실패했습니다", + "upToDate": "앱이 최신 상태입니다", + "currentVersion": "현재 버전 {{version}}을(를) 실행 중입니다", + "updateAvailable": "업데이트 사용 가능", + "newVersionAvailable": "새 버전이 사용 가능합니다! 현재 {{current}}을(를) 실행 중이지만 {{latest}}을(를) 사용할 수 있습니다.", + "releasedOn": "{{date}}에 릴리스됨", + "downloadUpdate": "업데이트 다운로드", + "dismiss": "무시", + "checking": "업데이트 확인 중...", + "checkUpdates": "업데이트 확인", + "checkingUpdates": "업데이트 확인 중...", + "refresh": "새로 고침", + "updateRequired": "업데이트 필요", + "updateDismissed": "업데이트 알림이 무시되었습니다", + "noUpdatesFound": "업데이트를 찾을 수 없습니다" + }, + "common": { + "close": "닫기", + "minimize": "최소화", + "online": "온라인", + "offline": "오프라인", + "continue": "계속", + "maintenance": "유지 관리", + "degraded": "성능 저하", + "discord": "Discord", + "error": "오류", + "warning": "경고", + "info": "정보", + "success": "성공", + "loading": "로드 중", + "required": "필수", + "optional": "선택사항", + "connect": "연결", + "connecting": "연결 중...", + "clear": "지우기", + "toggleSidebar": "사이드바 토글", + "sidebar": "사이드바", + "home": "홈", + "expired": "만료됨", + "expiresToday": "오늘 만료", + "expiresTomorrow": "내일 만료", + "expiresInDays": "{{days}}일 후 만료", + "updateAvailable": "업데이트 사용 가능", + "sshPath": "SSH 경로", + "localPath": "로컬 경로", + "noAuthCredentials": "이 SSH 호스트에 사용 가능한 인증 자격 증명이 없습니다", + "noReleases": "릴리스 없음", + "updatesAndReleases": "업데이트 및 릴리스", + "newVersionAvailable": "새 버전 ({{version}})을(를) 사용할 수 있습니다.", + "failedToFetchUpdateInfo": "업데이트 정보를 가져오는 데 실패했습니다", + "preRelease": "사전 릴리스", + "loginFailed": "로그인 실패", + "noReleasesFound": "릴리스를 찾을 수 없습니다.", + "yourBackupCodes": "백업 코드", + "sendResetCode": "재설정 코드 전송", + "verifyCode": "코드 확인", + "resetPassword": "비밀번호 재설정", + "resetCode": "재설정 코드", + "newPassword": "새 비밀번호", + "folder": "폴더", + "file": "파일", + "renamedSuccessfully": "이름이 성공적으로 변경되었습니다", + "deletedSuccessfully": "성공적으로 삭제되었습니다", + "noTunnelConnections": "구성된 터널 연결이 없습니다", + "sshTools": "SSH 도구", + "english": "English", + "chinese": "Chinese", + "german": "German", + "cancel": "취소", + "username": "사용자 이름", + "name": "이름", + "login": "로그인", + "logout": "로그아웃", + "register": "등록", + "password": "비밀번호", + "version": "버전", + "confirmPassword": "비밀번호 확인", + "back": "뒤로", + "email": "이메일", + "submit": "제출", + "change": "변경", + "save": "저장", + "saving": "저장 중...", + "delete": "삭제", + "edit": "편집", + "add": "추가", + "search": "검색", + "confirm": "확인", + "yes": "예", + "no": "아니오", + "ok": "확인", + "enabled": "활성화됨", + "disabled": "비활성화됨", + "important": "중요", + "notEnabled": "활성화되지 않음", + "settingUp": "설정 중...", + "next": "다음", + "previous": "이전", + "refresh": "새로 고침", + "settings": "설정", + "profile": "프로필", + "help": "도움말", + "about": "정보", + "language": "언어", + "autoDetect": "자동 감지", + "changeAccountPassword": "계정 비밀번호 변경", + "passwordResetTitle": "비밀번호 재설정", + "passwordResetDescription": "비밀번호를 재설정하려고 합니다. 모든 활성 세션에서 로그아웃됩니다.", + "enterSixDigitCode": "사용자의 Docker 컨테이너 로그에서 6자리 코드를 입력하세요:", + "enterNewPassword": "사용자의 새 비밀번호를 입력하세요:", + "passwordsDoNotMatch": "비밀번호가 일치하지 않습니다", + "passwordMinLength": "비밀번호는 최소 6자 이상이어야 합니다", + "passwordResetSuccess": "비밀번호가 성공적으로 재설정되었습니다! 이제 새 비밀번호로 로그인할 수 있습니다.", + "failedToInitiatePasswordReset": "비밀번호 재설정 시작에 실패했습니다", + "failedToVerifyResetCode": "재설정 코드 확인에 실패했습니다", + "failedToCompletePasswordReset": "비밀번호 재설정 완료에 실패했습니다", + "documentation": "문서", + "retry": "재시도", + "checking": "확인 중...", + "checkingDatabase": "데이터베이스 연결 확인 중..." + }, + "nav": { + "home": "홈", + "hosts": "호스트", + "credentials": "자격 증명", + "terminal": "터미널", + "tunnels": "터널", + "fileManager": "파일 관리자", + "serverStats": "서버 통계", + "admin": "관리자", + "userProfile": "사용자 프로필", + "tools": "도구", + "snippets": "스니펫", + "newTab": "새 탭", + "splitScreen": "화면 분할", + "closeTab": "탭 닫기", + "sshManager": "SSH 관리자", + "hostManager": "호스트 관리자", + "cannotSplitTab": "이 탭을 분할할 수 없습니다", + "tabNavigation": "탭 탐색" + }, + "admin": { + "title": "관리자 설정", + "oidc": "OIDC", + "users": "사용자", + "userManagement": "사용자 관리", + "makeAdmin": "관리자로 지정", + "removeAdmin": "관리자 제거", + "deleteUser": "사용자 삭제", + "allowRegistration": "등록 허용", + "oidcSettings": "OIDC 설정", + "clientId": "클라이언트 ID", + "clientSecret": "클라이언트 시크릿", + "issuerUrl": "발급자 URL", + "authorizationUrl": "인증 URL", + "tokenUrl": "토큰 URL", + "updateSettings": "설정 업데이트", + "confirmDelete": "이 사용자를 삭제하시겠습니까?", + "confirmMakeAdmin": "이 사용자를 관리자로 지정하시겠습니까?", + "confirmRemoveAdmin": "이 사용자의 관리자 권한을 제거하시겠습니까?", + "externalAuthentication": "외부 인증 (OIDC)", + "configureExternalProvider": "OIDC/OAuth2 인증을 위한 외부 ID 공급자를 구성합니다.", + "userIdentifierPath": "사용자 식별자 경로", + "displayNamePath": "표시 이름 경로", + "scopes": "범위", + "saving": "저장 중...", + "saveConfiguration": "구성 저장", + "reset": "재설정", + "success": "성공", + "loading": "로드 중...", + "refresh": "새로 고침", + "loadingUsers": "사용자 로드 중...", + "username": "사용자 이름", + "type": "유형", + "actions": "작업", + "external": "외부", + "local": "로컬", + "adminManagement": "관리자 관리", + "makeUserAdmin": "사용자를 관리자로 지정", + "adding": "추가 중...", + "currentAdmins": "현재 관리자", + "adminBadge": "관리자", + "removeAdminButton": "관리자 제거", + "general": "일반", + "userRegistration": "사용자 등록", + "allowNewAccountRegistration": "새 계정 등록 허용", + "allowPasswordLogin": "사용자 이름/비밀번호 로그인 허용", + "missingRequiredFields": "필수 필드 누락: {{fields}}", + "oidcConfigurationUpdated": "OIDC 구성이 성공적으로 업데이트되었습니다!", + "failedToFetchOidcConfig": "OIDC 구성을 가져오는 데 실패했습니다", + "failedToFetchRegistrationStatus": "등록 상태를 가져오는 데 실패했습니다", + "failedToFetchPasswordLoginStatus": "비밀번호 로그인 상태를 가져오는 데 실패했습니다", + "failedToFetchUsers": "사용자를 가져오는 데 실패했습니다", + "oidcConfigurationDisabled": "OIDC 구성이 성공적으로 비활성화되었습니다!", + "failedToUpdateOidcConfig": "OIDC 구성 업데이트에 실패했습니다", + "failedToDisableOidcConfig": "OIDC 구성 비활성화에 실패했습니다", + "enterUsernameToMakeAdmin": "관리자로 지정할 사용자 이름 입력", + "userIsNowAdmin": "사용자 {{username}}이(가) 이제 관리자입니다", + "failedToMakeUserAdmin": "사용자를 관리자로 지정하는 데 실패했습니다", + "removeAdminStatus": "{{username}}의 관리자 상태를 제거하시겠습니까?", + "adminStatusRemoved": "{{username}}의 관리자 상태가 제거되었습니다", + "failedToRemoveAdminStatus": "관리자 상태 제거에 실패했습니다", + "deleteUser": "사용자 {{username}}을(를) 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.", + "userDeletedSuccessfully": "사용자 {{username}}이(가) 성공적으로 삭제되었습니다", + "failedToDeleteUser": "사용자 삭제에 실패했습니다", + "overrideUserInfoUrl": "사용자 정보 URL 재정의 (필수 아님)", + "failedToFetchSessions": "세션을 가져오는 데 실패했습니다", + "sessionRevokedSuccessfully": "세션이 성공적으로 취소되었습니다", + "failedToRevokeSession": "세션 취소에 실패했습니다", + "confirmRevokeSession": "이 세션을 취소하시겠습니까?", + "confirmRevokeAllSessions": "이 사용자의 모든 세션을 취소하시겠습니까?", + "failedToRevokeSessions": "세션 취소에 실패했습니다", + "sessionsRevokedSuccessfully": "세션이 성공적으로 취소되었습니다", + "linkToPasswordAccount": "비밀번호 계정에 연결", + "linkOIDCDialogTitle": "OIDC 계정을 비밀번호 계정에 연결", + "linkOIDCDialogDescription": "{{username}} (OIDC 사용자)를 기존 비밀번호 계정에 연결합니다. 이렇게 하면 비밀번호 계정에 대한 이중 인증이 활성화됩니다.", + "linkOIDCWarningTitle": "경고: OIDC 사용자 데이터가 삭제됩니다", + "linkOIDCActionDeleteUser": "OIDC 사용자 계정 및 모든 데이터 삭제", + "linkOIDCActionAddCapability": "대상 비밀번호 계정에 OIDC 로그인 기능 추가", + "linkOIDCActionDualAuth": "비밀번호 계정이 비밀번호와 OIDC 모두로 로그인할 수 있도록 허용", + "linkTargetUsernameLabel": "대상 비밀번호 계정 사용자 이름", + "linkTargetUsernamePlaceholder": "비밀번호 계정의 사용자 이름 입력", + "linkAccountsButton": "계정 연결", + "linkingAccounts": "연결 중...", + "accountsLinkedSuccessfully": "OIDC 사용자 {{oidcUsername}}이(가) {{targetUsername}}에 연결되었습니다", + "failedToLinkAccounts": "계정 연결에 실패했습니다", + "linkTargetUsernameRequired": "대상 사용자 이름이 필요합니다", + "unlinkOIDCTitle": "OIDC 인증 연결 해제", + "unlinkOIDCDescription": "{{username}}에서 OIDC 인증을 제거하시겠습니까? 사용자는 이후 사용자 이름/비밀번호로만 로그인할 수 있습니다.", + "unlinkOIDCSuccess": "{{username}}에서 OIDC가 연결 해제되었습니다", + "failedToUnlinkOIDC": "OIDC 연결 해제에 실패했습니다", + "databaseSecurity": "데이터베이스 보안", + "encryptionStatus": "암호화 상태", + "encryptionEnabled": "암호화 활성화됨", + "enabled": "활성화됨", + "disabled": "비활성화됨", + "keyId": "키 ID", + "created": "생성됨", + "migrationStatus": "마이그레이션 상태", + "migrationCompleted": "마이그레이션 완료", + "migrationRequired": "마이그레이션 필요", + "deviceProtectedMasterKey": "환경 보호 마스터 키", + "legacyKeyStorage": "레거시 키 저장소", + "masterKeyEncryptedWithDeviceFingerprint": "환경 지문으로 암호화된 마스터 키 (KEK 보호 활성)", + "keyNotProtectedByDeviceBinding": "환경 바인딩으로 보호되지 않는 키 (업그레이드 권장)", + "valid": "유효", + "initializeDatabaseEncryption": "데이터베이스 암호화 초기화", + "enableAes256EncryptionWithDeviceBinding": "환경 바인딩 마스터 키 보호를 사용하여 AES-256 암호화를 활성화합니다. SSH 키, 비밀번호 및 인증 토큰에 대한 엔터프라이즈급 보안을 생성합니다.", + "featuresEnabled": "활성화된 기능:", + "aes256GcmAuthenticatedEncryption": "AES-256-GCM 인증 암호화", + "deviceFingerprintMasterKeyProtection": "환경 지문 마스터 키 보호 (KEK)", + "pbkdf2KeyDerivation": "100K 반복을 사용한 PBKDF2 키 파생", + "automaticKeyManagement": "자동 키 관리 및 순환", + "initializing": "초기화 중...", + "initializeEnterpriseEncryption": "엔터프라이즈 암호화 초기화", + "migrateExistingData": "기존 데이터 마이그레이션", + "encryptExistingUnprotectedData": "데이터베이스의 기존 보호되지 않은 데이터를 암호화합니다. 이 프로세스는 안전하며 자동 백업을 생성합니다.", + "testMigrationDryRun": "암호화 호환성 확인", + "migrating": "마이그레이션 중...", + "migrateData": "데이터 마이그레이션", + "securityInformation": "보안 정보", + "sshPrivateKeysEncryptedWithAes256": "SSH 개인 키 및 비밀번호는 AES-256-GCM으로 암호화됩니다", + "userAuthTokensProtected": "사용자 인증 토큰 및 2FA 비밀이 보호됩니다", + "masterKeysProtectedByDeviceFingerprint": "마스터 암호화 키는 장치 지문(KEK)으로 보호됩니다", + "keysBoundToServerInstance": "키는 현재 서버 환경에 바인딩됩니다 (환경 변수를 통해 마이그레이션 가능)", + "pbkdf2HkdfKeyDerivation": "100K 반복을 사용한 PBKDF2 + HKDF 키 파생", + "backwardCompatibleMigration": "마이그레이션 중에도 모든 데이터는 이전 버전과 호환됩니다", + "enterpriseGradeSecurityActive": "엔터프라이즈급 보안 활성", + "masterKeysProtectedByDeviceBinding": "마스터 암호화 키는 환경 지문으로 보호됩니다. 서버 호스트 이름, 경로 및 기타 환경 정보를 사용하여 보호 키를 생성합니다. 서버를 마이그레이션하려면 새 서버에서 DB_ENCRYPTION_KEY 환경 변수를 설정하세요.", + "important": "중요", + "keepEncryptionKeysSecure": "데이터 보안 보장: 데이터베이스 파일 및 서버 구성을 정기적으로 백업하세요. 새 서버로 마이그레이션하려면 새 환경에서 DB_ENCRYPTION_KEY 환경 변수를 설정하거나 동일한 호스트 이름 및 디렉터리 구조를 유지하세요.", + "loadingEncryptionStatus": "암호화 상태 로드 중...", + "testMigrationDescription": "실제로 데이터를 수정하지 않고 기존 데이터를 암호화된 형식으로 안전하게 마이그레이션할 수 있는지 확인합니다", + "serverMigrationGuide": "서버 마이그레이션 가이드", + "migrationInstructions": "암호화된 데이터를 새 서버로 마이그레이션하려면: 1) 데이터베이스 파일 백업, 2) 새 서버에서 환경 변수 DB_ENCRYPTION_KEY=\"your-key\" 설정, 3) 데이터베이스 파일 복원", + "environmentProtection": "환경 보호", + "environmentProtectionDesc": "서버 환경 정보(호스트 이름, 경로 등)를 기반으로 암호화 키를 보호하며, 환경 변수를 통해 마이그레이션 가능", + "verificationCompleted": "호환성 확인 완료 - 데이터가 변경되지 않았습니다", + "verificationInProgress": "확인 완료", + "dataMigrationCompleted": "데이터 마이그레이션이 성공적으로 완료되었습니다!", + "migrationCompleted": "마이그레이션 완료", + "verificationFailed": "호환성 확인 실패", + "migrationFailed": "마이그레이션 실패", + "runningVerification": "호환성 확인 실행 중...", + "startingMigration": "마이그레이션 시작 중...", + "hardwareFingerprintSecurity": "하드웨어 지문 보안", + "hardwareBoundEncryption": "하드웨어 바인딩 암호화 활성", + "masterKeysNowProtectedByHardwareFingerprint": "마스터 키는 이제 환경 변수 대신 실제 하드웨어 지문으로 보호됩니다", + "cpuSerialNumberDetection": "CPU 일련 번호 감지", + "motherboardUuidIdentification": "마더보드 UUID 식별", + "diskSerialNumberVerification": "디스크 일련 번호 확인", + "biosSerialNumberCheck": "BIOS 일련 번호 확인", + "stableMacAddressFiltering": "안정적인 MAC 주소 필터링", + "databaseFileEncryption": "데이터베이스 파일 암호화", + "dualLayerProtection": "이중 계층 보호 활성", + "bothFieldAndFileEncryptionActive": "최대 보안을 위해 필드 수준 및 파일 수준 암호화가 모두 활성화되었습니다", + "fieldLevelAes256Encryption": "민감한 데이터에 대한 필드 수준 AES-256 암호화", + "fileLevelDatabaseEncryption": "하드웨어 바인딩을 사용한 파일 수준 데이터베이스 암호화", + "hardwareBoundFileKeys": "하드웨어 바인딩 파일 암호화 키", + "automaticEncryptedBackups": "자동 암호화 백업 생성", + "createEncryptedBackup": "암호화된 백업 생성", + "creatingBackup": "백업 생성 중...", + "backupCreated": "백업 생성됨", + "encryptedBackupCreatedSuccessfully": "암호화된 백업이 성공적으로 생성되었습니다", + "backupCreationFailed": "백업 생성 실패", + "databaseMigration": "데이터베이스 마이그레이션", + "exportForMigration": "마이그레이션을 위한 내보내기", + "exportDatabaseForHardwareMigration": "새 하드웨어로 마이그레이션하기 위해 복호화된 데이터가 포함된 SQLite 파일로 데이터베이스 내보내기", + "exportDatabase": "SQLite 데이터베이스 내보내기", + "exporting": "내보내기 중...", + "exportCreated": "SQLite 내보내기 생성됨", + "exportContainsDecryptedData": "SQLite 내보내기에는 복호화된 데이터가 포함되어 있습니다 - 안전하게 보관하세요!", + "databaseExportedSuccessfully": "SQLite 데이터베이스가 성공적으로 내보내졌습니다", + "databaseExportFailed": "SQLite 데이터베이스 내보내기 실패", + "importFromMigration": "마이그레이션에서 가져오기", + "importDatabaseFromAnotherSystem": "다른 시스템 또는 하드웨어에서 SQLite 데이터베이스 가져오기", + "importDatabase": "SQLite 데이터베이스 가져오기", + "importing": "가져오기 중...", + "selectedFile": "선택된 SQLite 파일", + "importWillReplaceExistingData": "SQLite 가져오기는 기존 데이터를 대체합니다 - 백업 권장!", + "pleaseSelectImportFile": "SQLite 가져오기 파일을 선택하세요", + "databaseImportedSuccessfully": "SQLite 데이터베이스가 성공적으로 가져와졌습니다", + "databaseImportFailed": "SQLite 데이터베이스 가져오기 실패", + "manageEncryptionAndBackups": "암호화 키, 데이터베이스 보안 및 백업 작업 관리", + "activeSecurityFeatures": "현재 활성화된 보안 조치 및 보호", + "deviceBindingTechnology": "고급 하드웨어 기반 키 보호 기술", + "backupAndRecovery": "안전한 백업 생성 및 데이터베이스 복구 옵션", + "crossSystemDataTransfer": "다른 시스템 간 데이터베이스 내보내기 및 가져오기", + "noMigrationNeeded": "마이그레이션 불필요", + "encryptionKey": "암호화 키", + "keyProtection": "키 보호", + "active": "활성", + "legacy": "레거시", + "dataStatus": "데이터 상태", + "encrypted": "암호화됨", + "needsMigration": "마이그레이션 필요", + "ready": "준비됨", + "initializeEncryption": "암호화 초기화", + "initialize": "초기화", + "test": "테스트", + "migrate": "마이그레이션", + "backup": "백업", + "createBackup": "백업 생성", + "exportImport": "내보내기/가져오기", + "export": "내보내기", + "import": "가져오기", + "passwordRequired": "비밀번호 필요", + "confirmExport": "내보내기 확인", + "exportDescription": "SSH 호스트 및 자격 증명을 SQLite 파일로 내보내기", + "importDescription": "증분 병합으로 SQLite 파일 가져오기 (중복 건너뛰기)", + "criticalWarning": "중요 경고", + "cannotDisablePasswordLoginWithoutOIDC": "OIDC가 구성되지 않은 상태에서는 비밀번호 로그인을 비활성화할 수 없습니다! 비밀번호 로그인을 비활성화하기 전에 OIDC 인증을 구성해야 합니다. 그렇지 않으면 Termix에 액세스할 수 없게 됩니다.", + "confirmDisablePasswordLogin": "비밀번호 로그인을 비활성화하시겠습니까? 계속하기 전에 OIDC가 올바르게 구성되고 작동하는지 확인하세요. 그렇지 않으면 Termix 인스턴스에 액세스할 수 없게 됩니다.", + "passwordLoginDisabled": "비밀번호 로그인이 성공적으로 비활성화되었습니다", + "passwordLoginAndRegistrationDisabled": "비밀번호 로그인 및 새 계정 등록이 성공적으로 비활성화되었습니다", + "requiresPasswordLogin": "비밀번호 로그인 활성화 필요", + "passwordLoginDisabledWarning": "비밀번호 로그인이 비활성화되었습니다. OIDC가 올바르게 구성되어 있는지 확인하세요. 그렇지 않으면 Termix에 로그인할 수 없습니다.", + "oidcRequiredWarning": "중요: 비밀번호 로그인이 비활성화되었습니다. OIDC를 재설정하거나 잘못 구성하면 Termix에 대한 모든 액세스 권한을 잃고 인스턴스가 작동하지 않게 됩니다. 절대적으로 확신하는 경우에만 진행하세요.", + "confirmDisableOIDCWarning": "경고: 비밀번호 로그인도 비활성화된 상태에서 OIDC를 비활성화하려고 합니다. 이렇게 하면 Termix 인스턴스가 작동하지 않게 되고 모든 액세스 권한을 잃게 됩니다. 정말로 진행하시겠습니까?" + }, + "hosts": { + "title": "호스트 관리자", + "sshHosts": "SSH 호스트", + "noHosts": "SSH 호스트 없음", + "noHostsMessage": "아직 SSH 호스트를 추가하지 않았습니다. \"호스트 추가\"를 클릭하여 시작하세요.", + "loadingHosts": "호스트 로드 중...", + "failedToLoadHosts": "호스트 로드 실패", + "retry": "재시도", + "refresh": "새로 고침", + "hostsCount": "{{count}}개 호스트", + "importJson": "JSON 가져오기", + "importing": "가져오기 중...", + "importJsonTitle": "JSON에서 SSH 호스트 가져오기", + "importJsonDesc": "JSON 파일을 업로드하여 여러 SSH 호스트를 일괄 가져오기 (최대 100개).", + "downloadSample": "샘플 다운로드", + "formatGuide": "형식 가이드", + "exportCredentialWarning": "경고: 호스트 \"{{name}}\"은(는) 자격 증명 인증을 사용합니다. 내보낸 파일에는 자격 증명 데이터가 포함되지 않으며 가져오기 후 수동으로 다시 구성해야 합니다. 계속하시겠습니까?", + "exportSensitiveDataWarning": "경고: 호스트 \"{{name}}\"에는 민감한 인증 데이터(비밀번호/SSH 키)가 포함되어 있습니다. 내보낸 파일에는 이 데이터가 평문으로 포함됩니다. 파일을 안전하게 보관하고 사용 후 삭제하세요. 계속하시겠습니까?", + "uncategorized": "미분류", + "confirmDelete": "\"{{name}}\"을(를) 삭제하시겠습니까?", + "failedToDeleteHost": "호스트 삭제 실패", + "failedToExportHost": "호스트 내보내기 실패. 로그인되어 있고 호스트 데이터에 액세스할 수 있는지 확인하세요.", + "jsonMustContainHosts": "JSON에는 \"hosts\" 배열이 포함되어 있거나 호스트 배열이어야 합니다", + "noHostsInJson": "JSON 파일에서 호스트를 찾을 수 없습니다", + "maxHostsAllowed": "가져오기당 최대 100개 호스트 허용", + "importCompleted": "가져오기 완료: {{success}}개 성공, {{failed}}개 실패", + "importFailed": "가져오기 실패", + "importError": "가져오기 오류", + "failedToImportJson": "JSON 파일 가져오기 실패", + "connectionDetails": "연결 세부 정보", + "organization": "구성", + "ipAddress": "IP 주소", + "port": "포트", + "name": "이름", + "username": "사용자 이름", + "folder": "폴더", + "tags": "태그", + "pin": "고정", + "passwordRequired": "비밀번호 인증을 사용할 때 비밀번호가 필요합니다", + "sshKeyRequired": "키 인증을 사용할 때 SSH 개인 키가 필요합니다", + "keyTypeRequired": "키 인증을 사용할 때 키 유형이 필요합니다", + "mustSelectValidSshConfig": "목록에서 유효한 SSH 구성을 선택해야 합니다", + "addHost": "호스트 추가", + "editHost": "호스트 편집", + "cloneHost": "호스트 복제", + "updateHost": "호스트 업데이트", + "hostUpdatedSuccessfully": "호스트 \"{{name}}\"이(가) 성공적으로 업데이트되었습니다!", + "hostAddedSuccessfully": "호스트 \"{{name}}\"이(가) 성공적으로 추가되었습니다!", + "hostDeletedSuccessfully": "호스트 \"{{name}}\"이(가) 성공적으로 삭제되었습니다!", + "failedToSaveHost": "호스트 저장 실패. 다시 시도하세요.", + "enableTerminal": "터미널 활성화", + "enableTerminalDesc": "터미널 탭에서 호스트 표시 활성화/비활성화", + "enableTunnel": "터널 활성화", + "enableTunnelDesc": "터널 탭에서 호스트 표시 활성화/비활성화", + "enableFileManager": "파일 관리자 활성화", + "enableFileManagerDesc": "파일 관리자 탭에서 호스트 표시 활성화/비활성화", + "defaultPath": "기본 경로", + "defaultPathDesc": "이 호스트의 파일 관리자를 열 때 기본 디렉터리", + "tunnelConnections": "터널 연결", + "connection": "연결", + "remove": "제거", + "sourcePort": "소스 포트", + "sourcePortDesc": " (소스는 일반 탭의 현재 연결 세부 정보를 나타냅니다)", + "endpointPort": "엔드포인트 포트", + "endpointSshConfig": "엔드포인트 SSH 구성", + "tunnelForwardDescription": "이 터널은 소스 머신(일반 탭의 현재 연결 세부 정보)의 포트 {{sourcePort}}에서 엔드포인트 머신의 포트 {{endpointPort}}로 트래픽을 전달합니다.", + "maxRetries": "최대 재시도 횟수", + "maxRetriesDescription": "터널 연결에 대한 최대 재시도 횟수.", + "retryInterval": "재시도 간격 (초)", + "retryIntervalDescription": "재시도 시도 사이의 대기 시간.", + "autoStartContainer": "컨테이너 시작 시 자동 시작", + "autoStartDesc": "컨테이너가 시작될 때 이 터널을 자동으로 시작합니다", + "addConnection": "터널 연결 추가", + "sshpassRequired": "비밀번호 인증에 Sshpass 필요", + "sshpassRequiredDesc": "터널에서 비밀번호 인증을 사용하려면 시스템에 sshpass가 설치되어 있어야 합니다.", + "otherInstallMethods": "기타 설치 방법:", + "debianUbuntuEquivalent": "(Debian/Ubuntu) 또는 OS에 해당하는 방법.", + "or": "또는", + "centosRhelFedora": "CentOS/RHEL/Fedora", + "macos": "macOS", + "windows": "Windows", + "sshServerConfigRequired": "SSH 서버 구성 필요", + "sshServerConfigDesc": "터널 연결의 경우 SSH 서버가 포트 포워딩을 허용하도록 구성되어야 합니다:", + "gatewayPortsYes": "모든 인터페이스에 원격 포트를 바인딩하려면", + "allowTcpForwardingYes": "포트 포워딩을 활성화하려면", + "permitRootLoginYes": "터널링에 루트 사용자를 사용하는 경우", + "editSshConfig": "/etc/ssh/sshd_config를 편집하고 SSH를 재시작하세요: sudo systemctl restart sshd", + "upload": "업로드", + "authentication": "인증", + "password": "비밀번호", + "key": "키", + "credential": "자격 증명", + "none": "없음", + "selectCredential": "자격 증명 선택", + "selectCredentialPlaceholder": "자격 증명 선택...", + "credentialRequired": "자격 증명 인증을 사용할 때 자격 증명이 필요합니다", + "credentialDescription": "자격 증명을 선택하면 현재 사용자 이름을 덮어쓰고 자격 증명의 인증 세부 정보를 사용합니다.", + "sshPrivateKey": "SSH 개인 키", + "keyPassword": "키 비밀번호", + "keyType": "키 유형", + "autoDetect": "자동 감지", + "rsa": "RSA", + "ed25519": "ED25519", + "ecdsaNistP256": "ECDSA NIST P-256", + "ecdsaNistP384": "ECDSA NIST P-384", + "ecdsaNistP521": "ECDSA NIST P-521", + "dsa": "DSA", + "rsaSha2256": "RSA SHA2-256", + "rsaSha2512": "RSA SHA2-512", + "uploadFile": "파일 업로드", + "pasteKey": "키 붙여넣기", + "updateKey": "키 업데이트", + "existingKey": "기존 키 (클릭하여 변경)", + "existingCredential": "기존 자격 증명 (클릭하여 변경)", + "addTagsSpaceToAdd": "태그 추가 (스페이스바로 추가)", + "terminalBadge": "터미널", + "tunnelBadge": "터널", + "fileManagerBadge": "파일 관리자", + "general": "일반", + "terminal": "터미널", + "tunnel": "터널", + "fileManager": "파일 관리자", + "serverStats": "서버 통계", + "hostViewer": "호스트 뷰어", + "enableServerStats": "서버 통계 활성화", + "enableServerStatsDesc": "이 호스트에 대한 서버 통계 수집 활성화/비활성화", + "displayItems": "표시 항목", + "displayItemsDesc": "서버 통계 페이지에 표시할 메트릭 선택", + "enableCpu": "CPU 사용량", + "enableMemory": "메모리 사용량", + "enableDisk": "디스크 사용량", + "enableNetwork": "네트워크 통계 (곧 제공)", + "enableProcesses": "프로세스 수 (곧 제공)", + "enableUptime": "가동 시간 (곧 제공)", + "enableHostname": "호스트 이름 (곧 제공)", + "enableOs": "운영 체제 (곧 제공)", + "customCommands": "사용자 지정 명령 (곧 제공)", + "customCommandsDesc": "이 서버에 대한 사용자 지정 종료 및 재부팅 명령 정의", + "shutdownCommand": "종료 명령", + "rebootCommand": "재부팅 명령", + "confirmRemoveFromFolder": "폴더 \"{{folder}}\"에서 \"{{name}}\"을(를) 제거하시겠습니까? 호스트는 \"폴더 없음\"으로 이동됩니다.", + "removedFromFolder": "호스트 \"{{name}}\"이(가) 폴더에서 성공적으로 제거되었습니다", + "failedToRemoveFromFolder": "폴더에서 호스트 제거 실패", + "folderRenamed": "폴더 \"{{oldName}}\"이(가) \"{{newName}}\"(으)로 성공적으로 이름이 변경되었습니다", + "failedToRenameFolder": "폴더 이름 변경 실패", + "editFolderAppearance": "폴더 모양 편집", + "editFolderAppearanceDesc": "폴더의 색상 및 아이콘 사용자 지정", + "folderColor": "폴더 색상", + "folderIcon": "폴더 아이콘", + "preview": "미리보기", + "folderAppearanceUpdated": "폴더 모양이 성공적으로 업데이트되었습니다", + "failedToUpdateFolderAppearance": "폴더 모양 업데이트 실패", + "deleteAllHostsInFolder": "폴더의 모든 호스트 삭제", + "confirmDeleteAllHostsInFolder": "폴더 \"{{folder}}\"의 모든 {{count}}개 호스트를 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.", + "allHostsInFolderDeleted": "폴더 \"{{folder}}\"에서 {{count}}개 호스트가 성공적으로 삭제되었습니다", + "failedToDeleteHostsInFolder": "폴더의 호스트 삭제 실패", + "movedToFolder": "호스트 \"{{name}}\"이(가) \"{{folder}}\"(으)로 성공적으로 이동되었습니다", + "failedToMoveToFolder": "호스트를 폴더로 이동하는 데 실패했습니다", + "statistics": "통계", + "enabledWidgets": "활성화된 위젯", + "enabledWidgetsDesc": "이 호스트에 대해 표시할 통계 위젯 선택", + "monitoringConfiguration": "모니터링 구성", + "monitoringConfigurationDesc": "서버 통계 및 상태를 확인하는 빈도 구성", + "statusCheckEnabled": "상태 모니터링 활성화", + "statusCheckEnabledDesc": "서버가 온라인인지 오프라인인지 확인", + "statusCheckInterval": "상태 확인 간격", + "statusCheckIntervalDesc": "호스트가 온라인인지 확인하는 빈도 (5초 - 1시간)", + "metricsEnabled": "메트릭 모니터링 활성화", + "metricsEnabledDesc": "CPU, RAM, 디스크 및 기타 시스템 통계 수집", + "metricsInterval": "메트릭 수집 간격", + "metricsIntervalDesc": "서버 통계를 수집하는 빈도 (5초 - 1시간)", + "intervalSeconds": "초", + "intervalMinutes": "분", + "intervalValidation": "모니터링 간격은 5초에서 1시간(3600초) 사이여야 합니다", + "monitoringDisabled": "이 호스트에 대한 서버 모니터링이 비활성화되었습니다", + "enableMonitoring": "호스트 관리자 → 통계 탭에서 모니터링 활성화", + "monitoringDisabledBadge": "모니터링 꺼짐", + "statusMonitoring": "상태", + "metricsMonitoring": "메트릭", + "terminalCustomizationNotice": "참고: 터미널 사용자 지정은 데스크톱(웹사이트 및 Electron 앱)에서만 작동합니다. 모바일 앱 및 모바일 웹사이트는 시스템 기본 터미널 설정을 사용합니다.", + "terminalCustomization": "터미널 사용자 지정", + "appearance": "모양", + "behavior": "동작", + "advanced": "고급", + "themePreview": "테마 미리보기", + "theme": "테마", + "selectTheme": "테마 선택", + "chooseColorTheme": "터미널의 색상 테마 선택", + "fontFamily": "글꼴", + "selectFont": "글꼴 선택", + "selectFontDesc": "터미널에서 사용할 글꼴 선택", + "fontSize": "글꼴 크기", + "fontSizeValue": "글꼴 크기: {{value}}px", + "adjustFontSize": "터미널 글꼴 크기 조정", + "letterSpacing": "자간", + "letterSpacingValue": "자간: {{value}}px", + "adjustLetterSpacing": "문자 간 간격 조정", + "lineHeight": "줄 높이", + "lineHeightValue": "줄 높이: {{value}}", + "adjustLineHeight": "줄 간 간격 조정", + "cursorStyle": "커서 스타일", + "selectCursorStyle": "커서 스타일 선택", + "cursorStyleBlock": "블록", + "cursorStyleUnderline": "밑줄", + "cursorStyleBar": "바", + "chooseCursorAppearance": "커서 모양 선택", + "cursorBlink": "커서 깜박임", + "enableCursorBlink": "커서 깜박임 애니메이션 활성화", + "scrollbackBuffer": "스크롤백 버퍼", + "scrollbackBufferValue": "스크롤백 버퍼: {{value}}줄", + "scrollbackBufferDesc": "스크롤백 기록에 유지할 줄 수", + "bellStyle": "벨 스타일", + "selectBellStyle": "벨 스타일 선택", + "bellStyleNone": "없음", + "bellStyleSound": "소리", + "bellStyleVisual": "시각적", + "bellStyleBoth": "둘 다", + "bellStyleDesc": "터미널 벨(BEL 문자, \\x07) 처리 방법. 프로그램은 작업 완료, 오류 발생 또는 알림을 위해 이를 트리거합니다. \"소리\"는 오디오 비프음을 재생하고, \"시각적\"은 화면을 잠깐 깜박이며, \"둘 다\"는 둘 다 수행하고, \"없음\"은 벨 알림을 비활성화합니다.", + "rightClickSelectsWord": "우클릭으로 단어 선택", + "rightClickSelectsWordDesc": "우클릭하면 커서 아래의 단어를 선택합니다", + "fastScrollModifier": "빠른 스크롤 수정자", + "selectModifier": "수정자 선택", + "modifierAlt": "Alt", + "modifierCtrl": "Ctrl", + "modifierShift": "Shift", + "fastScrollModifierDesc": "빠른 스크롤을 위한 수정자 키", + "fastScrollSensitivity": "빠른 스크롤 감도", + "fastScrollSensitivityValue": "빠른 스크롤 감도: {{value}}", + "fastScrollSensitivityDesc": "수정자를 누르고 있을 때 스크롤 속도 배수", + "minimumContrastRatio": "최소 대비율", + "minimumContrastRatioValue": "최소 대비율: {{value}}", + "minimumContrastRatioDesc": "가독성 향상을 위해 색상 자동 조정", + "sshAgentForwarding": "SSH 에이전트 포워딩", + "sshAgentForwardingDesc": "SSH 인증 에이전트를 원격 호스트로 전달", + "backspaceMode": "백스페이스 모드", + "selectBackspaceMode": "백스페이스 모드 선택", + "backspaceModeNormal": "일반 (DEL)", + "backspaceModeControlH": "Control-H (^H)", + "backspaceModeDesc": "호환성을 위한 백스페이스 키 동작", + "startupSnippet": "시작 스니펫", + "selectSnippet": "스니펫 선택", + "searchSnippets": "스니펫 검색...", + "snippetNone": "없음", + "noneAuthTitle": "키보드 대화형 인증", + "noneAuthDescription": "이 인증 방법은 SSH 서버에 연결할 때 키보드 대화형 인증을 사용합니다.", + "noneAuthDetails": "키보드 대화형 인증을 사용하면 서버가 연결 중에 자격 증명을 요청할 수 있습니다. 다단계 인증이 필요한 서버나 자격 증명을 로컬에 저장하지 않으려는 경우에 유용합니다.", + "forceKeyboardInteractive": "키보드 대화형 강제", + "forceKeyboardInteractiveDesc": "키보드 대화형 인증을 강제로 사용합니다. 이는 2단계 인증(TOTP/2FA)을 사용하는 서버에 종종 필요합니다.", + "overrideCredentialUsername": "자격 증명 사용자 이름 재정의", + "overrideCredentialUsernameDesc": "자격 증명에 저장된 것과 다른 사용자 이름을 사용합니다. 이를 통해 동일한 자격 증명을 다른 사용자 이름과 함께 사용할 수 있습니다.", + "jumpHosts": "점프 호스트", + "jumpHostsDescription": "점프 호스트(배스천 호스트라고도 함)를 사용하면 하나 이상의 중간 서버를 통해 대상 서버에 연결할 수 있습니다. 방화벽 뒤 또는 개인 네트워크의 서버에 액세스하는 데 유용합니다.", + "jumpHostChain": "점프 호스트 체인", + "addJumpHost": "점프 호스트 추가", + "selectServer": "서버 선택", + "searchServers": "서버 검색...", + "noServerFound": "서버를 찾을 수 없습니다", + "jumpHostsOrder": "연결은 다음 순서로 이루어집니다: 점프 호스트 1 → 점프 호스트 2 → ... → 대상 서버", + "quickActions": "빠른 작업", + "quickActionsDescription": "빠른 작업을 사용하면 이 서버에서 SSH 스니펫을 실행하는 사용자 지정 버튼을 만들 수 있습니다. 이러한 버튼은 빠른 액세스를 위해 서버 통계 페이지 상단에 나타납니다.", + "quickActionsList": "빠른 작업 목록", + "addQuickAction": "빠른 작업 추가", + "quickActionName": "작업 이름", + "noSnippetFound": "스니펫을 찾을 수 없습니다", + "quickActionsOrder": "빠른 작업 버튼은 서버 통계 페이지에 위에 나열된 순서대로 나타납니다", + "advancedAuthSettings": "고급 인증 설정" + }, + "terminal": { + "title": "터미널", + "connect": "호스트에 연결", + "disconnect": "연결 해제", + "clear": "지우기", + "copy": "복사", + "paste": "붙여넣기", + "find": "찾기", + "fullscreen": "전체 화면", + "splitHorizontal": "가로 분할", + "splitVertical": "세로 분할", + "closePanel": "패널 닫기", + "reconnect": "재연결", + "sessionEnded": "세션 종료됨", + "connectionLost": "연결 끊김", + "error": "오류: {{message}}", + "disconnected": "연결 해제됨", + "connectionClosed": "연결이 닫혔습니다", + "connectionError": "연결 오류: {{message}}", + "connected": "연결됨", + "sshConnected": "SSH 연결이 설정되었습니다", + "authError": "인증 실패: {{message}}", + "unknownError": "알 수 없는 오류가 발생했습니다", + "messageParseError": "서버 메시지 파싱 실패", + "websocketError": "WebSocket 연결 오류", + "connecting": "연결 중...", + "reconnecting": "재연결 중... ({{attempt}}/{{max}})", + "reconnected": "성공적으로 재연결되었습니다", + "maxReconnectAttemptsReached": "최대 재연결 시도 횟수에 도달했습니다", + "connectionTimeout": "연결 시간 초과", + "terminalTitle": "터미널 - {{host}}", + "terminalWithPath": "터미널 - {{host}}:{{path}}", + "runTitle": "{{command}} 실행 중 - {{host}}", + "totpRequired": "2단계 인증 필요", + "totpCodeLabel": "인증 코드", + "totpPlaceholder": "000000", + "totpVerify": "확인" + }, + "fileManager": { + "title": "파일 관리자", + "file": "파일", + "folder": "폴더", + "connectToSsh": "파일 작업을 사용하려면 SSH에 연결하세요", + "uploadFile": "파일 업로드", + "downloadFile": "다운로드", + "extractArchive": "압축 해제", + "extractingArchive": "{{name}} 압축 해제 중...", + "archiveExtractedSuccessfully": "{{name}}이(가) 성공적으로 압축 해제되었습니다", + "extractFailed": "압축 해제 실패", + "compressFile": "파일 압축", + "compressFiles": "파일 압축", + "compressFilesDesc": "{{count}}개 항목을 압축 파일로 압축", + "archiveName": "압축 파일 이름", + "enterArchiveName": "압축 파일 이름 입력...", + "compressionFormat": "압축 형식", + "selectedFiles": "선택된 파일", + "andMoreFiles": "및 {{count}}개 더...", + "compress": "압축", + "compressingFiles": "{{count}}개 항목을 {{name}}(으)로 압축 중...", + "filesCompressedSuccessfully": "{{name}}이(가) 성공적으로 생성되었습니다", + "compressFailed": "압축 실패", + "edit": "편집", + "preview": "미리보기", + "previous": "이전", + "next": "다음", + "pageXOfY": "{{current}} / {{total}} 페이지", + "zoomOut": "축소", + "zoomIn": "확대", + "newFile": "새 파일", + "newFolder": "새 폴더", + "rename": "이름 변경", + "renameItem": "항목 이름 변경", + "deleteItem": "항목 삭제", + "currentPath": "현재 경로", + "uploadFileTitle": "파일 업로드", + "maxFileSize": "최대: 1GB (JSON) / 5GB (바이너리) - 대용량 파일 지원", + "removeFile": "파일 제거", + "clickToSelectFile": "클릭하여 파일 선택", + "chooseFile": "파일 선택", + "uploading": "업로드 중...", + "downloading": "다운로드 중...", + "uploadingFile": "{{name}} 업로드 중...", + "uploadingLargeFile": "대용량 파일 {{name}} ({{size}}) 업로드 중...", + "downloadingFile": "{{name}} 다운로드 중...", + "creatingFile": "{{name}} 생성 중...", + "creatingFolder": "{{name}} 생성 중...", + "deletingItem": "{{type}} {{name}} 삭제 중...", + "renamingItem": "{{type}} {{oldName}}을(를) {{newName}}(으)로 이름 변경 중...", + "createNewFile": "새 파일 생성", + "fileName": "파일 이름", + "creating": "생성 중...", + "createFile": "파일 생성", + "createNewFolder": "새 폴더 생성", + "folderName": "폴더 이름", + "createFolder": "폴더 생성", + "warningCannotUndo": "경고: 이 작업은 취소할 수 없습니다", + "itemPath": "항목 경로", + "thisIsDirectory": "디렉터리입니다 (재귀적으로 삭제됩니다)", + "deleting": "삭제 중...", + "currentPathLabel": "현재 경로", + "newName": "새 이름", + "thisIsDirectoryRename": "디렉터리입니다", + "renaming": "이름 변경 중...", + "fileUploadedSuccessfully": "파일 \"{{name}}\"이(가) 성공적으로 업로드되었습니다", + "failedToUploadFile": "파일 업로드 실패", + "fileDownloadedSuccessfully": "파일 \"{{name}}\"이(가) 성공적으로 다운로드되었습니다", + "failedToDownloadFile": "파일 다운로드 실패", + "noFileContent": "파일 내용이 수신되지 않았습니다", + "filePath": "파일 경로", + "fileCreatedSuccessfully": "파일 \"{{name}}\"이(가) 성공적으로 생성되었습니다", + "failedToCreateFile": "파일 생성 실패", + "folderCreatedSuccessfully": "폴더 \"{{name}}\"이(가) 성공적으로 생성되었습니다", + "failedToCreateFolder": "폴더 생성 실패", + "failedToCreateItem": "항목 생성 실패", + "operationFailed": "{{name}}에 대한 {{operation}} 작업 실패: {{error}}", + "failedToResolveSymlink": "심볼릭 링크 해석 실패", + "itemDeletedSuccessfully": "{{type}}이(가) 성공적으로 삭제되었습니다", + "itemsDeletedSuccessfully": "{{count}}개 항목이 성공적으로 삭제되었습니다", + "failedToDeleteItems": "항목 삭제 실패", + "dragFilesToUpload": "업로드할 파일을 여기에 드롭하세요", + "emptyFolder": "이 폴더는 비어 있습니다", + "itemCount": "{{count}}개 항목", + "selectedCount": "{{count}}개 선택됨", + "searchFiles": "파일 검색...", + "upload": "업로드", + "selectHostToStart": "파일 관리를 시작하려면 호스트를 선택하세요", + "failedToConnect": "SSH 연결 실패", + "failedToLoadDirectory": "디렉터리 로드 실패", + "noSSHConnection": "사용 가능한 SSH 연결이 없습니다", + "enterFolderName": "폴더 이름 입력:", + "enterFileName": "파일 이름 입력:", + "copy": "복사", + "cut": "잘라내기", + "paste": "붙여넣기", + "copyPath": "경로 복사", + "copyPaths": "경로 복사", + "delete": "삭제", + "properties": "속성", + "preview": "미리보기", + "refresh": "새로 고침", + "downloadFiles": "{{count}}개 파일을 브라우저로 다운로드", + "copyFiles": "{{count}}개 항목 복사", + "cutFiles": "{{count}}개 항목 잘라내기", + "deleteFiles": "{{count}}개 항목 삭제", + "filesCopiedToClipboard": "{{count}}개 항목이 클립보드에 복사되었습니다", + "filesCutToClipboard": "{{count}}개 항목이 클립보드에 잘라내기되었습니다", + "pathCopiedToClipboard": "경로가 클립보드에 복사되었습니다", + "pathsCopiedToClipboard": "{{count}}개 경로가 클립보드에 복사되었습니다", + "failedToCopyPath": "경로를 클립보드에 복사하는 데 실패했습니다", + "movedItems": "{{count}}개 항목이 이동되었습니다", + "failedToDeleteItem": "항목 삭제 실패", + "itemRenamedSuccessfully": "{{type}}의 이름이 성공적으로 변경되었습니다", + "failedToRenameItem": "항목 이름 변경 실패", + "upload": "업로드", + "download": "다운로드", + "newFile": "새 파일", + "newFolder": "새 폴더", + "rename": "이름 변경", + "delete": "삭제", + "permissions": "권한", + "size": "크기", + "modified": "수정됨", + "path": "경로", + "fileName": "파일 이름", + "folderName": "폴더 이름", + "confirmDelete": "{{name}}을(를) 삭제하시겠습니까?", + "uploadSuccess": "파일이 성공적으로 업로드되었습니다", + "uploadFailed": "파일 업로드 실패", + "downloadSuccess": "파일이 성공적으로 다운로드되었습니다", + "downloadFailed": "파일 다운로드 실패", + "permissionDenied": "권한이 거부되었습니다", + "checkDockerLogs": "자세한 오류 정보는 Docker 로그를 확인하세요", + "internalServerError": "내부 서버 오류가 발생했습니다", + "serverError": "서버 오류", + "error": "오류", + "requestFailed": "요청 실패, 상태 코드", + "unknownFileError": "알 수 없음", + "cannotReadFile": "파일을 읽을 수 없습니다", + "noSshSessionId": "사용 가능한 SSH 세션 ID가 없습니다", + "noFilePath": "사용 가능한 파일 경로가 없습니다", + "noCurrentHost": "사용 가능한 현재 호스트가 없습니다", + "fileSavedSuccessfully": "파일이 성공적으로 저장되었습니다", + "saveTimeout": "저장 작업 시간 초과. 파일이 성공적으로 저장되었을 수 있지만 작업 완료에 시간이 너무 오래 걸렸습니다. 확인을 위해 Docker 로그를 확인하세요.", + "failedToSaveFile": "파일 저장 실패", + "folder": "폴더", + "file": "파일", + "deletedSuccessfully": "성공적으로 삭제되었습니다", + "failedToDeleteItem": "항목 삭제 실패", + "connectToServer": "서버에 연결", + "selectServerToEdit": "사이드바에서 서버를 선택하여 파일 편집 시작", + "fileOperations": "파일 작업", + "confirmDeleteMessage": "{{name}}을(를) 삭제하시겠습니까?", + "confirmDeleteSingleItem": "\"{{name}}\"을(를) 영구적으로 삭제하시겠습니까?", + "confirmDeleteMultipleItems": "{{count}}개 항목을 영구적으로 삭제하시겠습니까?", + "confirmDeleteMultipleItemsWithFolders": "{{count}}개 항목을 영구적으로 삭제하시겠습니까? 폴더와 그 내용이 포함됩니다.", + "confirmDeleteFolder": "폴더 \"{{name}}\"과(와) 모든 내용을 영구적으로 삭제하시겠습니까?", + "deleteDirectoryWarning": "폴더와 모든 내용이 삭제됩니다.", + "actionCannotBeUndone": "이 작업은 취소할 수 없습니다.", + "permanentDeleteWarning": "이 작업은 취소할 수 없습니다. 항목이 서버에서 영구적으로 삭제됩니다.", + "recent": "최근", + "pinned": "고정됨", + "folderShortcuts": "폴더 바로가기", + "noRecentFiles": "최근 파일이 없습니다.", + "noPinnedFiles": "고정된 파일이 없습니다.", + "enterFolderPath": "폴더 경로 입력", + "noShortcuts": "바로가기가 없습니다.", + "searchFilesAndFolders": "파일 및 폴더 검색...", + "noFilesOrFoldersFound": "파일 또는 폴더를 찾을 수 없습니다.", + "failedToConnectSSH": "SSH 연결 실패", + "failedToReconnectSSH": "SSH 세션 재연결 실패", + "failedToListFiles": "파일 목록 가져오기 실패", + "fetchHomeDataTimeout": "홈 데이터 가져오기 시간 초과", + "sshStatusCheckTimeout": "SSH 상태 확인 시간 초과", + "sshReconnectionTimeout": "SSH 재연결 시간 초과", + "saveOperationTimeout": "저장 작업 시간 초과", + "cannotSaveFile": "파일을 저장할 수 없습니다", + "dragSystemFilesToUpload": "시스템 파일을 여기로 드래그하여 업로드", + "dragFilesToWindowToDownload": "파일을 창 밖으로 드래그하여 다운로드", + "openTerminalHere": "여기서 터미널 열기", + "run": "실행", + "saveToSystem": "다른 이름으로 저장...", + "selectLocationToSave": "저장 위치 선택", + "openTerminalInFolder": "이 폴더에서 터미널 열기", + "openTerminalInFileLocation": "파일 위치에서 터미널 열기", + "terminalWithPath": "터미널 - {{host}}:{{path}}", + "runningFile": "실행 중 - {{file}}", + "onlyRunExecutableFiles": "실행 가능한 파일만 실행할 수 있습니다", + "noHostSelected": "선택된 호스트 없음", + "starred": "즐겨찾기", + "shortcuts": "바로가기", + "directories": "디렉터리", + "removedFromRecentFiles": "최근 파일에서 \"{{name}}\"이(가) 제거되었습니다", + "removeFailed": "제거 실패", + "unpinnedSuccessfully": "\"{{name}}\"의 고정이 성공적으로 해제되었습니다", + "unpinFailed": "고정 해제 실패", + "removedShortcut": "바로가기 \"{{name}}\"이(가) 제거되었습니다", + "removeShortcutFailed": "바로가기 제거 실패", + "clearedAllRecentFiles": "모든 최근 파일이 지워졌습니다", + "clearFailed": "지우기 실패", + "removeFromRecentFiles": "최근 파일에서 제거", + "clearAllRecentFiles": "모든 최근 파일 지우기", + "unpinFile": "파일 고정 해제", + "removeShortcut": "바로가기 제거", + "saveFilesToSystem": "{{count}}개 파일을 다른 이름으로 저장...", + "saveToSystem": "다른 이름으로 저장...", + "pinFile": "파일 고정", + "addToShortcuts": "바로가기에 추가", + "selectLocationToSave": "저장 위치 선택", + "downloadToDefaultLocation": "기본 위치로 다운로드", + "pasteFailed": "붙여넣기 실패", + "noUndoableActions": "실행 취소할 작업이 없습니다", + "undoCopySuccess": "복사 작업 실행 취소: {{count}}개 복사된 파일 삭제됨", + "undoCopyFailedDelete": "실행 취소 실패: 복사된 파일을 삭제할 수 없습니다", + "undoCopyFailedNoInfo": "실행 취소 실패: 복사된 파일 정보를 찾을 수 없습니다", + "undoMoveSuccess": "이동 작업 실행 취소: {{count}}개 파일을 원래 위치로 이동함", + "undoMoveFailedMove": "실행 취소 실패: 파일을 되돌릴 수 없습니다", + "undoMoveFailedNoInfo": "실행 취소 실패: 이동된 파일 정보를 찾을 수 없습니다", + "undoDeleteNotSupported": "삭제 작업은 실행 취소할 수 없습니다: 파일이 서버에서 영구적으로 삭제되었습니다", + "undoTypeNotSupported": "지원되지 않는 실행 취소 작업 유형", + "undoOperationFailed": "실행 취소 작업 실패", + "unknownError": "알 수 없는 오류", + "enterPath": "경로 입력...", + "editPath": "경로 편집", + "confirm": "확인", + "cancel": "취소", + "folderName": "폴더 이름", + "find": "찾기...", + "replaceWith": "바꿀 내용...", + "replace": "바꾸기", + "replaceAll": "모두 바꾸기", + "downloadInstead": "대신 다운로드", + "keyboardShortcuts": "키보드 단축키", + "searchAndReplace": "검색 및 바꾸기", + "editing": "편집", + "navigation": "탐색", + "code": "코드", + "search": "검색", + "findNext": "다음 찾기", + "findPrevious": "이전 찾기", + "save": "저장", + "selectAll": "모두 선택", + "undo": "실행 취소", + "redo": "다시 실행", + "goToLine": "줄로 이동", + "moveLineUp": "줄 위로 이동", + "moveLineDown": "줄 아래로 이동", + "toggleComment": "주석 토글", + "indent": "들여쓰기", + "outdent": "내어쓰기", + "autoComplete": "자동 완성", + "imageLoadError": "이미지 로드 실패", + "zoomIn": "확대", + "zoomOut": "축소", + "rotate": "회전", + "originalSize": "원본 크기", + "startTyping": "입력 시작...", + "unknownSize": "알 수 없는 크기", + "fileIsEmpty": "파일이 비어 있습니다", + "modified": "수정됨", + "largeFileWarning": "대용량 파일 경고", + "largeFileWarningDesc": "이 파일의 크기는 {{size}}이며, 텍스트로 열 때 성능 문제가 발생할 수 있습니다.", + "fileNotFoundAndRemoved": "파일 \"{{name}}\"을(를) 찾을 수 없어 최근/고정 파일에서 제거되었습니다", + "failedToLoadFile": "파일 로드 실패: {{error}}", + "serverErrorOccurred": "서버 오류가 발생했습니다. 나중에 다시 시도하세요.", + "fileSavedSuccessfully": "파일이 성공적으로 저장되었습니다", + "autoSaveFailed": "자동 저장 실패", + "fileAutoSaved": "파일 자동 저장됨", + "fileDownloadedSuccessfully": "파일이 성공적으로 다운로드되었습니다", + "moveFileFailed": "{{name}} 이동 실패", + "moveOperationFailed": "이동 작업 실패", + "canOnlyCompareFiles": "두 개의 파일만 비교할 수 있습니다", + "comparingFiles": "파일 비교 중: {{file1}}과(와) {{file2}}", + "dragFailed": "드래그 작업 실패", + "filePinnedSuccessfully": "파일 \"{{name}}\"이(가) 성공적으로 고정되었습니다", + "pinFileFailed": "파일 고정 실패", + "fileUnpinnedSuccessfully": "파일 \"{{name}}\"의 고정이 성공적으로 해제되었습니다", + "unpinFileFailed": "파일 고정 해제 실패", + "shortcutAddedSuccessfully": "폴더 바로가기 \"{{name}}\"이(가) 성공적으로 추가되었습니다", + "addShortcutFailed": "바로가기 추가 실패", + "operationCompletedSuccessfully": "{{count}}개 항목 {{operation}} 성공", + "operationCompleted": "{{count}}개 항목 {{operation}}", + "downloadFileSuccess": "파일 {{name}}이(가) 성공적으로 다운로드되었습니다", + "downloadFileFailed": "다운로드 실패", + "moveTo": "{{name}}(으)로 이동", + "diffCompareWith": "{{name}}과(와) 차이 비교", + "dragOutsideToDownload": "창 밖으로 드래그하여 다운로드 ({{count}}개 파일)", + "newFolderDefault": "NewFolder", + "newFileDefault": "NewFile.txt", + "successfullyMovedItems": "{{count}}개 항목을 {{target}}(으)로 성공적으로 이동했습니다", + "move": "이동", + "searchInFile": "파일에서 검색 (Ctrl+F)", + "showKeyboardShortcuts": "키보드 단축키 표시", + "startWritingMarkdown": "마크다운 콘텐츠 작성 시작...", + "loadingFileComparison": "파일 비교 로드 중...", + "reload": "다시 로드", + "compare": "비교", + "sideBySide": "나란히", + "inline": "인라인", + "fileComparison": "파일 비교: {{file1}} vs {{file2}}", + "fileTooLarge": "파일이 너무 큽니다: {{error}}", + "sshConnectionFailed": "SSH 연결 실패. {{name}} ({{ip}}:{{port}})에 대한 연결을 확인하세요", + "loadFileFailed": "파일 로드 실패: {{error}}", + "connectedSuccessfully": "성공적으로 연결되었습니다", + "totpVerificationFailed": "TOTP 확인 실패", + "changePermissions": "권한 변경", + "changePermissionsDesc": "파일 권한 수정", + "currentPermissions": "현재 권한", + "newPermissions": "새 권한", + "owner": "소유자", + "group": "그룹", + "others": "기타", + "read": "읽기", + "write": "쓰기", + "execute": "실행", + "permissionsChangedSuccessfully": "권한이 성공적으로 변경되었습니다", + "failedToChangePermissions": "권한 변경 실패" + }, + "tunnels": { + "title": "SSH 터널", + "noSshTunnels": "SSH 터널 없음", + "createFirstTunnelMessage": "아직 SSH 터널을 생성하지 않았습니다. 시작하려면 호스트 관리자에서 터널 연결을 구성하세요.", + "connected": "연결됨", + "disconnected": "연결 해제됨", + "connecting": "연결 중...", + "disconnecting": "연결 해제 중...", + "unknownTunnelStatus": "알 수 없음", + "unknown": "알 수 없음", + "error": "오류", + "failed": "실패", + "retrying": "재시도 중", + "waiting": "대기 중", + "waitingForRetry": "재시도 대기 중", + "retryingConnection": "연결 재시도 중", + "canceling": "취소 중...", + "connect": "연결", + "disconnect": "연결 해제", + "cancel": "취소", + "port": "포트", + "attempt": "시도 {{current}}/{{max}}", + "nextRetryIn": "{{seconds}}초 후 재시도", + "checkDockerLogs": "오류 원인은 Docker 로그를 확인하거나", + "noTunnelConnections": "구성된 터널 연결 없음", + "tunnelConnections": "터널 연결", + "addTunnel": "터널 추가", + "editTunnel": "터널 편집", + "deleteTunnel": "터널 삭제", + "tunnelName": "터널 이름", + "localPort": "로컬 포트", + "remoteHost": "원격 호스트", + "remotePort": "원격 포트", + "autoStart": "자동 시작", + "status": "상태", + "active": "활성", + "inactive": "비활성", + "start": "시작", + "stop": "중지", + "restart": "재시작", + "connectionType": "연결 유형", + "local": "로컬", + "remote": "원격", + "dynamic": "동적", + "noSshTunnels": "SSH 터널 없음", + "createFirstTunnelMessage": "첫 번째 SSH 터널을 생성하여 시작하세요. SSH 관리자를 사용하여 터널 연결이 있는 호스트를 추가하세요.", + "unknownConnectionStatus": "알 수 없음", + "connected": "연결됨", + "connecting": "연결 중...", + "disconnecting": "연결 해제 중...", + "disconnected": "연결 해제됨", + "portMapping": "포트 {{sourcePort}} → {{endpointHost}}:{{endpointPort}}", + "disconnect": "연결 해제", + "connect": "연결", + "canceling": "취소 중...", + "endpointHostNotFound": "엔드포인트 호스트를 찾을 수 없습니다", + "discord": "Discord", + "githubIssue": "GitHub 이슈", + "forHelp": "에서 도움을 받으세요" + }, + "serverStats": { + "title": "서버 통계", + "cpu": "CPU", + "memory": "메모리", + "disk": "디스크", + "network": "네트워크", + "uptime": "가동 시간", + "loadAverage": "평균 부하", + "processes": "프로세스", + "connections": "연결", + "usage": "사용량", + "available": "사용 가능", + "total": "전체", + "free": "여유", + "used": "사용됨", + "percentage": "백분율", + "refreshStatusAndMetrics": "상태 및 메트릭 새로 고침", + "refreshStatus": "상태 새로 고침", + "fileManagerAlreadyOpen": "이 호스트에 대한 파일 관리자가 이미 열려 있습니다", + "openFileManager": "파일 관리자 열기", + "cpuCores_one": "{{count}}개 CPU", + "cpuCores_other": "{{count}}개 CPU", + "naCpus": "N/A CPU", + "loadAverage": "평균: {{avg1}}, {{avg5}}, {{avg15}}", + "loadAverageNA": "평균: N/A", + "cpuUsage": "CPU 사용량", + "memoryUsage": "메모리 사용량", + "diskUsage": "디스크 사용량", + "rootStorageSpace": "루트 저장 공간", + "of": "/", + "feedbackMessage": "서버 관리를 위해 다음에 추가되어야 할 아이디어가 있으신가요? 공유하세요", + "failedToFetchHostConfig": "호스트 구성 가져오기 실패", + "failedToFetchStatus": "서버 상태 가져오기 실패", + "failedToFetchMetrics": "서버 메트릭 가져오기 실패", + "failedToFetchHomeData": "홈 데이터 가져오기 실패", + "loadingMetrics": "메트릭 로드 중...", + "refreshing": "새로 고침 중...", + "serverOffline": "서버 오프라인", + "cannotFetchMetrics": "오프라인 서버에서 메트릭을 가져올 수 없습니다", + "totpRequired": "TOTP 인증 필요", + "totpUnavailable": "TOTP가 활성화된 서버에서는 서버 통계를 사용할 수 없습니다", + "load": "부하", + "available": "사용 가능", + "editLayout": "레이아웃 편집", + "cancelEdit": "취소", + "addWidget": "위젯 추가", + "saveLayout": "레이아웃 저장", + "unsavedChanges": "저장되지 않은 변경사항", + "layoutSaved": "레이아웃이 성공적으로 저장되었습니다", + "failedToSaveLayout": "레이아웃 저장 실패", + "systemInfo": "시스템 정보", + "hostname": "호스트 이름", + "operatingSystem": "운영 체제", + "kernel": "커널", + "totalUptime": "총 가동 시간", + "seconds": "초", + "networkInterfaces": "네트워크 인터페이스", + "noInterfacesFound": "네트워크 인터페이스를 찾을 수 없습니다", + "totalProcesses": "총 프로세스", + "running": "실행 중", + "noProcessesFound": "프로세스를 찾을 수 없습니다", + "loginStats": "SSH 로그인 통계", + "totalLogins": "총 로그인", + "uniqueIPs": "고유 IP", + "recentSuccessfulLogins": "최근 성공한 로그인", + "recentFailedAttempts": "최근 실패한 시도", + "noRecentLoginData": "최근 로그인 데이터 없음", + "from": "출처", + "quickActions": "빠른 작업", + "executeQuickAction": "{{name}} 실행", + "executingQuickAction": "{{name}} 실행 중...", + "quickActionSuccess": "{{name}}이(가) 성공적으로 완료되었습니다", + "quickActionFailed": "{{name}} 실패", + "quickActionError": "{{name}} 실행 실패" + }, + "auth": { + "tagline": "SSH 서버 관리자", + "description": "안전하고 강력하며 직관적인 SSH 연결 관리", + "welcomeBack": "TERMIX에 다시 오신 것을 환영합니다", + "createAccount": "TERMIX 계정 만들기", + "continueExternal": "외부 공급자로 계속", + "loginTitle": "Termix 로그인", + "registerTitle": "계정 만들기", + "loginButton": "로그인", + "registerButton": "등록", + "forgotPassword": "비밀번호를 잊으셨나요?", + "rememberMe": "로그인 상태 유지", + "noAccount": "계정이 없으신가요?", + "hasAccount": "이미 계정이 있으신가요?", + "loginSuccess": "로그인 성공", + "loginFailed": "로그인 실패", + "registerSuccess": "등록 성공", + "registerFailed": "등록 실패", + "logoutSuccess": "성공적으로 로그아웃되었습니다", + "invalidCredentials": "잘못된 사용자 이름 또는 비밀번호", + "accountCreated": "계정이 성공적으로 생성되었습니다", + "passwordReset": "비밀번호 재설정 링크가 전송되었습니다", + "twoFactorAuth": "2단계 인증", + "enterCode": "인증 코드 입력", + "backupCode": "또는 백업 코드 사용", + "verifyCode": "코드 확인", + "redirectingToApp": "앱으로 리디렉션 중...", + "enableTwoFactor": "2단계 인증 활성화", + "disableTwoFactor": "2단계 인증 비활성화", + "scanQRCode": "인증 앱으로 이 QR 코드를 스캔하세요", + "backupCodes": "백업 코드", + "saveBackupCodes": "이 백업 코드를 안전한 곳에 저장하세요", + "twoFactorEnabledSuccess": "2단계 인증이 성공적으로 활성화되었습니다!", + "twoFactorDisabled": "2단계 인증이 비활성화되었습니다", + "newBackupCodesGenerated": "새 백업 코드가 생성되었습니다", + "backupCodesDownloaded": "백업 코드가 다운로드되었습니다", + "pleaseEnterSixDigitCode": "6자리 코드를 입력하세요", + "invalidVerificationCode": "잘못된 인증 코드", + "failedToDisableTotp": "TOTP 비활성화 실패", + "failedToGenerateBackupCodes": "백업 코드 생성 실패", + "enterPassword": "비밀번호 입력", + "lockedOidcAuth": "잠김 (OIDC 인증)", + "twoFactorTitle": "2단계 인증", + "twoFactorProtected": "귀하의 계정은 2단계 인증으로 보호됩니다", + "twoFactorActive": "2단계 인증이 현재 계정에서 활성화되어 있습니다", + "disable2FA": "2FA 비활성화", + "disableTwoFactorWarning": "2단계 인증을 비활성화하면 계정 보안이 약화됩니다", + "passwordOrTotpCode": "비밀번호 또는 TOTP 코드", + "or": "또는", + "generateNewBackupCodesText": "기존 백업 코드를 분실한 경우 새 백업 코드를 생성하세요", + "generateNewBackupCodes": "새 백업 코드 생성", + "yourBackupCodes": "백업 코드", + "download": "다운로드", + "setupTwoFactorTitle": "2단계 인증 설정", + "sshAuthenticationRequired": "SSH 인증 필요", + "sshNoKeyboardInteractive": "키보드 대화형 인증 사용 불가", + "sshAuthenticationFailed": "인증 실패", + "sshAuthenticationTimeout": "인증 시간 초과", + "sshNoKeyboardInteractiveDescription": "서버가 키보드 대화형 인증을 지원하지 않습니다. 비밀번호 또는 SSH 키를 제공하세요.", + "sshAuthFailedDescription": "제공된 자격 증명이 올바르지 않습니다. 유효한 자격 증명으로 다시 시도하세요.", + "sshTimeoutDescription": "인증 시도가 시간 초과되었습니다. 다시 시도하세요.", + "sshProvideCredentialsDescription": "이 서버에 연결하려면 SSH 자격 증명을 제공하세요.", + "sshPasswordDescription": "이 SSH 연결의 비밀번호를 입력하세요.", + "sshKeyPasswordDescription": "SSH 키가 암호화된 경우 여기에 암호를 입력하세요.", + "step1ScanQR": "1단계: 인증 앱으로 QR 코드 스캔", + "manualEntryCode": "수동 입력 코드", + "cannotScanQRText": "QR 코드를 스캔할 수 없는 경우 인증 앱에 이 코드를 수동으로 입력하세요", + "nextVerifyCode": "다음: 코드 확인", + "verifyAuthenticator": "인증 앱 확인", + "step2EnterCode": "2단계: 인증 앱에서 6자리 코드 입력", + "verificationCode": "인증 코드", + "back": "뒤로", + "verifyAndEnable": "확인 및 활성화", + "saveBackupCodesTitle": "백업 코드 저장", + "step3StoreCodesSecurely": "3단계: 이 코드를 안전한 곳에 저장", + "importantBackupCodesText": "이 백업 코드를 안전한 위치에 저장하세요. 인증 장치를 분실한 경우 이 코드를 사용하여 계정에 액세스할 수 있습니다.", + "completeSetup": "설정 완료", + "notEnabledText": "2단계 인증은 로그인 시 인증 앱의 코드를 요구하여 추가 보안 계층을 제공합니다.", + "enableTwoFactorButton": "2단계 인증 활성화", + "addExtraSecurityLayer": "계정에 추가 보안 계층 추가", + "firstUser": "첫 번째 사용자", + "firstUserMessage": "귀하는 첫 번째 사용자이며 관리자로 지정됩니다. 사이드바 사용자 드롭다운에서 관리자 설정을 볼 수 있습니다. 이것이 실수라고 생각되면 Docker 로그를 확인하거나 GitHub 이슈를 생성하세요.", + "external": "외부", + "loginWithExternal": "외부 공급자로 로그인", + "loginWithExternalDesc": "구성된 외부 ID 공급자를 사용하여 로그인", + "externalNotSupportedInElectron": "외부 인증은 아직 Electron 앱에서 지원되지 않습니다. OIDC 로그인을 위해 웹 버전을 사용하세요.", + "resetPasswordButton": "비밀번호 재설정", + "sendResetCode": "재설정 코드 전송", + "resetCodeDesc": "사용자 이름을 입력하여 비밀번호 재설정 코드를 받으세요. 코드는 Docker 컨테이너 로그에 기록됩니다.", + "resetCode": "재설정 코드", + "verifyCodeButton": "코드 확인", + "enterResetCode": "사용자에 대한 Docker 컨테이너 로그에서 6자리 코드 입력:", + "goToLogin": "로그인으로 이동", + "newPassword": "새 비밀번호", + "confirmNewPassword": "비밀번호 확인", + "enterNewPassword": "사용자에 대한 새 비밀번호 입력:", + "signUp": "가입", + "mobileApp": "모바일 앱", + "loggingInToMobileApp": "모바일 앱에 로그인 중", + "desktopApp": "데스크톱 앱", + "loggingInToDesktopApp": "데스크톱 앱에 로그인 중", + "loggingInToDesktopAppViaWeb": "웹 인터페이스를 통해 데스크톱 앱에 로그인 중", + "loadingServer": "서버 로드 중...", + "authenticating": "인증 중...", + "dataLossWarning": "이 방법으로 비밀번호를 재설정하면 저장된 모든 SSH 호스트, 자격 증명 및 기타 암호화된 데이터가 삭제됩니다. 이 작업은 취소할 수 없습니다. 비밀번호를 잊어버렸고 로그인되어 있지 않은 경우에만 사용하세요.", + "authenticationDisabled": "인증 비활성화됨", + "authenticationDisabledDesc": "현재 모든 인증 방법이 비활성화되어 있습니다. 관리자에게 문의하세요." + }, + "errors": { + "notFound": "페이지를 찾을 수 없습니다", + "unauthorized": "권한 없는 액세스", + "forbidden": "액세스 금지", + "serverError": "서버 오류", + "networkError": "네트워크 오류", + "databaseConnection": "데이터베이스에 연결할 수 없습니다", + "unknownError": "알 수 없는 오류", + "loginFailed": "로그인 실패", + "failedPasswordReset": "비밀번호 재설정 시작 실패", + "failedVerifyCode": "재설정 코드 확인 실패", + "failedCompleteReset": "비밀번호 재설정 완료 실패", + "invalidTotpCode": "잘못된 TOTP 코드", + "failedOidcLogin": "OIDC 로그인 시작 실패", + "failedUserInfo": "OIDC 로그인 후 사용자 정보 가져오기 실패", + "oidcAuthFailed": "OIDC 인증 실패", + "noTokenReceived": "로그인에서 토큰을 받지 못했습니다", + "invalidAuthUrl": "백엔드에서 잘못된 인증 URL을 받았습니다", + "invalidInput": "잘못된 입력", + "requiredField": "이 필드는 필수입니다", + "minLength": "최소 길이는 {{min}}입니다", + "maxLength": "최대 길이는 {{max}}입니다", + "invalidEmail": "잘못된 이메일 주소", + "passwordMismatch": "비밀번호가 일치하지 않습니다", + "passwordLoginDisabled": "사용자 이름/비밀번호 로그인이 현재 비활성화되어 있습니다", + "weakPassword": "비밀번호가 너무 약합니다", + "usernameExists": "사용자 이름이 이미 존재합니다", + "emailExists": "이메일이 이미 존재합니다", + "loadFailed": "데이터 로드 실패", + "saveError": "저장 실패", + "sessionExpired": "세션이 만료되었습니다 - 다시 로그인하세요" + }, + "messages": { + "saveSuccess": "성공적으로 저장되었습니다", + "saveError": "저장 실패", + "deleteSuccess": "성공적으로 삭제되었습니다", + "deleteError": "삭제 실패", + "updateSuccess": "성공적으로 업데이트되었습니다", + "updateError": "업데이트 실패", + "copySuccess": "클립보드에 복사되었습니다", + "copyError": "복사 실패", + "copiedToClipboard": "{{item}}이(가) 클립보드에 복사되었습니다", + "connectionEstablished": "연결이 설정되었습니다", + "connectionClosed": "연결이 닫혔습니다", + "reconnecting": "재연결 중...", + "processing": "처리 중...", + "pleaseWait": "잠시 기다려주세요...", + "registrationDisabled": "새 계정 등록이 현재 관리자에 의해 비활성화되어 있습니다. 로그인하거나 관리자에게 문의하세요.", + "databaseConnected": "데이터베이스가 성공적으로 연결되었습니다", + "databaseConnectionFailed": "데이터베이스 서버에 연결하지 못했습니다", + "checkServerConnection": "서버 연결을 확인하고 다시 시도하세요", + "resetCodeSent": "재설정 코드가 Docker 로그로 전송되었습니다", + "codeVerified": "코드가 성공적으로 확인되었습니다", + "passwordResetSuccess": "비밀번호가 성공적으로 재설정되었습니다", + "loginSuccess": "로그인 성공", + "registrationSuccess": "등록 성공" + }, + "profile": { + "title": "사용자 프로필", + "description": "계정 설정 및 보안 관리", + "security": "보안", + "changePassword": "비밀번호 변경", + "twoFactorAuth": "2단계 인증", + "accountInfo": "계정 정보", + "role": "역할", + "admin": "관리자", + "user": "사용자", + "authMethod": "인증 방법", + "local": "로컬", + "external": "외부 (OIDC)", + "externalAndLocal": "이중 인증", + "selectPreferredLanguage": "인터페이스에 대한 선호 언어 선택", + "fileColorCoding": "파일 색상 코딩", + "fileColorCodingDesc": "유형별로 파일을 색상으로 구분: 폴더(빨강), 파일(파랑), 심볼릭 링크(초록)", + "commandAutocomplete": "명령 자동 완성", + "commandAutocompleteDesc": "명령 기록을 기반으로 터미널 명령에 대한 Tab 키 자동 완성 제안 활성화", + "currentPassword": "현재 비밀번호", + "passwordChangedSuccess": "비밀번호가 성공적으로 변경되었습니다! 다시 로그인하세요.", + "failedToChangePassword": "비밀번호 변경 실패. 현재 비밀번호를 확인하고 다시 시도하세요." + }, + "user": { + "failedToLoadVersionInfo": "버전 정보 로드 실패" + }, + "placeholders": { + "enterCode": "000000", + "ipAddress": "127.0.0.1", + "port": "22", + "maxRetries": "3", + "retryInterval": "10", + "language": "언어", + "username": "사용자 이름", + "hostname": "호스트 이름", + "folder": "폴더", + "password": "비밀번호", + "keyPassword": "키 비밀번호", + "pastePrivateKey": "개인 키를 여기에 붙여넣으세요...", + "pastePublicKey": "공개 키를 여기에 붙여넣으세요...", + "credentialName": "내 SSH 서버", + "description": "SSH 자격 증명 설명", + "searchCredentials": "이름, 사용자 이름 또는 태그로 자격 증명 검색...", + "sshConfig": "엔드포인트 ssh 구성", + "homePath": "/home", + "clientId": "your-client-id", + "clientSecret": "your-client-secret", + "authUrl": "https://your-provider.com/application/o/authorize/", + "redirectUrl": "https://your-provider.com/application/o/termix/", + "tokenUrl": "https://your-provider.com/application/o/token/", + "userIdField": "sub", + "usernameField": "name", + "scopes": "openid email profile", + "userinfoUrl": "https://your-provider.com/application/o/userinfo/", + "enterUsername": "관리자로 지정할 사용자 이름 입력", + "searchHosts": "이름, 사용자 이름, IP, 폴더, 태그로 호스트 검색...", + "enterPassword": "비밀번호 입력", + "totpCode": "6자리 TOTP 코드", + "searchHostsAny": "모든 정보로 호스트 검색...", + "confirmPassword": "확인을 위해 비밀번호 입력", + "typeHere": "여기에 입력", + "fileName": "파일 이름 입력 (예: example.txt)", + "folderName": "폴더 이름 입력", + "fullPath": "항목의 전체 경로 입력", + "currentPath": "항목의 현재 경로 입력", + "newName": "새 이름 입력" + }, + "leftSidebar": { + "failedToLoadHosts": "호스트 로드 실패", + "noFolder": "폴더 없음", + "passwordRequired": "비밀번호가 필요합니다", + "failedToDeleteAccount": "계정 삭제 실패", + "failedToMakeUserAdmin": "사용자를 관리자로 지정하는 데 실패했습니다", + "userIsNowAdmin": "사용자 {{username}}이(가) 이제 관리자입니다", + "removeAdminConfirm": "{{username}}의 관리자 상태를 제거하시겠습니까?", + "deleteUserConfirm": "사용자 {{username}}을(를) 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.", + "deleteAccount": "계정 삭제", + "closeDeleteAccount": "계정 삭제 닫기", + "deleteAccountWarning": "이 작업은 취소할 수 없습니다. 계정과 모든 관련 데이터가 영구적으로 삭제됩니다.", + "deleteAccountWarningDetails": "계정을 삭제하면 SSH 호스트, 구성 및 설정을 포함한 모든 데이터가 제거됩니다. 이 작업은 되돌릴 수 없습니다.", + "deleteAccountWarningShort": "이 작업은 되돌릴 수 없으며 계정이 영구적으로 삭제됩니다.", + "cannotDeleteAccount": "계정을 삭제할 수 없습니다", + "lastAdminWarning": "귀하는 마지막 관리자 사용자입니다. 시스템에 관리자가 없게 되므로 계정을 삭제할 수 없습니다. 먼저 다른 사용자를 관리자로 지정하거나 시스템 지원에 문의하세요.", + "confirmPassword": "비밀번호 확인", + "deleting": "삭제 중...", + "cancel": "취소" + }, + "interface": { + "sidebar": "사이드바", + "toggleSidebar": "사이드바 토글", + "close": "닫기", + "online": "온라인", + "offline": "오프라인", + "maintenance": "유지보수", + "degraded": "저하됨", + "noTunnelConnections": "구성된 터널 연결 없음", + "discord": "Discord", + "connectToSshForOperations": "파일 작업을 사용하려면 SSH에 연결하세요", + "uploadFile": "파일 업로드", + "newFile": "새 파일", + "newFolder": "새 폴더", + "rename": "이름 변경", + "deleteItem": "항목 삭제", + "createNewFile": "새 파일 생성", + "createNewFolder": "새 폴더 생성", + "deleteItem": "항목 삭제", + "renameItem": "항목 이름 변경", + "clickToSelectFile": "클릭하여 파일 선택", + "noSshHosts": "SSH 호스트 없음", + "sshHosts": "SSH 호스트", + "importSshHosts": "JSON에서 SSH 호스트 가져오기", + "clientId": "클라이언트 ID", + "clientSecret": "클라이언트 시크릿", + "error": "오류", + "warning": "경고", + "deleteAccount": "계정 삭제", + "closeDeleteAccount": "계정 삭제 닫기", + "cannotDeleteAccount": "계정을 삭제할 수 없습니다", + "confirmPassword": "비밀번호 확인", + "deleting": "삭제 중...", + "externalAuth": "외부 인증 (OIDC)", + "configureExternalProvider": "외부 ID 공급자 구성", + "waitingForRetry": "재시도 대기 중", + "retryingConnection": "연결 재시도 중", + "resetSplitSizes": "분할 크기 재설정", + "sshManagerAlreadyOpen": "SSH 관리자가 이미 열려 있습니다", + "disabledDuringSplitScreen": "분할 화면 중 비활성화됨", + "unknown": "알 수 없음", + "connected": "연결됨", + "disconnected": "연결 해제됨", + "maxRetriesExhausted": "최대 재시도 횟수 소진", + "endpointHostNotFound": "엔드포인트 호스트를 찾을 수 없습니다", + "administrator": "관리자", + "user": "사용자", + "external": "외부", + "local": "로컬", + "saving": "저장 중...", + "saveConfiguration": "구성 저장", + "loading": "로드 중...", + "refresh": "새로 고침", + "adding": "추가 중...", + "makeAdmin": "관리자로 지정", + "verifying": "확인 중...", + "verifyAndEnable": "확인 및 활성화", + "secretKey": "비밀 키", + "totpQrCode": "TOTP QR 코드", + "passwordRequired": "비밀번호 인증을 사용할 때 비밀번호가 필요합니다", + "sshKeyRequired": "키 인증을 사용할 때 SSH 개인 키가 필요합니다", + "keyTypeRequired": "키 인증을 사용할 때 키 유형이 필요합니다", + "validSshConfigRequired": "목록에서 유효한 SSH 구성을 선택해야 합니다", + "updateHost": "호스트 업데이트", + "addHost": "호스트 추가", + "editHost": "호스트 편집", + "pinConnection": "연결 고정", + "authentication": "인증", + "password": "비밀번호", + "key": "키", + "sshPrivateKey": "SSH 개인 키", + "keyPassword": "키 비밀번호", + "keyType": "키 유형", + "enableTerminal": "터미널 활성화", + "enableTunnel": "터널 활성화", + "enableFileManager": "파일 관리자 활성화", + "defaultPath": "기본 경로", + "tunnelConnections": "터널 연결", + "maxRetries": "최대 재시도 횟수", + "upload": "업로드", + "updateKey": "키 업데이트", + "productionFolder": "프로덕션", + "databaseServer": "데이터베이스 서버", + "developmentServer": "개발 서버", + "developmentFolder": "개발", + "webServerProduction": "웹 서버 - 프로덕션", + "unknownError": "알 수 없는 오류", + "failedToInitiatePasswordReset": "비밀번호 재설정 시작 실패", + "failedToVerifyResetCode": "재설정 코드 확인 실패", + "failedToCompletePasswordReset": "비밀번호 재설정 완료 실패", + "invalidTotpCode": "잘못된 TOTP 코드", + "failedToStartOidcLogin": "OIDC 로그인 시작 실패", + "failedToGetUserInfoAfterOidc": "OIDC 로그인 후 사용자 정보 가져오기 실패", + "loginWithExternalProvider": "외부 공급자로 로그인", + "loginWithExternal": "외부 공급자로 로그인", + "sendResetCode": "재설정 코드 전송", + "verifyCode": "코드 확인", + "resetPassword": "비밀번호 재설정", + "login": "로그인", + "signUp": "가입", + "failedToUpdateOidcConfig": "OIDC 구성 업데이트 실패", + "failedToMakeUserAdmin": "사용자를 관리자로 지정하는 데 실패했습니다", + "failedToStartTotpSetup": "TOTP 설정 시작 실패", + "invalidVerificationCode": "잘못된 인증 코드", + "failedToDisableTotp": "TOTP 비활성화 실패", + "failedToGenerateBackupCodes": "백업 코드 생성 실패" + }, + "mobile": { + "selectHostToStart": "터미널 세션을 시작하려면 호스트를 선택하세요", + "limitedSupportMessage": "웹사이트 모바일 지원은 아직 진행 중입니다. 더 나은 경험을 위해 모바일 앱을 사용하세요.", + "mobileAppInProgress": "모바일 앱 개발 중", + "mobileAppInProgressDesc": "모바일 장치에서 더 나은 경험을 제공하기 위해 전용 모바일 앱을 개발 중입니다.", + "viewMobileAppDocs": "모바일 앱 설치", + "mobileAppDocumentation": "모바일 앱 문서" + }, + "dashboard": { + "title": "대시보드", + "github": "GitHub", + "support": "지원", + "discord": "Discord", + "donate": "기부", + "serverOverview": "서버 개요", + "version": "버전", + "upToDate": "최신 상태", + "updateAvailable": "업데이트 사용 가능", + "uptime": "가동 시간", + "database": "데이터베이스", + "healthy": "정상", + "error": "오류", + "totalServers": "총 서버", + "totalTunnels": "총 터널", + "totalCredentials": "총 자격 증명", + "recentActivity": "최근 활동", + "reset": "재설정", + "loadingRecentActivity": "최근 활동 로드 중...", + "noRecentActivity": "최근 활동 없음", + "quickActions": "빠른 작업", + "addHost": "호스트 추가", + "addCredential": "자격 증명 추가", + "adminSettings": "관리자 설정", + "userProfile": "사용자 프로필", + "serverStats": "서버 통계", + "loadingServerStats": "서버 통계 로드 중...", + "noServerData": "사용 가능한 서버 데이터 없음", + "cpu": "CPU", + "ram": "RAM", + "notAvailable": "N/A" + }, + "commandPalette": { + "searchPlaceholder": "호스트 또는 빠른 작업 검색...", + "recentActivity": "최근 활동", + "navigation": "탐색", + "addHost": "호스트 추가", + "addCredential": "자격 증명 추가", + "adminSettings": "관리자 설정", + "userProfile": "사용자 프로필", + "updateLog": "업데이트 로그", + "hosts": "호스트", + "openServerDetails": "서버 세부 정보 열기", + "openFileManager": "파일 관리자 열기", + "edit": "편집", + "links": "링크", + "github": "GitHub", + "support": "지원", + "discord": "Discord", + "donate": "기부", + "press": "누르기", + "toToggle": "토글하려면", + "close": "닫기", + "hostManager": "호스트 관리자" + } +} \ No newline at end of file diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index 9e9350e3..71623d95 100644 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -59,7 +59,6 @@ "keyTypeRSA": "RSA", "keyTypeECDSA": "ECDSA", "keyTypeEd25519": "Ed25519", - "updateCredential": "Atualizar Credencial", "basicInfo": "Informações básicas", "authentication": "Autenticação", "organization": "Organização", @@ -93,7 +92,7 @@ "deploySSHKey": "Implantar Chave SSH", "deploySSHKeyDescription": "Implantar chave pública no servidor de destino", "sourceCredential": "Credencial de Origem", - "targetHost": "Host de Destino", + "targetHost": "Host de Destinenhum", "deploymentProcess": "Processo de Implantação", "deploymentProcessDescription": "Isso adicionará com segurança a chave pública ao arquivo ~/.ssh/authorized_keys do host de destino sem sobrescrever chaves existentes. A operação é reversível.", "chooseHostToDeploy": "Escolha um host para implantar...", @@ -118,7 +117,6 @@ "credentialSecuredDescription": "Todos os dados sensíveis são criptografados com AES-256", "passwordAuthentication": "Autenticação por senha", "keyAuthentication": "Autenticação por chave", - "keyType": "Tipo de chave", "securityReminder": "Lembrete de segurança", "securityReminderText": "Nunca compartilhe suas credenciais. Todos os dados são criptografados em repouso.", "hostsUsingCredential": "Hosts usando esta credencial", @@ -166,7 +164,8 @@ "generateKeyPairNote": "Gere um novo par de chaves SSH diretamente. Isso substituirá quaisquer chaves existentes no formulário.", "invalidKey": "Chave inválida", "detectionError": "Erro de detecção", - "unknown": "Desconhecido" + "unknown": "Desconhecido", + "credentialId": "Credencial ID" }, "dragIndicator": { "error": "Erro: {{error}}", @@ -201,7 +200,7 @@ "noResults": "Nenhum comando encontrado", "noResultsHint": "Nenhum comando correspondente a \"{{query}}\"", "deleteSuccess": "Comando removido do histórico", - "deleteFailed": "Falha ao excluir comando.", + "deleteFailed": "Falha ao excluir comeo.", "deleteTooltip": "Excluir comando", "tabHint": "Use Tab no Terminal para autocompletar do histórico de comandos" }, @@ -219,21 +218,25 @@ "testConnectionFirst": "Por favor, teste a conexão primeiro", "connectionSuccess": "Conexão bem-sucedida!", "connectionFailed": "Conexão falhou", - "connectionError": "Ocorreu um erro de conexão", + "connectionError": "Ocoureu um erro de conexão", "connected": "Conectado", "disconnected": "Desconectado", "configSaved": "Configuração salva com sucesso", "saveFailed": "Falha ao salvar configuração", "saveError": "Erro ao salvar configuração", - "saving": "Salvando...", + "saving": "Salveo...", "saveConfig": "Salvar Configuração", - "helpText": "Digite a URL onde seu servidor Termix está rodando (ex.: http://localhost:30001 ou https://seu-servidor.com)" + "helpText": "Digite a URL onde seu servidor Termix está rodando (ex.: http://localhost:30001 ou https://seu-servidor.com)", + "changeServer": "Alterar Servidor", + "mustIncludeProtocol": "URL do Servidor deve começar com http:// ou https://", + "notValidatedWarning": "URL não validada - verifique se está correta", + "warning": "Aviso" }, "versionCheck": { "error": "Erro na verificação de versão", "checkFailed": "Falha ao verificar atualizações", "upToDate": "Aplicativo atualizado", - "currentVersion": "Você está usando a versão {{version}}", + "currentVersion": "Você está useo a versão {{version}}", "updateAvailable": "Atualização disponível", "newVersionAvailable": "Uma nova versão está disponível! Você está usando {{current}}, mas {{latest}} está disponível.", "releasedOn": "Lançada em {{date}}", @@ -255,12 +258,12 @@ "continue": "Continuar", "maintenance": "Manutenção", "degraded": "Degradado", - "discord": "Discord", + "discord": "Discoud", "error": "Erro", "warning": "Aviso", "info": "Info", "success": "Sucesso", - "loading": "Carregando", + "loading": "Carregando...", "required": "Obrigatório", "optional": "Opcional", "clear": "Limpar", @@ -274,14 +277,13 @@ "updateAvailable": "Atualização Disponível", "sshPath": "Caminho SSH", "localPath": "Caminho Local", - "loading": "Carregando...", "noAuthCredentials": "Não há credenciais de autenticação disponíveis para este host SSH", "noReleases": "Sem Versões", "updatesAndReleases": "Atualizações e Versões", "newVersionAvailable": "Uma nova versão ({{version}}) está disponível.", "failedToFetchUpdateInfo": "Falha ao buscar informações de atualização", "preRelease": "Pré-lançamento", - "loginFailed": "Falha no login", + "loginFailed": "Falha nenhum login", "noReleasesFound": "Nenhuma versão encontrada.", "yourBackupCodes": "Seus Códigos de Backup", "sendResetCode": "Enviar Código de Redefinição", @@ -289,13 +291,10 @@ "resetPassword": "Redefinir Senha", "resetCode": "Código de Redefinição", "newPassword": "Nova Senha", - "sshPath": "Caminho SSH", - "localPath": "Caminho Local", "folder": "Pasta", "file": "Arquivo", "renamedSuccessfully": "renomeado com sucesso", "deletedSuccessfully": "excluído com sucesso", - "noAuthCredentials": "Não há credenciais de autenticação disponíveis para este host SSH", "noTunnelConnections": "Não há conexões de túnel configuradas", "sshTools": "Ferramentas SSH", "english": "Inglês", @@ -307,36 +306,27 @@ "login": "Entrar", "logout": "Sair", "register": "Registrar", - "username": "Usuário", "password": "Senha", "version": "Versão", "confirmPassword": "Confirmar Senha", "back": "Voltar", "email": "Email", "submit": "Enviar", - "cancel": "Cancelar", "change": "Alterar", "save": "Salvar", "delete": "Excluir", "edit": "Editar", "add": "Adicionar", "search": "Buscar", - "loading": "Carregando...", - "error": "Erro", - "success": "Sucesso", - "warning": "Aviso", - "info": "Info", "confirm": "Confirmar", "yes": "Sim", "no": "Não", "ok": "OK", - "close": "Fechar", "enabled": "Habilitado", "disabled": "Desabilitado", "important": "Importante", "notEnabled": "Não Habilitado", "settingUp": "Configurando...", - "back": "Voltar", "next": "Próximo", "previous": "Anterior", "refresh": "Atualizar", @@ -353,7 +343,7 @@ "passwordResetDescription": "Você está prestes a redefinir sua senha. Isso fará com que você seja desconectado de todas as sessões ativas.", "enterSixDigitCode": "Digite o código de 6 dígitos dos logs do container docker para o usuário:", "enterNewPassword": "Digite sua nova senha para o usuário:", - "passwordsDoNotMatch": "As senhas não correspondem", + "passwordsDoNotMatch": "As senhas não courespondem", "passwordMinLength": "A senha deve ter pelo menos 6 caracteres", "passwordResetSuccess": "Senha redefinida com sucesso! Você pode agora entrar com sua nova senha.", "failedToInitiatePasswordReset": "Falha ao iniciar redefinição de senha", @@ -362,7 +352,8 @@ "documentation": "Documentação", "retry": "Tentar Novamente", "checking": "Verificando...", - "checkingDatabase": "Verificando conexão com o banco de dados..." + "checkingDatabase": "Verificando conexão com o banco de dados...", + "saving": "Salveo..." }, "nav": { "home": "Início", @@ -372,7 +363,7 @@ "tunnels": "Túneis", "fileManager": "Gerenciador de Arquivos", "serverStats": "Estatísticas do Servidor", - "admin": "Admin", + "admin": "Administrador", "userProfile": "Perfil do Usuário", "tools": "Ferramentas", "newTab": "Nova Aba", @@ -381,15 +372,16 @@ "sshManager": "Gerenciador SSH", "hostManager": "Gerenciador de Hosts", "cannotSplitTab": "Não é possível dividir esta aba", - "tabNavigation": "Navegação de Abas" + "tabNavigation": "Navegação de Abas", + "snippets": "Snippets" }, "admin": { - "title": "Configurações de Admin", + "title": "Configurações de Administrador", "oidc": "OIDC", "users": "Usuários", "userManagement": "Gerenciamento de Usuários", - "makeAdmin": "Tornar Admin", - "removeAdmin": "Remover Admin", + "makeAdmin": "Tornar Administrador", + "removeAdmin": "Remover Administrador", "deleteUser": "Excluir Usuário", "allowRegistration": "Permitir Registro", "oidcSettings": "Configurações OIDC", @@ -400,11 +392,11 @@ "tokenUrl": "URL do Token", "updateSettings": "Atualizar Configurações", "confirmDelete": "Tem certeza que deseja excluir este usuário?", - "confirmMakeAdmin": "Tem certeza que deseja tornar este usuário um admin?", - "confirmRemoveAdmin": "Tem certeza que deseja remover os privilégios de admin deste usuário?", + "confirmMakeAdmin": "Tem certeza que deseja tornar este usuário um administrador?", + "confirmRemoveAdmin": "Tem certeza que deseja remover os privilégios de administrador deste usuário?", "externalAuthentication": "Autenticação Externa (OIDC)", "configureExternalProvider": "Configure o provedor de identidade externo para autenticação OIDC/OAuth2.", - "userIdentifierPath": "Caminho do Identificador do Usuário", + "userIdentifierPath": "Caminho do Identificadou do Usuário", "displayNamePath": "Caminho do Nome de Exibição", "scopes": "Escopos", "saving": "Salvando...", @@ -419,12 +411,12 @@ "actions": "Ações", "external": "Externo", "local": "Local", - "adminManagement": "Gerenciamento de Admin", - "makeUserAdmin": "Tornar Usuário Admin", + "adminManagement": "Gerenciamento de Administrador", + "makeUserAdmin": "Tornar Usuário Administrador", "adding": "Adicionando...", - "currentAdmins": "Admins Atuais", - "adminBadge": "Admin", - "removeAdminButton": "Remover Admin", + "currentAdmins": "Administradores Atuais", + "adminBadge": "Administrador", + "removeAdminButton": "Remover Administrador", "general": "Geral", "userRegistration": "Registro de Usuário", "allowNewAccountRegistration": "Permitir registro de novas contas", @@ -436,13 +428,12 @@ "oidcConfigurationDisabled": "Configuração OIDC desativada com sucesso!", "failedToUpdateOidcConfig": "Falha ao atualizar configuração OIDC", "failedToDisableOidcConfig": "Falha ao desativar configuração OIDC", - "enterUsernameToMakeAdmin": "Insira o nome de usuário para tornar admin", + "enterUsernameToMakeAdmin": "Insira o nome de usuário para tornar administrador", "userIsNowAdmin": "O usuário {{username}} agora é um administrador", "failedToMakeUserAdmin": "Falha ao tornar o usuário administrador", "removeAdminStatus": "Remover status de administrador de {{username}}?", "adminStatusRemoved": "Status de administrador removido de {{username}}", "failedToRemoveAdminStatus": "Falha ao remover o status de administrador", - "confirmDeleteUser": "Excluir usuário {{username}}? Esta ação não pode ser desfeita.", "userDeletedSuccessfully": "Usuário {{username}} excluído com sucesso", "failedToDeleteUser": "Falha ao excluir usuário", "overrideUserInfoUrl": "Sobrescrever URL de informações do usuário (não obrigatório)", @@ -495,7 +486,6 @@ "verificationCompleted": "Verificação de compatibilidade concluída - nenhum dado foi alterado", "verificationInProgress": "Verificação concluída", "dataMigrationCompleted": "Migração de dados concluída com sucesso!", - "migrationCompleted": "Migração concluída", "verificationFailed": "Falha na verificação de compatibilidade", "migrationFailed": "Falha na migração", "runningVerification": "Executando verificação de compatibilidade...", @@ -540,13 +530,13 @@ "databaseImportFailed": "Falha na importação do banco de dados SQLite", "manageEncryptionAndBackups": "Gerenciar chaves de criptografia, segurança do banco de dados e operações de backup", "activeSecurityFeatures": "Medidas e proteções de segurança atualmente ativas", - "deviceBindingTechnology": "Tecnologia avançada de proteção de chave baseada em hardware", + "deviceBindingTechnology": "Tecnenhumlogia avançada de proteção de chave baseada em hardware", "backupAndRecovery": "Opções seguras de criação de backup e recuperação de banco de dados", "crossSystemDataTransfer": "Exportar e importar bancos de dados entre diferentes sistemas", "noMigrationNeeded": "Nenhuma migração necessária", "encryptionKey": "Chave de Criptografia", "keyProtection": "Proteção da Chave", - "active": "Ativo", + "active": "Ativa", "legacy": "Legado", "dataStatus": "Status dos Dados", "encrypted": "Criptografado", @@ -576,13 +566,38 @@ "confirmDisableOIDCWarning": "AVISO: Você está prestes a desativar o OIDC enquanto o login por senha também está desativado. Isso inutilizará sua instância do Termix e você perderá todo o acesso. Tem absoluta certeza de que deseja continuar?", "allowPasswordLogin": "Permitir login com nome de usuário/senha", "failedToFetchPasswordLoginStatus": "Falha ao buscar status do login por senha", - "failedToUpdatePasswordLoginStatus": "Falha ao atualizar status do login por senha" + "failedToUpdatePasswordLoginStatus": "Falha ao atualizar status do login por senha", + "accountsLinkedSuccessfully": "Usuário OIDC {{oidcUsername}} foi vinculado a {{targetUsername}}", + "confirmRevokeAllSessions": "Tem certeza de que deseja revogar todas as sessões para este usuário?", + "confirmRevokeSession": "Tem certeza de que deseja revogar esta sessão?", + "failedToFetchSessions": "Falha ao buscar sessões", + "failedToLinkAccounts": "Falha ao vincular contas", + "failedToRevokeSession": "Falha ao revogar sessão", + "failedToRevokeSessions": "Falha ao revogar sessões", + "failedToUnlinkOIDC": "Falha ao desvincular OIDC", + "linkAccountsButton": "Vincular Contas", + "linkOIDCActionAddCapability": "Adicionar capacidade de login OIDC à conta de senha de destino", + "linkOIDCActionDeleteUser": "Excluir a conta de usuário OIDC e todos os outros dados", + "linkOIDCActionDualAuth": "Permitir que a conta de senha faça login com senha e OIDC", + "linkOIDCDialogDescription": "Vincular {{username}} (usuário OIDC) a uma conta de senha existente. Isso ativará autenticação dupla para a conta de senha.", + "linkOIDCDialogTitle": "Link OIDC Account para Senha Account", + "linkOIDCWarningTitle": "Aviso: Dados do Usuário OIDC Serão Deletados", + "linkTargetUsernameLabel": "Nome de usuário da conta de senha de destino", + "linkTargetUsernamePlaceholder": "Inserir nome de usuário da conta de senha", + "linkTargetUsernameRequired": "Nome de usuário de destino é obrigatório", + "linkToPasswordAccount": "Vincular à Conta de Senha", + "linkingAccounts": "Vinculando...", + "sessionRevokedSuccessfully": "Sessão revogada com sucesso", + "sessionsRevokedSuccessfully": "Sessões revogadas com sucesso", + "unlinkOIDCDescription": "Remover autenticação OIDC de {{username}}? O usuário só poderá fazer login com nome de usuário/senha após isso.", + "unlinkOIDCSuccess": "OIDC desvinculado de {{username}}", + "unlinkOIDCTitle": "Desvincular Autenticação OIDC" }, "hosts": { "title": "Gerenciador de Hosts", "sshHosts": "Hosts SSH", "noHosts": "Sem Hosts SSH", - "noHostsMessage": "Você ainda não adicionou nenhum host SSH. Clique em \"Adicionar Host\" para começar.", + "noHostsMessage": "Você ainda não adicionenhumu nenhum host SSH. Clique em \"Adicionar Host\" para começar.", "loadingHosts": "Carregando hosts...", "failedToLoadHosts": "Falha ao carregar hosts", "retry": "Tentar Novamente", @@ -596,12 +611,12 @@ "formatGuide": "Guia de Formato", "exportCredentialWarning": "Aviso: O host \"{{name}}\" usa autenticação por credencial. O arquivo exportado não incluirá os dados da credencial e precisará ser reconfigurado manualmente após a importação. Deseja continuar?", "exportSensitiveDataWarning": "Aviso: O host \"{{name}}\" contém dados de autenticação sensíveis (senha/chave SSH). O arquivo exportado incluirá esses dados em texto simples. Mantenha o arquivo seguro e exclua-o após o uso. Deseja continuar?", - "uncategorized": "Sem categoria", + "uncategorized": "Sem categouia", "confirmDelete": "Tem certeza que deseja excluir \"{{name}}\"?", "failedToDeleteHost": "Falha ao excluir host", "failedToExportHost": "Falha ao exportar host. Certifique-se de que está logado e tem acesso aos dados do host.", "jsonMustContainHosts": "O JSON deve conter um array \"hosts\" ou ser um array de hosts", - "noHostsInJson": "Nenhum host encontrado no arquivo JSON", + "noHostsInJson": "Nenhum host encontrado nenhum arquivo JSON", "maxHostsAllowed": "Máximo de 100 hosts permitidos por importação", "importCompleted": "Importação concluída: {{success}} com sucesso, {{failed}} falhas", "importFailed": "Falha na importação", @@ -611,7 +626,7 @@ "organization": "Organização", "ipAddress": "Endereço IP", "port": "Porta", - "name": "Nome", + "name": "Nenhumme", "username": "Usuário", "folder": "Pasta", "tags": "Tags", @@ -632,13 +647,13 @@ "enableTerminalDesc": "Habilitar/desabilitar visibilidade do host na aba Terminal", "enableTunnel": "Habilitar Túnel", "enableTunnelDesc": "Habilitar/desabilitar visibilidade do host na aba Túnel", - "enableFileManager": "Habilitar Gerenciador de Arquivos", - "enableFileManagerDesc": "Habilitar/desabilitar visibilidade do host na aba Gerenciador de Arquivos", + "enableFileManager": "Habilitar Gerenciadou de Arquivos", + "enableFileManagerDesc": "Habilitar/desabilitar visibilidade do host na aba Gerenciadou de Arquivos", "defaultPath": "Caminho Padrão", - "defaultPathDesc": "Diretório padrão ao abrir o gerenciador de arquivos para este host", + "defaultPathDesc": "Diretório padrão ao abrir o gerenciadou de arquivos para este host", "tunnelConnections": "Conexões de Túnel", "connection": "Conexão", - "remove": "Remover", + "remove": "Removerr", "sourcePort": "Porta de Origem", "sourcePortDesc": "(Source refere-se aos Detalhes da Conexão Atual na aba Geral)", "endpointPort": "Porta de Destino", @@ -695,12 +710,12 @@ "addTagsSpaceToAdd": "adicionar tags (espaço para adicionar)", "terminalBadge": "Terminal", "tunnelBadge": "Túnel", - "fileManagerBadge": "Gerenciador de Arquivos", + "fileManagerBadge": "Gerenciadou de Arquivos", "general": "Geral", "terminal": "Terminal", "tunnel": "Túnel", - "fileManager": "Gerenciador de Arquivos", - "hostViewer": "Visualizador de Host", + "fileManager": "Gerenciadou de Arquivos", + "hostViewer": "Visualizadou de Host", "confirmRemoveFromFolder": "Tem certeza que deseja remover \"{{name}}\" da pasta \"{{folder}}\"? O host será movido para \"Sem Pasta\".", "removedFromFolder": "Host \"{{name}}\" removido da pasta com sucesso", "failedToRemoveFromFolder": "Falha ao remover host da pasta", @@ -745,7 +760,105 @@ "searchServers": "Pesquisar servidores...", "noServerFound": "Nenhum servidor encontrado", "jumpHostsOrder": "As conexões serão feitas na ordem: Host de Salto 1 → Host de Salto 2 → ... → Servidor de Destino", - "advancedAuthSettings": "Configurações Avançadas de Autenticação" + "advancedAuthSettings": "Configurações Avançadas de Autenticação", + "addQuickAction": "Adicionar Ação Rápida", + "adjustFontSize": "Ajustar o tamanho da fonte do terminal", + "adjustLetterSpacing": "Ajustar espaçamento entre caracteres", + "adjustLineHeight": "Ajustar espaçamento entre linhas", + "advanced": "Avançado", + "allHostsInFolderDeleted": "Excluídos {{count}} hosts da pasta \"{{folder}}\" com sucesso", + "appearance": "Aparência", + "backspaceMode": "Modo Backspace", + "backspaceModeControlH": "Control-H (^H)", + "backspaceModeDesc": "Comportamento da tecla Backspace para compatibilidade", + "backspaceModeNormal": "Normal (DEL)", + "behavior": "Comportamento", + "bellStyle": "Estilo de Campainha", + "bellStyleBoth": "Both", + "bellStyleDesc": "Como lidar com a campainha do terminal (caractere BEL, \\x07). Programas acionam isso ao completar tarefas, encontrar erros, ou para notificações. \"Som\" toca um bipe de áudio, \"Visual\" pisca a tela brevemente, \"Ambos\" faz os dois, \"Nenhum\" desativa os alertas de campainha.", + "bellStyleNone": "Nenhum", + "bellStyleSound": "Som", + "bellStyleVisual": "Visual", + "chooseColorTheme": "Escolha um tema de cor para o terminal", + "chooseCursorAppearance": "Escolha a aparência do cursor", + "confirmDeleteAllHostsInFolder": "Tem certeza de que deseja excluir todos os {{count}} hosts na pasta \"{{folder}}\"? Esta ação não pode ser desfeita.", + "cursorBlink": "Piscar Cursor", + "cursorStyle": "Estilo do Cursor", + "cursorStyleBar": "Barra", + "cursorStyleBlock": "Bloco", + "cursorStyleUnderline": "Sublinhado", + "customCommands": "Comandos Personalizados (Em Breve)", + "customCommandsDesc": "Defina comandos personalizados de desligamento e reinicialização para este servidor", + "deleteAllHostsInFolder": "Excluir Todos os Hosts na Pasta", + "displayItems": "Exibir Itens", + "displayItemsDesc": "Escolha quais métricas exibir na página de estatísticas do servidor", + "editFolderAppearance": "Editar Aparência da Pasta", + "editFolderAppearanceDesc": "Personalize a cor e o ícone da pasta", + "enableCpu": "Uso da CPU", + "enableCursorBlink": "Ativar animação de piscar do cursor", + "enableDisk": "Uso de Disco", + "enableHostname": "Hostname (Em Breve)", + "enableMemory": "Uso de Memória", + "enableNetwork": "Estatísticas de Rede (Em Breve)", + "enableOs": "Sistema Operacional (Em Breve)", + "enableProcesses": "Contagem de Processos (Em Breve)", + "enableServerStats": "Ativar Estatísticas do Servidor", + "enableServerStatsDesc": "Ativar/desativar coleta de estatísticas do servidor para este host", + "enableUptime": "Tempo de Atividade (Em Breve)", + "failedToDeleteHostsInFolder": "Falha ao excluir hosts na pasta", + "failedToUpdateFolderAppearance": "Falha ao atualizar aparência da pasta", + "fastScrollModifier": "Modificador de Rolagem Rápida", + "fastScrollModifierDesc": "Tecla modificadora para rolagem rápida", + "fastScrollSensitivity": "Sensibilidade de Rolagem Rápida", + "fastScrollSensitivityDesc": "Multiplicador de velocidade de rolagem quando modificador é segurado", + "fastScrollSensitivityValue": "Sensibilidade de Rolagem Rápida: {{value}}", + "folderAppearanceUpdated": "Aparência da pasta atualizada com sucesso", + "folderColor": "Cor da Pasta", + "folderIcon": "Ícone da Pasta", + "fontFamily": "Família da Fonte", + "fontSize": "Tamanho da Fonte", + "fontSizeValue": "Tamanho da Fonte: {{value}}px", + "letterSpacing": "Espaçamento entre Letras", + "letterSpacingValue": "Espaçamento entre Letras: {{value}}px", + "lineHeight": "Altura da Linha", + "lineHeightValue": "Altura da Linha: {{value}}", + "minimumContrastRatio": "Taxa de Contraste Mínima", + "minimumContrastRatioDesc": "Ajustar cores automaticamente para melhor legibilidade", + "minimumContrastRatioValue": "Taxa de Contraste Mínima: {{value}}", + "modifierAlt": "Alt", + "modifierCtrl": "Ctrl", + "modifierShift": "Shift", + "noSnippetFound": "Nenhum snippet encontrado", + "preview": "Pré-visualização", + "quickActionName": "Nome da Ação", + "quickActions": "Ações Rápidas", + "quickActionsDescription": "Ações rápidas permitem criar botões personalizados que executam snippets SSH neste servidor. Esses botões aparecerão no topo da página de Estatísticas do Servidor para acesso rápido.", + "quickActionsList": "Lista de Ações Rápidas", + "quickActionsOrder": "Botões de ação rápida aparecerão na ordem listada acima na página de Estatísticas do Servidor", + "rebootCommand": "Comando de Reinicialização", + "rightClickSelectsWord": "Clique Direito Seleciona Palavra", + "rightClickSelectsWordDesc": "Clicar com o botão direito seleciona a palavra sob o cursor", + "scrollbackBuffer": "Histórico de Rolagem", + "scrollbackBufferDesc": "Número de linhas para manter no histórico de rolagem", + "scrollbackBufferValue": "Histórico de Rolagem: {{value}} linhas", + "searchSnippets": "Pesquisar snippets...", + "selectBackspaceMode": "Selecionar modo backspace", + "selectBellStyle": "Selecionar estilo de campainha", + "selectCursorStyle": "Selecionar estilo de cursor", + "selectFont": "Selecionar fonte", + "selectFontDesc": "Selecione a fonte para usar no terminal", + "selectModifier": "Selecionar modificador", + "selectSnippet": "Selecionar snippet", + "selectTheme": "Selecionar tema", + "serverStats": "Estatísticas do Servidor", + "shutdownCommand": "Comando de Desligamento", + "snippetNone": "Nenhum", + "sshAgentForwarding": "Encaminhamento de Agente SSH", + "sshAgentForwardingDesc": "Encaminhar agente de autenticação SSH para host remoto", + "startupSnippet": "Snippet de Inicialização", + "terminalCustomization": "Personalização do Terminal", + "theme": "Tema", + "themePreview": "Pré-visualização do Tema" }, "terminal": { "title": "Terminal", @@ -779,7 +892,11 @@ "connectionTimeout": "Tempo limite de conexão esgotado", "terminalTitle": "Terminal - {{host}}", "terminalWithPath": "Terminal - {{host}}:{{path}}", - "runTitle": "Executando {{command}} - {{host}}" + "runTitle": "Executando {{command}} - {{host}}", + "totpCodeLabel": "Código de Verificação", + "totpPlaceholder": "000000", + "totpRequired": "Autenticação de Dois Fatores Obrigatória", + "totpVerify": "Verificar" }, "fileManager": { "title": "Gerenciador de Arquivos", @@ -865,7 +982,6 @@ "copyPaths": "Copiar caminhos", "delete": "Excluir", "properties": "Propriedades", - "preview": "Visualizar", "refresh": "Atualizar", "downloadFiles": "Baixar {{count}} arquivos para o Navegador", "copyFiles": "Copiar {{count}} itens", @@ -880,18 +996,11 @@ "failedToDeleteItem": "Falha ao excluir item", "itemRenamedSuccessfully": "{{type}} renomeado com sucesso", "failedToRenameItem": "Falha ao renomear item", - "upload": "Enviar", "download": "Baixar", - "newFile": "Novo Arquivo", - "newFolder": "Nova Pasta", - "rename": "Renomear", - "delete": "Excluir", "permissions": "Permissões", "size": "Tamanho", "modified": "Modificado", "path": "Caminho", - "fileName": "Nome do Arquivo", - "folderName": "Nome da Pasta", "confirmDelete": "Tem certeza que deseja excluir {{name}}?", "uploadSuccess": "Arquivo enviado com sucesso", "uploadFailed": "Falha ao enviar arquivo", @@ -911,10 +1020,7 @@ "fileSavedSuccessfully": "Arquivo salvo com sucesso", "saveTimeout": "Tempo limite da operação de salvamento esgotado. O arquivo pode ter sido salvo com sucesso, mas a operação demorou muito para ser concluída. Verifique os logs do Docker para confirmação.", "failedToSaveFile": "Falha ao salvar arquivo", - "folder": "Pasta", - "file": "Arquivo", "deletedSuccessfully": "excluído com sucesso", - "failedToDeleteItem": "Falha ao excluir item", "connectToServer": "Conectar a um Servidor", "selectServerToEdit": "Selecione um servidor da barra lateral para começar a editar arquivos", "fileOperations": "Operações de Arquivo", @@ -943,7 +1049,7 @@ "sshReconnectionTimeout": "Tempo limite excedido na reconexão SSH", "saveOperationTimeout": "Tempo limite excedido na operação de salvar", "cannotSaveFile": "Não é possível salvar o arquivo", - "dragSystemFilesToUpload": "Arraste arquivos do sistema aqui para fazer upload", + "dragSystemFilesToUpload": "Arraste arquivos do sistema aqui para enviar", "dragFilesToWindowToDownload": "Arraste arquivos para fora da janela para baixar", "openTerminalHere": "Abrir Terminal Aqui", "run": "Executar", @@ -971,14 +1077,12 @@ "unpinFile": "Desfixar arquivo", "removeShortcut": "Remover atalho", "saveFilesToSystem": "Salvar {{count}} arquivos como...", - "saveToSystem": "Salvar como...", "pinFile": "Fixar arquivo", "addToShortcuts": "Adicionar aos atalhos", - "selectLocationToSave": "Selecionar local para salvar", "downloadToDefaultLocation": "Baixar para o local padrão", "pasteFailed": "Falha ao colar", "noUndoableActions": "Nenhuma ação pode ser desfeita", - "undoCopySuccess": "Operação de cópia desfeita: {{count}} arquivos copiados foram excluídos", + "undoCopySuccess": "oOperação de cópia desfeita: {{count}} arquivos copiados foram excluídos", "undoCopyFailedDelete": "Falha ao desfazer: Não foi possível excluir os arquivos copiados", "undoCopyFailedNoInfo": "Falha ao desfazer: Não foi possível encontrar informações do arquivo copiado", "undoMoveSuccess": "Operação de mover desfeita: {{count}} arquivos movidos de volta ao local original", @@ -989,10 +1093,9 @@ "undoOperationFailed": "Falha na operação de desfazer", "unknownError": "Erro desconhecido", "enterPath": "Digite o caminho...", - "editPath": "Editar caminho", + "editPath": "Editarar caminho", "confirm": "Confirmar", "cancel": "Cancelar", - "folderName": "Nome da pasta", "find": "Localizar...", "replaceWith": "Substituir por...", "replace": "Substituir", @@ -1016,25 +1119,20 @@ "toggleComment": "Alternar Comentário", "indent": "Indentar", "outdent": "Remover Indentação", - "autoComplete": "Auto Completar", + "autoComplete": "Autocompletar", "imageLoadError": "Falha ao carregar imagem", - "zoomIn": "Aumentar Zoom", - "zoomOut": "Diminuir Zoom", "rotate": "Rotacionar", "originalSize": "Tamanho Original", "startTyping": "Comece a digitar...", "unknownSize": "Tamanho desconhecido", "fileIsEmpty": "Arquivo está vazio", - "modified": "Modificado", "largeFileWarning": "Aviso de Arquivo Grande", "largeFileWarningDesc": "Este arquivo tem {{size}} de tamanho, o que pode causar problemas de desempenho quando aberto como texto.", "fileNotFoundAndRemoved": "Arquivo \"{{name}}\" não encontrado e foi removido dos arquivos recentes/fixados", "failedToLoadFile": "Falha ao carregar arquivo: {{error}}", "serverErrorOccurred": "Ocorreu um erro no servidor. Por favor, tente novamente mais tarde.", - "fileSavedSuccessfully": "Arquivo salvo com sucesso", "autoSaveFailed": "Falha no salvamento automático", "fileAutoSaved": "Arquivo salvo automaticamente", - "moveFileFailed": "Falha ao mover {{name}}", "moveOperationFailed": "Falha na operação de mover", "canOnlyCompareFiles": "Só é possível comparar dois arquivos", @@ -1049,7 +1147,7 @@ "operationCompletedSuccessfully": "{{operation}} {{count}} itens com sucesso", "operationCompleted": "{{operation}} {{count}} itens", "downloadFileSuccess": "Arquivo {{name}} baixado com sucesso", - "downloadFileFailed": "Falha no download", + "downloadFileFailed": "Falha no baixar", "moveTo": "Mover para {{name}}", "diffCompareWith": "Comparar diferenças com {{name}}", "dragOutsideToDownload": "Arraste para fora da janela para baixar ({{count}} arquivos)", @@ -1068,12 +1166,42 @@ "fileComparison": "Comparação de Arquivos: {{file1}} vs {{file2}}", "fileTooLarge": "Arquivo muito grande: {{error}}", "sshConnectionFailed": "Falha na conexão SSH. Por favor, verifique sua conexão com {{name}} ({{ip}}:{{port}})", - "loadFileFailed": "Falha ao carregar arquivo: {{error}}" + "loadFileFailed": "Falha ao carregar arquivo: {{error}}", + "andMoreFiles": "e {{count}} mais...", + "archiveExtractedSuccessfully": "{{name}} extraído com sucesso", + "archiveName": "Nome do Arquivo", + "changePermissions": "Alterar Permissões", + "changePermissionsDesc": "Modificar permissões do arquivo para", + "compress": "Comprimir", + "compressFailed": "Falha na compressão", + "compressFile": "Comprimir Arquivo", + "compressFiles": "Comprimir Arquivos", + "compressFilesDesc": "Comprimir {{count}} itens em um arquivo", + "compressingFiles": "Comprimindo {{count}} itens em {{name}}...", + "compressionFormat": "Formato de Compressão", + "connectedSuccessfully": "Conectado com sucesso", + "currentPermissions": "Permissões Atuais", + "enterArchiveName": "Inserir nome do arquivo...", + "execute": "Executar", + "extractArchive": "Extrair Arquivo", + "extractFailed": "Extração falhou", + "extractingArchive": "Extraindo {{name}}...", + "failedToChangePermissions": "Falha ao alterar permissões", + "filesCompressedSuccessfully": "{{name}} criado com sucesso", + "group": "Grupo", + "newPermissions": "Novas Permissões", + "others": "Outros", + "owner": "Proprietário", + "permissionsChangedSuccessfully": "Permissões alteradas com sucesso", + "read": "Leitura", + "selectedFiles": "Arquivos Selecionados", + "totpVerificationFailed": "Verificação TOTP falhou", + "write": "Escrita" }, "tunnels": { "title": "Túneis SSH", "noSshTunnels": "Sem Túneis SSH", - "createFirstTunnelMessage": "Você ainda não criou nenhum túnel SSH. Configure conexões de túnel no Gerenciador de Hosts para começar.", + "createFirstTunnelMessage": "Crie seu primeiro túnel SSH para começar. Use o Gerenciador SSH para adicionar hosts com conexões de túnel.", "connected": "Conectado", "disconnected": "Desconectado", "connecting": "Conectando...", @@ -1093,7 +1221,7 @@ "port": "Porta", "attempt": "Tentativa {{current}} de {{max}}", "nextRetryIn": "Próxima tentativa em {{seconds}} segundos", - "checkDockerLogs": "Verifique seus logs do Docker para ver o motivo do erro, entre no", + "checkDockerLogs": "Verifique seus logs do Docker para ver o motivo do erro", "noTunnelConnections": "Nenhuma conexão de túnel configurada", "tunnelConnections": "Conexões de Túnel", "addTunnel": "Adicionar Túnel", @@ -1114,18 +1242,9 @@ "local": "Local", "remote": "Remoto", "dynamic": "Dinâmico", - "noSshTunnels": "Sem Túneis SSH", - "createFirstTunnelMessage": "Crie seu primeiro túnel SSH para começar. Use o Gerenciador SSH para adicionar hosts com conexões de túnel.", "unknownConnectionStatus": "Desconhecido", - "connected": "Conectado", - "connecting": "Conectando...", - "disconnecting": "Desconectando...", - "disconnected": "Desconectado", "portMapping": "Porta {{sourcePort}} → {{endpointHost}}:{{endpointPort}}", - "disconnect": "Desconectar", - "connect": "Conectar", - "canceling": "Cancelando...", - "endpointHostNotFound": "Host de destino não encontrado", + "endpointHostNotFound": "Host de destinenhum não encontrado", "discord": "Discord", "githubIssue": "issue no GitHub", "forHelp": "para ajuda" @@ -1136,8 +1255,8 @@ "memory": "Memória", "disk": "Disco", "network": "Rede", - "uptime": "Tempo Ativo", - "loadAverage": "Carga Média", + "uptime": "Tempo de Atividade", + "loadAverage": "Média: {{avg1}}, {{avg5}}, {{avg15}}", "processes": "Processos", "connections": "Conexões", "usage": "Uso", @@ -1153,7 +1272,6 @@ "cpuCores_one": "{{count}} CPU", "cpuCores_other": "{{count}} CPUs", "naCpus": "N/D CPU(s)", - "loadAverage": "Média: {{avg1}}, {{avg5}}, {{avg15}}", "loadAverageNA": "Média: N/D", "cpuUsage": "Uso da CPU", "memoryUsage": "Uso de Memória", @@ -1169,8 +1287,40 @@ "serverOffline": "Servidor Offline", "cannotFetchMetrics": "Não é possível buscar métricas do servidor offline", "load": "Carga", - "free": "Livre", - "available": "Disponível" + "addWidget": "Adicionar Widget", + "cancelEdit": "Cancelar", + "diskUsage": "Uso de Disco", + "editLayout": "Editar Layout", + "executeQuickAction": "Executar {{name}}", + "executingQuickAction": "Executando {{name}}...", + "failedToSaveLayout": "Falha ao salvar layout", + "from": "de", + "hostname": "Hostname", + "kernel": "Kernel", + "layoutSaved": "Layout salvo com sucesso", + "loginStats": "Estatísticas de Login SSH", + "networkInterfaces": "Interfaces de Rede", + "noInterfacesFound": "Nenhuma interface de rede encontrada", + "noProcessesFound": "Nenhum processo encontrado", + "noRecentLoginData": "Nenhum dado de login recente", + "operatingSystem": "Sistema Operacional", + "quickActionError": "Falha ao executar {{name}}", + "quickActionFailed": "{{name}} falhou", + "quickActionSuccess": "{{name}} concluído com sucesso", + "quickActions": "Ações Rápidas", + "recentFailedAttempts": "Tentativas Falhas Recentemente", + "recentSuccessfulLogins": "Logins Bem-sucedidos Recentemente", + "running": "Executando", + "saveLayout": "Salvar Layout", + "seconds": "segundos", + "systemInfo": "Informações do Sistema", + "totalLogins": "Total de Logins", + "totalProcesses": "Total de Processos", + "totalUptime": "Tempo de Atividade Total", + "totpRequired": "Autenticação TOTP Obrigatória", + "totpUnavailable": "Estatísticas do Servidor indisponíveis para servidores com TOTP ativado", + "uniqueIPs": "IPs Únicos", + "unsavedChanges": "Alterações não salvas" }, "auth": { "tagline": "GERENCIADOR DE TERMINAL SSH", @@ -1242,7 +1392,7 @@ "enableTwoFactorButton": "Ativar Autenticação de Dois Fatores", "addExtraSecurityLayer": "Adicione uma camada extra de segurança à sua conta", "firstUser": "Primeiro Usuário", - "firstUserMessage": "Você é o primeiro usuário e será tornado admin. Você pode ver as configurações de admin no menu suspenso do usuário na barra lateral. Se você acha que isso é um erro, verifique os logs do docker ou crie uma issue no GitHub.", + "firstUserMessage": "Você é o primeiro usuário e será tornado administrador. Você pode ver as configurações de administrador no menu suspenso do usuário na barra lateral. Se você acha que isso é um erro, verifique os logs do docker ou crie uma issue no GitHub.", "external": "Externo", "loginWithExternal": "Entrar com Provedor Externo", "loginWithExternalDesc": "Entre usando seu provedor de identidade externo configurado", @@ -1269,7 +1419,18 @@ "sshTimeoutDescription": "A tentativa de autenticação expirou. Por favor, tente novamente.", "sshProvideCredentialsDescription": "Por favor, forneça suas credenciais SSH para conectar a este servidor.", "sshPasswordDescription": "Digite a senha para esta conexão SSH.", - "sshKeyPasswordDescription": "Se sua chave SSH estiver criptografada, digite a senha aqui." + "sshKeyPasswordDescription": "Se sua chave SSH estiver criptografada, digite a senha aqui.", + "authenticating": "Autenticando...", + "authenticationDisabled": "Autenticação Desativada", + "authenticationDisabledDesc": "Todos os métodos de autenticação estão atualmente desativados. Por favor, contate seu administrador.", + "desktopApp": "App Desktop", + "loadingServer": "Carregando servidor...", + "loggingInToDesktopApp": "Entrando no app desktop", + "loggingInToDesktopAppViaWeb": "Entrando no app desktop via interface web", + "loggingInToMobileApp": "Entrando no app mobile", + "mobileApp": "App Mobile", + "redirectingToApp": "Redirecionando para o app...", + "passwordResetSuccessDesc": "Sua senha foi redefinida com sucesso. Você pode agora entrar com sua nova senha." }, "errors": { "notFound": "Página não encontrada", @@ -1300,7 +1461,8 @@ "emailExists": "Email já existe", "loadFailed": "Falha ao carregar dados", "saveError": "Falha ao salvar", - "sessionExpired": "Sessão expirada - por favor, faça login novamente" + "sessionExpired": "Sessão expirada - por favor, faça login novamente", + "passwordLoginDisabled": "Login com nome de usuário/senha está atualmente desativado" }, "messages": { "saveSuccess": "Salvo com sucesso", @@ -1345,9 +1507,12 @@ "fileColorCodingDesc": "Codificar arquivos por cores por tipo: pastas (vermelho), arquivos (azul), links simbólicos (verde)", "commandAutocomplete": "Autocompletar Comandos", "commandAutocompleteDesc": "Ativar sugestões de autocompletar com a tecla Tab para comandos do terminal baseado no seu histórico", + "defaultSnippetFoldersCollapsed": "Recolher Pastas de Snippets por Padrão", + "defaultSnippetFoldersCollapsedDesc": "Quando ativado, todas as pastas de snippets serão recolhidas ao abrir a aba de snippets", "currentPassword": "Senha Atual", "passwordChangedSuccess": "Senha alterada com sucesso! Por favor, faça login novamente.", - "failedToChangePassword": "Falha ao alterar a senha. Por favor, verifique sua senha atual e tente novamente." + "failedToChangePassword": "Falha ao alterar a senha. Por favor, verifique sua senha atual e tente novamente.", + "externalAndLocal": "Autenticação Dupla" }, "user": { "failedToLoadVersionInfo": "Falha ao carregar informações da versão" @@ -1377,10 +1542,10 @@ "redirectUrl": "https://seu-provedor.com/application/o/termix/", "tokenUrl": "https://seu-provedor.com/application/o/token/", "userIdField": "sub", - "usernameField": "name", + "usernameField": "nome", "scopes": "openid email profile", - "userinfoUrl": "https://your-provider.com/application/o/userinfo/", - "enterUsername": "Digite o nome de usuário para tornar admin", + "userinfoUrl": "https://seu-provider.com/application/o/userinfo/", + "enterUsername": "Digite o nome de usuário para tornar administrador", "searchHosts": "Procurar hosts por nome, usuário, IP, pasta, tags...", "enterPassword": "Digite sua senha", "totpCode": "Código TOTP de 6 dígitos", @@ -1398,9 +1563,9 @@ "noFolder": "Sem Pasta", "passwordRequired": "Senha é obrigatória", "failedToDeleteAccount": "Falha ao excluir conta", - "failedToMakeUserAdmin": "Falha ao tornar usuário admin", - "userIsNowAdmin": "Usuário {{username}} agora é um admin", - "removeAdminConfirm": "Tem certeza que deseja remover o status de admin de {{username}}?", + "failedToMakeUserAdmin": "Falha ao tornar usuário administrador", + "userIsNowAdmin": "Usuário {{username}} agora é um administrador", + "removeAdminConfirm": "Tem certeza que deseja remover o status de administrador de {{username}}?", "deleteUserConfirm": "Tem certeza que deseja excluir o usuário {{username}}? Esta ação não pode ser desfeita.", "deleteAccount": "Excluir Conta", "closeDeleteAccount": "Fechar Exclusão de Conta", @@ -1408,7 +1573,7 @@ "deleteAccountWarningDetails": "Excluir sua conta removerá todos os seus dados, incluindo hosts SSH, configurações e preferências. Esta ação é irreversível.", "deleteAccountWarningShort": "Esta ação é irreversível e excluirá permanentemente sua conta.", "cannotDeleteAccount": "Não é Possível Excluir Conta", - "lastAdminWarning": "Você é o último usuário administrador. Você não pode excluir sua conta pois isso deixaria o sistema sem administradores. Por favor, torne outro usuário administrador primeiro, ou contate o suporte do sistema.", + "lastAdminWarning": "Você é o último administrador. Você não pode excluir sua conta pois isso deixaria o sistema sem administradores. Por favor, torne outro usuário administrador primeiro, ou contate o suporte do sistema.", "confirmPassword": "Confirmar Senha", "deleting": "Excluindo...", "cancel": "Cancelar" @@ -1431,7 +1596,6 @@ "deleteItem": "Excluir Item", "createNewFile": "Criar Novo Arquivo", "createNewFolder": "Criar Nova Pasta", - "deleteItem": "Excluir Item", "renameItem": "Renomear Item", "clickToSelectFile": "Clique para selecionar um arquivo", "noSshHosts": "Sem Hosts SSH", @@ -1527,5 +1691,95 @@ "mobileAppInProgressDesc": "Estamos trabalhando em um aplicativo móvel dedicado para proporcionar uma melhor experiência em dispositivos móveis.", "viewMobileAppDocs": "Instalar Aplicativo Móvel", "mobileAppDocumentation": "Documentação do Aplicativo Móvel" + }, + "commandPalette": { + "addCredential": "Adicionar Credencial", + "addHost": "Adicionar Host", + "adminSettings": "Configurações de Administrador", + "close": "Fechar", + "discord": "Discord", + "donate": "Doar", + "edit": "Editar", + "github": "GitHub", + "hostManager": "Gerenciador de Hosts", + "hosts": "Hosts", + "links": "Links", + "navigation": "Navegação", + "openFileManager": "Abrir Gerenciador de Arquivos", + "openServerDetails": "Abrir Detalhes do Servidor", + "press": "Pressione", + "recentActivity": "Atividade Recente", + "searchPlaceholder": "Pesquisar por hosts ou ações rápidas...", + "support": "Suporte", + "toToggle": "para alternar", + "updateLog": "Log de Atualizações", + "userProfile": "Perfil do Usuário" + }, + "dashboard": { + "addCredential": "Adicionar Credencial", + "addHost": "Adicionar Host", + "adminSettings": "Configurações de Administrador", + "cpu": "CPU", + "database": "Banco de Dados", + "discord": "Discord", + "donate": "Doar", + "error": "Erro", + "github": "GitHub", + "healthy": "Saudável", + "loadingRecentActivity": "Carregando atividade recente...", + "loadingServerStats": "Carregando estatísticas do servidor...", + "noRecentActivity": "Nenhuma atividade recente", + "noServerData": "Nenhum dado do servidor disponível", + "notAvailable": "N/D", + "quickActions": "Ações Rápidas", + "ram": "RAM", + "recentActivity": "Atividade Recente", + "reset": "Redefinir", + "serverOverview": "Visão Geral do Servidor", + "serverStats": "Estatísticas do Servidor", + "support": "Suporte", + "title": "Painel", + "totalCredentials": "Total de Credenciais", + "totalServers": "Total de Servidores", + "totalTunnels": "Total de Túneis", + "upToDate": "Atualizado", + "updateAvailable": "Atualização Disponível", + "uptime": "Tempo de Atividade", + "userProfile": "Perfil do Usuário", + "version": "Versão" + }, + "snippets": { + "content": "Comando", + "contentPlaceholder": "ex: sudo systemctl restart nginx", + "contentRequired": "Comando é obrigatório", + "copySuccess": "Copiado \"{{name}}\" para área de transferência", + "copyTooltip": "Copiar snippet para área de transferência", + "create": "Criar Snippet", + "createDescription": "Criar um novo snippet de comando para execução rápida", + "createFailed": "Falha ao criar snippet", + "createSuccess": "Snippet criado com sucesso", + "deleteConfirmDescription": "Tem certeza de que deseja excluir \"{{name}}\"?", + "deleteConfirmTitle": "Excluir Snippet", + "deleteFailed": "Falha ao excluir snippet", + "deleteSuccess": "Snippet excluído com sucesso", + "deleteTooltip": "Excluir este snippet", + "description": "Descrição", + "descriptionPlaceholder": "Descrição opcional", + "edit": "Editar Snippet", + "editDescription": "Editar este snippet de comando", + "editTooltip": "Editar este snippet", + "empty": "Nenhum snippet ainda", + "emptyHint": "Crie um snippet para salvar comandos comumente usados", + "executeSuccess": "Executando: {{name}}", + "failedToFetch": "Falha ao buscar snippets", + "name": "Nome", + "namePlaceholder": "ex: Reiniciar Nginx", + "nameRequired": "Nome é obrigatório", + "new": "Novo Snippet", + "run": "Executar", + "runTooltip": "Executar este snippet no terminal", + "title": "Snippets", + "updateFailed": "Falha ao atualizar snippet", + "updateSuccess": "Snippet atualizado com sucesso" } -} +} \ No newline at end of file diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json index b7d329ab..6507f8e8 100644 --- a/src/locales/ru/translation.json +++ b/src/locales/ru/translation.json @@ -59,7 +59,6 @@ "keyTypeRSA": "RSA", "keyTypeECDSA": "ECDSA", "keyTypeEd25519": "Ed25519", - "updateCredential": "Обновить учетные данные", "basicInfo": "Основная информация", "authentication": "Аутентификация", "organization": "Организация", @@ -118,7 +117,6 @@ "credentialSecuredDescription": "Все конфиденциальные данные зашифрованы с помощью AES-256", "passwordAuthentication": "Аутентификация по паролю", "keyAuthentication": "Аутентификация по ключу", - "keyType": "Тип ключа", "securityReminder": "Напоминание о безопасности", "securityReminderText": "Никогда не передавайте ваши учетные данные. Все данные зашифрованы при хранении.", "hostsUsingCredential": "Хосты, использующие эти учетные данные", @@ -166,7 +164,8 @@ "generateKeyPairNote": "Сгенерировать новую пару SSH-ключей напрямую. Это заменит любые существующие ключи в форме.", "invalidKey": "Неверный ключ", "detectionError": "Ошибка определения", - "unknown": "Неизвестно" + "unknown": "Неизвестно", + "credentialId": "Учётные данные ID" }, "dragIndicator": { "error": "Ошибка: {{error}}", @@ -261,7 +260,11 @@ "saveError": "Ошибка сохранения конфигурации", "saving": "Сохранение...", "saveConfig": "Сохранить конфигурацию", - "helpText": "Введите URL, где работает ваш сервер Termix (например, http://localhost:30001 или https://your-server.com)" + "helpText": "Введите URL, где работает ваш сервер Termix (например, http://localhost:30001 или https://your-server.com)", + "changeServer": "Сменить сервер", + "mustIncludeProtocol": "URL сервера должен начинаться с http:// или https://", + "notValidatedWarning": "URL не проверен - убедитесь, что он правильный", + "warning": "Предупреждение" }, "versionCheck": { "error": "Ошибка проверки версии", @@ -294,7 +297,7 @@ "warning": "Предупреждение", "info": "Информация", "success": "Успех", - "loading": "Загрузка", + "loading": "Загрузка...", "required": "Обязательно", "optional": "Опционально", "clear": "Очистить", @@ -308,7 +311,6 @@ "updateAvailable": "Доступно обновление", "sshPath": "SSH-путь", "localPath": "Локальный путь", - "loading": "Загрузка...", "noAuthCredentials": "Нет учетных данных аутентификации для этого SSH-хоста", "noReleases": "Нет выпусков", "updatesAndReleases": "Обновления и выпуски", @@ -323,17 +325,13 @@ "resetPassword": "Сбросить пароль", "resetCode": "Код сброса", "newPassword": "Новый пароль", - "sshPath": "SSH-путь", - "localPath": "Локальный путь", "folder": "Папка", "file": "Файл", "renamedSuccessfully": "успешно переименован", "deletedSuccessfully": "успешно удален", - "noAuthCredentials": "Нет учетных данных аутентификации для этого SSH-хоста", "noTunnelConnections": "Нет настроенных туннельных подключений", "sshTools": "SSH-инструменты", "english": "Английский", - "russia": "Русский", "chinese": "Китайский", "german": "Немецкий", "cancel": "Отмена", @@ -342,36 +340,27 @@ "login": "Войти", "logout": "Выйти", "register": "Зарегистрироваться", - "username": "Имя пользователя", "password": "Пароль", "version": "Версия", "confirmPassword": "Подтвердите пароль", "back": "Назад", "email": "Email", "submit": "Отправить", - "cancel": "Отмена", "change": "Изменить", "save": "Сохранить", "delete": "Удалить", "edit": "Редактировать", "add": "Добавить", "search": "Поиск", - "loading": "Загрузка...", - "error": "Ошибка", - "success": "Успех", - "warning": "Предупреждение", - "info": "Информация", "confirm": "Подтвердить", "yes": "Да", "no": "Нет", "ok": "OK", - "close": "Закрыть", "enabled": "Включено", "disabled": "Отключено", "important": "Важно", "notEnabled": "Не включено", "settingUp": "Настройка...", - "back": "Назад", "next": "Далее", "previous": "Назад", "refresh": "Обновить", @@ -395,7 +384,10 @@ "documentation": "Документация", "retry": "Повторить", "checking": "Проверка...", - "checkingDatabase": "Проверка подключения к базе данных..." + "checkingDatabase": "Проверка подключения к базе данных...", + "connect": "Подключить", + "connecting": "Подключение...", + "saving": "Сохранение..." }, "nav": { "home": "Главная", @@ -424,7 +416,7 @@ "userManagement": "Управление пользователями", "makeAdmin": "Сделать администратором", "removeAdmin": "Убрать администратора", - "deleteUser": "Удалить пользователя", + "deleteUser": "Удалить пользователя {{username}}? Это нельзя отменить.", "allowRegistration": "Разрешить регистрацию", "oidcSettings": "Настройки OIDC", "clientId": "Client ID", @@ -478,10 +470,9 @@ "removeAdminStatus": "Убрать статус администратора у {{username}}?", "adminStatusRemoved": "Статус администратора убран у {{username}}", "failedToRemoveAdminStatus": "Не удалось убрать статус администратора", - "deleteUser": "Удалить пользователя {{username}}? Это нельзя отменить.", "userDeletedSuccessfully": "Пользователь {{username}} успешно удален", "failedToDeleteUser": "Не удалось удалить пользователя", - "overrideUserInfoUrl": "Переопределить User Info URL (не требуется)", + "overrideUserInfoUrl": "Переопределить Пользователь Info URL (не требуется)", "databaseSecurity": "Безопасность базы данных", "encryptionStatus": "Статус шифрования", "encryptionEnabled": "Шифрование включено", @@ -531,7 +522,6 @@ "verificationCompleted": "Проверка совместимости завершена - данные не изменялись", "verificationInProgress": "Проверка завершена", "dataMigrationCompleted": "Миграция данных успешно завершена!", - "migrationCompleted": "Миграция завершена", "verificationFailed": "Проверка совместимости не удалась", "migrationFailed": "Миграция не удалась", "runningVerification": "Выполняется проверка совместимости...", @@ -609,7 +599,33 @@ "requiresPasswordLogin": "Требуется включенный вход по паролю", "passwordLoginDisabledWarning": "Вход по паролю отключен. Убедитесь, что OIDC правильно настроен, иначе вы не сможете войти в Termix.", "oidcRequiredWarning": "КРИТИЧЕСКИ: Вход по паролю отключен. Если вы сбросите или неправильно настроите OIDC, вы потеряете весь доступ к Termix и заблокируете свой экземпляр. Продолжайте только если вы абсолютно уверены.", - "confirmDisableOIDCWarning": "ПРЕДУПРЕЖДЕНИЕ: Вы собираетесь отключить OIDC, пока вход по паролю также отключен. Это заблокирует ваш экземпляр Termix, и вы потеряете весь доступ. Вы абсолютно уверены, что хотите продолжить?" + "confirmDisableOIDCWarning": "ПРЕДУПРЕЖДЕНИЕ: Вы собираетесь отключить OIDC, пока вход по паролю также отключен. Это заблокирует ваш экземпляр Termix, и вы потеряете весь доступ. Вы абсолютно уверены, что хотите продолжить?", + "accountsLinkedSuccessfully": "OIDC пользователь {{oidcUsername}} связан с {{targetUsername}}", + "confirmRevokeAllSessions": "Вы уверены, что хотите отозвать все сессии для этого пользователя?", + "confirmRevokeSession": "Вы уверены, что хотите отозвать эту сессию?", + "failedToFetchSessions": "Не удалось загрузить сессии", + "failedToLinkAccounts": "Не удалось связать аккаунты", + "failedToRevokeSession": "Не удалось отозвать сессию", + "failedToRevokeSessions": "Не удалось отозвать сессии", + "failedToUnlinkOIDC": "Не удалось отвязать OIDC", + "linkAccountsButton": "Связать аккаунты", + "linkOIDCActionAddCapability": "Добавить возможность входа через OIDC к целевому аккаунту с паролем", + "linkOIDCActionDeleteUser": "Удалить аккаунт пользователя OIDC и все его данные", + "linkOIDCActionDualAuth": "Разрешить аккаунту с паролем вход как по паролю, так и через OIDC", + "linkOIDCDialogDescription": "Связать {{username}} (пользователь OIDC) с существующим аккаунтом с паролем. Это включит двойную аутентификацию для аккаунта с паролем.", + "linkOIDCDialogTitle": "Связать аккаунт OIDC с аккаунтом с паролем", + "linkOIDCWarningTitle": "Предупреждение: Данные пользователя OIDC будут удалены", + "linkTargetUsernameLabel": "Имя пользователя целевого аккаунта с паролем", + "linkTargetUsernamePlaceholder": "Введите имя пользователя аккаунта с паролем", + "linkTargetUsernameRequired": "Целевое имя пользователя обязательно", + "linkToPasswordAccount": "Связать с аккаунтом с паролем", + "linkingAccounts": "Связывание...", + "sessionRevokedSuccessfully": "Сессия успешно отозвана", + "sessionsRevokedSuccessfully": "Сессии успешно отозваны", + "unlinkOIDCDescription": "Удалить аутентификацию OIDC для {{username}}? После этого пользователь сможет войти только с помощью имени пользователя/пароля.", + "unlinkOIDCSuccess": "OIDC отвязан от {{username}}", + "unlinkOIDCTitle": "Отвязать аутентификацию OIDC", + "failedToUpdatePasswordLoginStatus": "Не удалось обновить статус входа по паролю" }, "hosts": { "title": "Менеджер хостов", @@ -834,11 +850,11 @@ "minimumContrastRatioDesc": "Автоматически настраивать цвета для лучшей читаемости", "sshAgentForwarding": "Переадресация SSH-агента", "sshAgentForwardingDesc": "Переадресовать агент SSH-аутентификации на удаленный хост", - "backspaceMode": "Режим Backspace", - "selectBackspaceMode": "Выбрать режим Backspace", + "backspaceMode": "Режим Назадspace", + "selectBackspaceMode": "Выбрать режим Назадspace", "backspaceModeNormal": "Обычный (DEL)", "backspaceModeControlH": "Control-H (^H)", - "backspaceModeDesc": "Поведение клавиши Backspace для совместимости", + "backspaceModeDesc": "Поведение клавиши Назадspace для совместимости", "startupSnippet": "Сниппет запуска", "selectSnippet": "Выбрать сниппет", "searchSnippets": "Поиск сниппетов...", @@ -900,6 +916,25 @@ "proxyNode": "Узел прокси", "proxyType": "Тип прокси", "advancedAuthSettings": "Расширенные настройки аутентификации" + "advancedAuthSettings": "Расширенные настройки аутентификации", + "addQuickAction": "Добавить Quick Action", + "allHostsInFolderDeleted": "{{count}} хостов успешно удалены из папки \"{{folder}}\"", + "confirmDeleteAllHostsInFolder": "Вы уверены, что хотите удалить все {{count}} хостов в папке \"{{folder}}\"? Это действие нельзя отменить.", + "deleteAllHostsInFolder": "Удалить все хосты в папке", + "editFolderAppearance": "Редактировать вид папки", + "editFolderAppearanceDesc": "Настроить цвет и иконку для папки", + "failedToDeleteHostsInFolder": "Не удалось удалить хосты в папке", + "failedToUpdateFolderAppearance": "Не удалось обновить вид папки", + "folderAppearanceUpdated": "Вид папки успешно обновлен", + "folderColor": "Цвет папки", + "folderIcon": "Иконка папки", + "noSnippetFound": "Сниппет не найден", + "preview": "Предпросмотр", + "quickActionName": "Название действия", + "quickActions": "Быстрые действия", + "quickActionsDescription": "Быстрые действия позволяют создавать пользовательские кнопки, выполняющие SSH-сниппеты на этом сервере. Эти кнопки появятся в верхней части страницы статистики сервера для быстрого доступа.", + "quickActionsList": "Список быстрых действий", + "quickActionsOrder": "Кнопки быстрых действий появятся в указанном выше порядке на странице статистики сервера" }, "terminal": { "title": "Терминал", @@ -937,7 +972,11 @@ "totpRequired": "Требуется двухфакторная аутентификация", "totpCodeLabel": "Код проверки", "totpPlaceholder": "000000", - "totpVerify": "Проверить" + "totpVerify": "Проверить", + "sudoPasswordPopupTitle": "Вставить пароль?", + "sudoPasswordPopupHint": "Нажмите Enter для вставки, Esc для отмены", + "sudoPasswordPopupConfirm": "Вставить", + "sudoPasswordPopupDismiss": "Отмена" }, "fileManager": { "title": "Файловый менеджер", @@ -1023,7 +1062,6 @@ "copyPaths": "Копировать пути", "delete": "Удалить", "properties": "Свойства", - "preview": "Просмотр", "refresh": "Обновить", "downloadFiles": "Скачать {{count}} файлов в браузер", "copyFiles": "Копировать {{count}} элементов", @@ -1038,18 +1076,11 @@ "failedToDeleteItem": "Не удалось удалить элемент", "itemRenamedSuccessfully": "{{type}} успешно переименован", "failedToRenameItem": "Не удалось переименовать элемент", - "upload": "Загрузить", "download": "Скачать", - "newFile": "Новый файл", - "newFolder": "Новая папка", - "rename": "Переименовать", - "delete": "Удалить", "permissions": "Права доступа", "size": "Размер", "modified": "Изменен", "path": "Путь", - "fileName": "Имя файла", - "folderName": "Имя папки", "confirmDelete": "Вы уверены, что хотите удалить {{name}}?", "uploadSuccess": "Файл успешно загружен", "uploadFailed": "Не удалось загрузить файл", @@ -1069,10 +1100,7 @@ "fileSavedSuccessfully": "Файл успешно сохранен", "saveTimeout": "Операция сохранения превысила время ожидания. Файл мог быть успешно сохранен, но операция заняла слишком много времени для завершения. Проверьте логи Docker для подтверждения.", "failedToSaveFile": "Не удалось сохранить файл", - "folder": "Папка", - "file": "Файл", "deletedSuccessfully": "успешно удален", - "failedToDeleteItem": "Не удалось удалить элемент", "connectToServer": "Подключиться к серверу", "selectServerToEdit": "Выберите сервер на боковой панели, чтобы начать редактирование файлов", "fileOperations": "Файловые операции", @@ -1129,10 +1157,8 @@ "unpinFile": "Открепить файл", "removeShortcut": "Удалить ярлык", "saveFilesToSystem": "Сохранить {{count}} файлов как...", - "saveToSystem": "Сохранить как...", "pinFile": "Закрепить файл", "addToShortcuts": "Добавить в ярлыки", - "selectLocationToSave": "Выберите место для сохранения", "downloadToDefaultLocation": "Скачать в место по умолчанию", "pasteFailed": "Вставка не удалась", "noUndoableActions": "Нет действий для отмены", @@ -1150,7 +1176,6 @@ "editPath": "Редактировать путь", "confirm": "Подтвердить", "cancel": "Отмена", - "folderName": "Имя папки", "find": "Найти...", "replaceWith": "Заменить на...", "replace": "Заменить", @@ -1176,23 +1201,18 @@ "outdent": "Уменьшить отступ", "autoComplete": "Автозавершение", "imageLoadError": "Не удалось загрузить изображение", - "zoomIn": "Увеличить", - "zoomOut": "Уменьшить", "rotate": "Повернуть", "originalSize": "Оригинальный размер", "startTyping": "Начните печатать...", "unknownSize": "Неизвестный размер", "fileIsEmpty": "Файл пуст", - "modified": "Изменен", "largeFileWarning": "Предупреждение о большом файле", "largeFileWarningDesc": "Этот файл имеет размер {{size}}, что может вызвать проблемы с производительностью при открытии как текста.", "fileNotFoundAndRemoved": "Файл \"{{name}}\" не найден и был удален из недавних/закрепленных файлов", "failedToLoadFile": "Не удалось загрузить файл: {{error}}", "serverErrorOccurred": "Произошла ошибка сервера. Пожалуйста, попробуйте позже.", - "fileSavedSuccessfully": "Файл успешно сохранен", "autoSaveFailed": "Автосохранение не удалось", "fileAutoSaved": "Файл автосохранен", - "fileDownloadedSuccessfully": "Файл успешно скачан", "moveFileFailed": "Не удалось переместить {{name}}", "moveOperationFailed": "Операция перемещения не удалась", "canOnlyCompareFiles": "Можно сравнивать только два файла", @@ -1228,12 +1248,40 @@ "sshConnectionFailed": "SSH-подключение не удалось. Пожалуйста, проверьте ваше подключение к {{name}} ({{ip}}:{{port}})", "loadFileFailed": "Не удалось загрузить файл: {{error}}", "connectedSuccessfully": "Успешно подключено", - "totpVerificationFailed": "Проверка TOTP не удалась" + "totpVerificationFailed": "Проверка TOTP не удалась", + "andMoreFiles": "and {{count}} more...", + "archiveExtractedSuccessfully": "{{name}} успешно извлечен", + "archiveName": "Имя архива", + "changePermissions": "Изменить права", + "changePermissionsDesc": "Изменить права файла для", + "compress": "Сжать", + "compressFailed": "Сжатие не удалось", + "compressFile": "Сжать файл", + "compressFiles": "Сжать файлы", + "compressFilesDesc": "Сжать {{count}} элементов в архив", + "compressingFiles": "Сжатие {{count}} элементов в {{name}}...", + "compressionFormat": "Формат сжатия", + "currentPermissions": "Текущие права", + "enterArchiveName": "Введите имя архива...", + "execute": "Выполнить", + "extractArchive": "Извлечь архив", + "extractFailed": "Извлечение не удалось", + "extractingArchive": "Извлечение {{name}}...", + "failedToChangePermissions": "Не удалось изменить права", + "filesCompressedSuccessfully": "{{name}} успешно создан", + "group": "Группа", + "newPermissions": "Новые права", + "others": "Другие", + "owner": "Владелец", + "permissionsChangedSuccessfully": "Права успешно изменены", + "read": "Чтение", + "selectedFiles": "Выбранные файлы", + "write": "Запись" }, "tunnels": { "title": "SSH-туннели", "noSshTunnels": "Нет SSH-туннелей", - "createFirstTunnelMessage": "Вы еще не создали SSH-туннели. Настройте туннельные подключения в Менеджере хостов, чтобы начать.", + "createFirstTunnelMessage": "Создайте ваш первый SSH-туннель, чтобы начать. Используйте SSH-менеджер для добавления хостов с туннельными подключениями.", "connected": "Подключено", "disconnected": "Отключено", "connecting": "Подключение...", @@ -1274,17 +1322,8 @@ "local": "Локальный", "remote": "Удаленный", "dynamic": "Динамический", - "noSshTunnels": "Нет SSH-туннелей", - "createFirstTunnelMessage": "Создайте ваш первый SSH-туннель, чтобы начать. Используйте SSH-менеджер для добавления хостов с туннельными подключениями.", "unknownConnectionStatus": "Неизвестно", - "connected": "Подключено", - "connecting": "Подключение...", - "disconnecting": "Отключение...", - "disconnected": "Отключено", "portMapping": "Порт {{sourcePort}} → {{endpointHost}}:{{endpointPort}}", - "disconnect": "Отключить", - "connect": "Подключить", - "canceling": "Отмена...", "endpointHostNotFound": "Хост конечной точки не найден", "discord": "Discord", "githubIssue": "Проблема на GitHub", @@ -1297,7 +1336,7 @@ "disk": "Диск", "network": "Сеть", "uptime": "Время работы", - "loadAverage": "Средняя загрузка", + "loadAverage": "Средняя: {{avg1}}, {{avg5}}, {{avg15}}", "processes": "Процессы", "connections": "Подключения", "usage": "Использование", @@ -1313,7 +1352,6 @@ "cpuCores_one": "{{count}} CPU", "cpuCores_other": "{{count}} CPU", "naCpus": "N/A CPU", - "loadAverage": "Средняя: {{avg1}}, {{avg5}}, {{avg15}}", "loadAverageNA": "Средняя: N/A", "cpuUsage": "Использование CPU", "memoryUsage": "Использование памяти", @@ -1332,8 +1370,6 @@ "totpRequired": "Требуется TOTP-аутентификация", "totpUnavailable": "Статистика сервера недоступна для серверов с включенным TOTP", "load": "Загрузка", - "free": "Свободно", - "available": "Доступно", "editLayout": "Редактировать макет", "cancelEdit": "Отмена", "addWidget": "Добавить виджет", @@ -1358,7 +1394,13 @@ "recentSuccessfulLogins": "Последние успешные входы", "recentFailedAttempts": "Последние неудачные попытки", "noRecentLoginData": "Нет данных о недавних входах", - "from": "с" + "from": "с", + "executeQuickAction": "Выполнить {{name}}", + "executingQuickAction": "Выполнение {{name}}...", + "quickActionError": "Не удалось выполнить {{name}}", + "quickActionFailed": "{{name}} завершилось ошибкой", + "quickActionSuccess": "{{name}} завершено успешно", + "quickActions": "Быстрые действия" }, "auth": { "tagline": "SSH ТЕРМИНАЛ МЕНЕДЖЕР", @@ -1448,7 +1490,27 @@ "signUp": "Зарегистрироваться", "dataLossWarning": "Сброс пароля этим способом удалит все ваши сохраненные SSH-хосты, учетные данные и другие зашифрованные данные. Это действие нельзя отменить. Используйте это только если вы забыли пароль и не вошли в систему.", "authenticationDisabled": "Аутентификация отключена", - "authenticationDisabledDesc": "Все методы аутентификации в настоящее время отключены. Пожалуйста, свяжитесь с вашим администратором." + "authenticationDisabledDesc": "Все методы аутентификации в настоящее время отключены. Пожалуйста, свяжитесь с вашим администратором.", + "authenticating": "Аутентификация...", + "desktopApp": "Настольное приложение", + "loadingServer": "Загрузка сервера...", + "loggingInToDesktopApp": "Вход в настольное приложение", + "loggingInToDesktopAppViaWeb": "Вход в настольное приложение через веб-интерфейс", + "loggingInToMobileApp": "Вход в мобильное приложение", + "mobileApp": "Мобильное приложение", + "redirectingToApp": "Перенаправление в приложение...", + "sshAuthFailedDescription": "Предоставленные учетные данные неверны. Пожалуйста, попробуйте снова с правильными учетными данными.", + "sshAuthenticationFailed": "Аутентификация не удалась", + "sshAuthenticationRequired": "Требуется SSH-аутентификация", + "sshAuthenticationTimeout": "Тайм-аут аутентификации", + "sshKeyPasswordDescription": "Если ваш SSH-ключ зашифрован, введите парольную фразу здесь.", + "sshNoKeyboardInteractive": "Клавиатурная интерактивная аутентификация недоступна", + "sshNoKeyboardInteractiveDescription": "Сервер не поддерживает клавиатурную интерактивную аутентификацию. Пожалуйста, укажите ваш пароль или SSH-ключ.", + "sshPasswordDescription": "Введите пароль для этого SSH-подключения.", + "sshProvideCredentialsDescription": "Пожалуйста, предоставьте ваши SSH-учетные данные для подключения к этому серверу.", + "sshTimeoutDescription": "Попытка аутентификации истекла по времени. Пожалуйста, попробуйте снова.", + "passwordResetSuccess": "Сброс пароля прошел успешно", + "passwordResetSuccessDesc": "Ваш пароль был успешно сброшен. Теперь вы можете войти с новым паролем." }, "errors": { "notFound": "Страница не найдена", @@ -1525,9 +1587,12 @@ "fileColorCodingDesc": "Цветовая кодировка файлов по типу: папки (красный), файлы (синий), символические ссылки (зелёный)", "commandAutocomplete": "Автодополнение команд", "commandAutocompleteDesc": "Включить автодополнение команд терминала клавишей Tab на основе вашей истории команд", + "defaultSnippetFoldersCollapsed": "Сворачивать папки сниппетов по умолчанию", + "defaultSnippetFoldersCollapsedDesc": "Если включено, все папки сниппетов будут свёрнуты при открытии вкладки сниппетов", "currentPassword": "Текущий пароль", "passwordChangedSuccess": "Пароль успешно изменен! Пожалуйста, войдите снова.", - "failedToChangePassword": "Не удалось изменить пароль. Пожалуйста, проверьте ваш текущий пароль и попробуйте снова." + "failedToChangePassword": "Не удалось изменить пароль. Пожалуйста, проверьте ваш текущий пароль и попробуйте снова.", + "externalAndLocal": "Двойная аутентификация" }, "user": { "failedToLoadVersionInfo": "Не удалось загрузить информацию о версии" @@ -1595,7 +1660,8 @@ "lastAdminWarning": "Вы последний пользователь-администратор. Вы не можете удалить свою учетную запись, так как это оставит систему без администраторов. Пожалуйста, сначала сделайте другого пользователя администратором или свяжитесь с поддержкой системы.", "confirmPassword": "Подтвердите пароль", "deleting": "Удаление...", - "cancel": "Отмена" + "cancel": "Отмена", + "deleteAccountWarningShort": "Это действие необратимо и приведет к окончательному удалению вашей учетной записи." }, "interface": { "sidebar": "Боковая панель", @@ -1615,7 +1681,6 @@ "deleteItem": "Удалить элемент", "createNewFile": "Создать новый файл", "createNewFolder": "Создать новую папку", - "deleteItem": "Удалить элемент", "renameItem": "Переименовать элемент", "clickToSelectFile": "Нажмите для выбора файла", "noSshHosts": "Нет SSH-хостов", @@ -1768,4 +1833,4 @@ "close": "Закрыть", "hostManager": "Менеджер хостов" } -} +} \ No newline at end of file diff --git a/src/locales/zh/translation.json b/src/locales/zh/translation.json index 3b15347d..7544dd64 100644 --- a/src/locales/zh/translation.json +++ b/src/locales/zh/translation.json @@ -1,7 +1,6 @@ { "credentials": { "credentialsViewer": "凭证查看器", - "credentialsManager": "凭据管理器", "manageYourSSHCredentials": "安全管理您的SSH凭据", "addCredential": "添加凭据", "createCredential": "创建凭据", @@ -164,7 +163,9 @@ "failedToGenerateKeyPair": "生成密钥对失败", "generateKeyPairNote": "直接生成新的SSH密钥对。这将替换表单中的现有密钥。", "invalidKey": "无效密钥", - "detectionError": "检测错误" + "detectionError": "检测错误", + "credentialId": "凭据 ID", + "unknown": "未知" }, "dragIndicator": { "error": "错误:{{error}}", @@ -259,7 +260,11 @@ "saveError": "保存配置时出错", "saving": "保存中...", "saveConfig": "保存配置", - "helpText": "输入您的 Termix 服务器运行地址(例如:http://localhost:30001 或 https://your-server.com)" + "helpText": "输入您的 Termix 服务器运行地址(例如:http://localhost:30001 或 https://your-server.com)", + "changeServer": "更换服务器", + "mustIncludeProtocol": "服务器URL必须以 http:// 或 https:// 开头", + "notValidatedWarning": "URL 未经验证 - 请确保其正确", + "warning": "警告" }, "versionCheck": { "error": "版本检查错误", @@ -335,13 +340,11 @@ "login": "登录", "logout": "登出", "register": "注册", - "username": "用户名", "password": "密码", "confirmPassword": "确认密码", "back": "返回", "email": "邮箱", "submit": "提交", - "cancel": "取消", "change": "更改", "save": "保存", "delete": "删除", @@ -382,7 +385,13 @@ "documentation": "文档", "retry": "重试", "checking": "检查中...", - "checkingDatabase": "正在检查数据库连接..." + "checkingDatabase": "正在检查数据库连接...", + "actions": "操作", + "remove": "移除", + "revoke": "撤销", + "create": "创建", + "saving": "保存中...", + "version": "Version" }, "nav": { "home": "首页", @@ -511,7 +520,7 @@ "loadingEncryptionStatus": "正在加载加密状态...", "testMigrationDescription": "验证现有数据是否可以安全地迁移到加密格式,不会实际修改任何数据", "serverMigrationGuide": "服务器迁移指南", - "migrationInstructions": "要将加密数据迁移到新服务器:1) 备份数据库文件,2) 在新服务器设置环境变量 DB_ENCRYPTION_KEY=\"你的密钥\",3) 恢复数据库文件", + "migrationInstructions": "要将加密数据迁移到新服务器:1) 备份数据库文件,2) 在新服务器设置环境变量 DB_ENCRYPTION_KEY=\"你的key\",3) 恢复数据库文件", "environmentProtection": "环境保护", "environmentProtectionDesc": "基于服务器环境信息(主机名、路径等)保护加密密钥,可通过环境变量实现迁移", "verificationCompleted": "兼容性验证完成 - 未修改任何数据", @@ -595,7 +604,32 @@ "passwordLoginDisabledWarning": "密码登录已禁用。请确保 OIDC 已正确配置,否则您将无法登录 Termix。", "oidcRequiredWarning": "严重警告:密码登录已禁用。如果您重置或错误配置 OIDC,您将失去对 Termix 的所有访问权限并使您的实例无法使用。只有在您完全确定的情况下才能继续。", "confirmDisableOIDCWarning": "警告:您即将在密码登录也已禁用的情况下禁用 OIDC。这将使您的 Termix 实例无法使用,您将失去所有访问权限。您确定要继续吗?", - "failedToUpdatePasswordLoginStatus": "更新密码登录状态失败" + "failedToUpdatePasswordLoginStatus": "更新密码登录状态失败", + "accountsLinkedSuccessfully": "OIDC 用户 {{oidcUsername}} 已关联到 {{targetUsername}}", + "confirmRevokeAllSessions": "您确定要撤销此用户的所有会话吗?", + "confirmRevokeSession": "您确定要撤销此会话吗?", + "failedToFetchSessions": "获取会话失败", + "failedToLinkAccounts": "关联账户失败", + "failedToRevokeSession": "撤销会话失败", + "failedToRevokeSessions": "撤销会话失败", + "failedToUnlinkOIDC": "取消 OIDC 关联失败", + "linkAccountsButton": "关联账户", + "linkOIDCActionAddCapability": "将 OIDC 登录功能添加到目标密码账户", + "linkOIDCActionDeleteUser": "删除 OIDC 用户账户及其所有数据", + "linkOIDCActionDualAuth": "允许密码账户同时使用密码和 OIDC 登录", + "linkOIDCDialogDescription": "将 {{username}} (OIDC 用户) 关联到现有的密码账户。这将为密码账户启用双重认证。", + "linkOIDCDialogTitle": "将 OIDC 账户关联到密码账户", + "linkOIDCWarningTitle": "警告: OIDC 用户数据将被删除", + "linkTargetUsernameLabel": "目标密码账户用户名", + "linkTargetUsernamePlaceholder": "输入密码账户的用户名", + "linkTargetUsernameRequired": "目标用户名是必需的", + "linkToPasswordAccount": "关联到密码账户", + "linkingAccounts": "关联中...", + "sessionRevokedSuccessfully": "会话撤销成功", + "sessionsRevokedSuccessfully": "会话撤销成功", + "unlinkOIDCDescription": "移除 {{username}} 的 OIDC 认证?此操作后用户只能使用用户名/密码登录。", + "unlinkOIDCSuccess": "已取消 {{username}} 的 OIDC 关联", + "unlinkOIDCTitle": "取消 OIDC 认证关联" }, "hosts": { "title": "主机管理", @@ -632,7 +666,6 @@ "port": "端口", "name": "名称", "username": "用户名", - "hostName": "主机名", "folder": "文件夹", "tags": "标签", "passwordRequired": "使用密码认证时需要密码", @@ -642,10 +675,6 @@ "addHost": "添加主机", "editHost": "编辑主机", "cloneHost": "克隆主机", - "deleteHost": "删除主机", - "authType": "认证类型", - "passwordAuth": "密码", - "keyAuth": "SSH 密钥", "keyPassword": "密钥密码", "keyType": "密钥类型", "pin": "固定", @@ -653,15 +682,6 @@ "enableTunnel": "启用隧道", "enableFileManager": "启用文件管理器", "defaultPath": "默认路径", - "testConnection": "测试连接", - "connect": "连接", - "disconnect": "断开连接", - "connected": "已连接", - "disconnected": "已断开", - "connecting": "连接中...", - "connectionFailed": "连接失败", - "connectionSuccess": "连接成功", - "addTags": "添加标签(空格添加)", "sourcePort": "源端口", "sourcePortDesc": "(源指通用标签页中的当前连接详情)", "endpointPort": "目标端口", @@ -671,20 +691,7 @@ "remove": "移除", "addConnection": "添加连接", "sshpassRequired": "密码认证需要安装 Sshpass", - "sshpassInstallCommand": "安装命令:sudo apt install sshpass", - "sshServerConfig": "需要配置 SSH 服务器", - "sshServerConfigInstructions": "运行以下命令以允许密码认证:", - "sshConfigCommand1": "sudo sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config", - "sshConfigCommand2": "sudo systemctl restart sshd", - "localPortForwarding": "本地端口转发", - "localPortForwardingDesc": "通过 SSH 连接将本地端口转发到远程服务器", - "remotePortForwarding": "远程端口转发", - "remotePortForwardingDesc": "通过 SSH 连接将远程端口转发到本地服务器", - "dynamicPortForwarding": "动态端口转发(SOCKS 代理)", - "dynamicPortForwardingDesc": "在本地计算机上创建 SOCKS 代理,通过 SSH 连接路由流量", - "bindAddress": "绑定地址", "hostViewer": "主机查看器", - "configuration": "配置", "maxRetries": "最大重试次数", "tunnelConnections": "隧道连接", "enableTerminalDesc": "启用/禁用在终端选项卡中显示此主机", @@ -693,8 +700,6 @@ "autoStartDesc": "容器启动时自动启动此隧道", "defaultPathDesc": "打开此主机文件管理器时的默认目录", "tunnelForwardDescription": "此隧道将从源计算机(常规选项卡中的当前连接详情)的端口 {{sourcePort}} 转发流量到端点计算机的端口 {{endpointPort}}。", - "endpointSshConfiguration": "端点 SSH 配置", - "sourcePortDescription": "(源指的是常规选项卡中的当前连接详情)", "autoStartContainer": "容器启动时自动启动", "upload": "上传", "authentication": "认证方式", @@ -715,20 +720,12 @@ "centosRhelFedora": "CentOS/RHEL/Fedora", "macos": "macOS", "windows": "Windows", - "sshpassOSInstructions": { - "centos": "CentOS/RHEL/Fedora: sudo yum install sshpass 或 sudo dnf install sshpass", - "macos": "macOS: brew install hudochenkov/sshpass/sshpass", - "windows": "Windows: 使用 WSL 或考虑使用 SSH 密钥认证" - }, + "sshpassOSInstructions": {}, "sshServerConfigRequired": "SSH 服务器配置要求", "sshServerConfigDesc": "对于隧道连接,SSH 服务器必须配置允许端口转发:", "gatewayPortsYes": "绑定远程端口到所有接口", "allowTcpForwardingYes": "启用端口转发", "permitRootLoginYes": "如果使用 root 用户进行隧道连接", - "sshServerConfigReverse": "对于反向 SSH 隧道,端点 SSH 服务器必须允许:", - "gatewayPorts": "GatewayPorts yes(绑定远程端口)", - "allowTcpForwarding": "AllowTcpForwarding yes(端口转发)", - "permitRootLogin": "PermitRootLogin yes(如果使用 root)", "editSshConfig": "编辑 /etc/ssh/sshd_config 并重启 SSH: sudo systemctl restart sshd", "updateHost": "更新主机", "hostUpdatedSuccessfully": "主机 \"{{name}}\" 更新成功!", @@ -758,7 +755,6 @@ "tunnel": "隧道", "fileManager": "文件管理器", "serverStats": "服务器统计", - "hostViewer": "主机查看器", "enableServerStats": "启用服务器统计", "enableServerStatsDesc": "启用/禁用此主机的服务器统计信息收集", "displayItems": "显示项目", @@ -893,13 +889,21 @@ "searchServers": "搜索服务器...", "noServerFound": "未找到服务器", "jumpHostsOrder": "连接将按顺序进行:跳板主机 1 → 跳板主机 2 → ... → 目标服务器", - "advancedAuthSettings": "高级身份验证设置" + "advancedAuthSettings": "高级身份验证设置", + "addQuickAction": "添加 Quick Action", + "noSnippetFound": "没有 snippet found", + "quickActionName": "Action 名称", + "quickActions": "Quick Actions", + "quickActionsDescription": "Quick actions allow you to 创建 custom buttons that execute SSH snippets on this server. These buttons will appear at the top of the Server Stats page for quick access.", + "quickActionsList": "Quick Actions List", + "quickActionsOrder": "Quick action buttons will appear in the order listed above on the Server Stats page", + "sshpassRequiredDesc": "For 密码 认证 in tunnels, sshpass must be installed on the system." }, "terminal": { "title": "终端", "terminalTitle": "终端 - {{host}}", "terminalWithPath": "终端 - {{host}}:{{path}}", - "runTitle": "运行 {{command}} - {{host}}", + "runTitle": "运行 {{command}} - {{name}}", "totpRequired": "需要双因素认证", "totpCodeLabel": "验证码", "totpPlaceholder": "000000", @@ -931,7 +935,11 @@ "reconnecting": "重新连接中... ({{attempt}}/{{max}})", "reconnected": "重新连接成功", "maxReconnectAttemptsReached": "已达到最大重连尝试次数", - "connectionTimeout": "连接超时" + "connectionTimeout": "连接超时", + "sudoPasswordPopupTitle": "插入密码?", + "sudoPasswordPopupHint": "按 Enter 插入,Esc 取消", + "sudoPasswordPopupConfirm": "插入", + "sudoPasswordPopupDismiss": "取消" }, "fileManager": { "title": "文件管理器", @@ -1222,7 +1230,16 @@ "write": "写入", "execute": "执行", "permissionsChangedSuccessfully": "权限修改成功", - "failedToChangePermissions": "权限修改失败" + "failedToChangePermissions": "权限修改失败", + "autoSaveFailed": "自动保存失败", + "delete": "删除", + "download": "下载", + "fileAutoSaved": "文件已自动保存", + "fileDownloadedSuccessfully": "文件 \"{{name}}\" 下载成功", + "fileSavedSuccessfully": "文件保存成功", + "path": "Path", + "permissions": "Permissions", + "size": "Size" }, "tunnels": { "title": "SSH 隧道", @@ -1272,7 +1289,8 @@ "endpointHostNotFound": "未找到端点主机", "discord": "Discord", "githubIssue": "GitHub 问题", - "forHelp": "寻求帮助" + "forHelp": "寻求帮助", + "unknownConnectionStatus": "Unk没有wn" }, "serverStats": { "title": "服务器统计", @@ -1314,8 +1332,6 @@ "totpRequired": "需要 TOTP 认证", "totpUnavailable": "启用了 TOTP 的服务器无法使用服务器统计功能", "load": "负载", - "free": "空闲", - "available": "可用", "editLayout": "编辑布局", "cancelEdit": "取消", "addWidget": "添加小组件", @@ -1340,7 +1356,14 @@ "recentSuccessfulLogins": "最近成功登录", "recentFailedAttempts": "最近失败尝试", "noRecentLoginData": "无最近登录数据", - "from": "来自" + "from": "来自", + "executeQuickAction": "执行 {{name}}", + "executingQuickAction": "执行中 {{name}}...", + "failedToFetchHomeData": "获取主页数据失败", + "quickActionError": "无法执行 {{name}}", + "quickActionFailed": "{{name}} 失败", + "quickActionSuccess": "{{name}} 完成成功", + "quickActions": "Quick Actions" }, "auth": { "tagline": "SSH 终端管理器", @@ -1440,7 +1463,17 @@ "sshTimeoutDescription": "身份验证尝试超时。请重试。", "sshProvideCredentialsDescription": "请提供您的 SSH 凭据以连接到此服务器。", "sshPasswordDescription": "输入此 SSH 连接的密码。", - "sshKeyPasswordDescription": "如果您的 SSH 密钥已加密,请在此处输入密码。" + "sshKeyPasswordDescription": "如果您的 SSH 密钥已加密,请在此处输入密码。", + "authenticating": "Authenticating...", + "authenticationDisabled": "认证已禁用", + "authenticationDisabledDesc": "所有认证方式当前已禁用。请联系您的管理员。", + "desktopApp": "桌面应用", + "loadingServer": "加载服务器中...", + "loggingInToDesktopApp": "登录桌面应用", + "loggingInToDesktopAppViaWeb": "通过网页界面登录桌面应用", + "loggingInToMobileApp": "登录移动应用", + "mobileApp": "移动应用", + "redirectingToApp": "重定向到应用..." }, "errors": { "notFound": "页面未找到", @@ -1517,9 +1550,12 @@ "fileColorCodingDesc": "按类型对文件进行颜色编码:文件夹(红色)、文件(蓝色)、符号链接(绿色)", "commandAutocomplete": "命令自动补全", "commandAutocompleteDesc": "启用基于命令历史记录的 Tab 键终端命令自动补全建议", + "defaultSnippetFoldersCollapsed": "默认折叠代码片段文件夹", + "defaultSnippetFoldersCollapsedDesc": "启用后,打开代码片段标签时所有文件夹将默认折叠", "currentPassword": "当前密码", "passwordChangedSuccess": "密码修改成功!请重新登录。", - "failedToChangePassword": "修改密码失败。请检查您当前的密码并重试。" + "failedToChangePassword": "修改密码失败。请检查您当前的密码并重试。", + "externalAndLocal": "Dual Auth" }, "user": { "failedToLoadVersionInfo": "加载版本信息失败" @@ -1549,7 +1585,7 @@ "redirectUrl": "https://your-provider.com/application/o/termix/", "tokenUrl": "https://your-provider.com/application/o/token/", "userIdField": "sub", - "usernameField": "name", + "usernameField": "名称", "scopes": "openid email profile", "userinfoUrl": "https://your-provider.com/application/o/userinfo/", "enterUsername": "输入用户名以设为管理员", @@ -1571,9 +1607,9 @@ "passwordRequired": "需要输入密码", "failedToDeleteAccount": "删除账户失败", "failedToMakeUserAdmin": "设为管理员失败", - "userIsNowAdmin": "用户 {{username}} 现在是管理员", - "removeAdminConfirm": "确定要移除 {{username}} 的管理员权限吗?", - "deleteUserConfirm": "确定要删除用户 {{username}} 吗?此操作无法撤销。", + "userIsNowAdmin": "用户 {{用户名}} 现在是管理员", + "removeAdminConfirm": "确定要移除 {{用户名}} 的管理员权限吗?", + "deleteUserConfirm": "确定要删除用户 {{用户名}} 吗?此操作无法撤销。", "deleteAccount": "删除账户", "closeDeleteAccount": "关闭删除账户", "deleteAccountWarning": "此操作无法撤销。这将永久删除您的账户和所有相关数据。", @@ -1620,7 +1656,76 @@ "failedToStartOidcLogin": "启动 OIDC 登录失败", "failedToGetUserInfoAfterOidc": "OIDC 登录后获取用户信息失败", "loginWithExternalProvider": "使用外部提供者登录", - "failedToStartTotpSetup": "启动 TOTP 设置失败" + "failedToStartTotpSetup": "启动 TOTP 设置失败", + "addHost": "添加 主机", + "adding": "添加ing...", + "authentication": "认证", + "cannotDeleteAccount": "Can没有t 删除 Account", + "clickToSelectFile": "Click to 选择 a 文件", + "clientId": "Client ID", + "clientSecret": "Client Secret", + "closeDeleteAccount": "关闭 删除 Account", + "configureExternalProvider": "Configure external identity provider for", + "confirmPassword": "Confirm 密码", + "connected": "已连接", + "createNewFile": "创建 New 文件", + "createNewFolder": "创建 New 文件夹", + "defaultPath": "Default Path", + "deleteAccount": "删除 Account", + "deleteItem": "删除 Item", + "deleting": "删除中...", + "disconnected": "已断开", + "editHost": "编辑 主机", + "enableFileManager": "启用 文件 Manager", + "enableTerminal": "启用 终端", + "enableTunnel": "启用 隧道", + "endpointHostNotFound": "Endpoint host 未找到", + "external": "External", + "failedToCompletePasswordReset": "无法 完成 密码 reset", + "failedToDisableTotp": "无法 禁用 TOTP", + "failedToGenerateBackupCodes": "无法 generate 返回up codes", + "failedToInitiatePasswordReset": "无法 initiate 密码 reset", + "failedToMakeUserAdmin": "无法 make 用户 管理员", + "failedToUpdateOidcConfig": "无法 更新 OIDC 配置", + "failedToVerifyResetCode": "无法 verify reset code", + "invalidTotpCode": "Invalid TOTP code", + "invalidVerificationCode": "Invalid verification code", + "key": "密钥", + "keyPassword": "密钥 密码", + "keyType": "密钥 Type", + "keyTypeRequired": "密钥 Type 是必需的 when using 密钥 认证", + "loading": "加载中...", + "local": "Local", + "login": "Login", + "loginWithExternal": "Login with External Provider", + "makeAdmin": "Make 管理员", + "maxRetries": "Max Retries", + "newFile": "New 文件", + "newFolder": "New 文件夹", + "password": "密码", + "passwordRequired": "密码 是必需的 when using 密码 认证", + "refresh": "刷新", + "renameItem": "Re名称 Item", + "resetPassword": "Reset 密码", + "retryingConnection": "重试ing 连接", + "saveConfiguration": "保存 配置", + "saving": "保存中...", + "sendResetCode": "Send Reset Code", + "signUp": "Sign Up", + "sshHosts": "SSH 主机s", + "sshKeyRequired": "SSH Private 密钥 是必需的 when using 密钥 认证", + "sshPrivateKey": "SSH Private 密钥", + "tunnelConnections": "隧道 连接s", + "unknown": "Unk没有wn", + "unknownError": "Unk没有wn 错误", + "updateHost": "更新 主机", + "updateKey": "更新 密钥", + "upload": "上传", + "user": "用户", + "verifyAndEnable": "Verify and 启用", + "verifyCode": "Verify Code", + "waitingForRetry": "Waiting for 重试", + "warning": "警告" }, "mobile": { "selectHostToStart": "选择一个主机以开始您的终端会话", @@ -1663,6 +1768,172 @@ "ram": "内存", "notAvailable": "不可用" }, + "rbac": { + "shareHost": "分享主机", + "shareHostTitle": "分享主机访问权限", + "shareHostDescription": "授予临时或永久访问此主机的权限", + "targetUser": "目标用户", + "selectUser": "选择要分享的用户", + "duration": "时长", + "durationHours": "时长(小时)", + "neverExpires": "永不过期", + "permissionLevel": "权限级别", + "permissionLevels": { + "readonly": "只读", + "readonlyDesc": "仅可查看,无法输入命令", + "restricted": "受限", + "restrictedDesc": "阻止危险命令(passwd、rm -rf等)", + "monitored": "监控", + "monitoredDesc": "记录所有命令但不阻止(推荐)", + "full": "完全访问", + "fullDesc": "无任何限制(不推荐)" + }, + "blockedCommands": "阻止的命令", + "blockedCommandsPlaceholder": "输入要阻止的命令,如:passwd, rm, dd", + "maxSessionDuration": "最大会话时长(分钟)", + "createTempUser": "创建临时用户", + "createTempUserDesc": "在服务器上创建受限用户而不是共享您的凭据。需要sudo权限。最安全的选项。", + "expiresAt": "过期时间", + "expiresIn": "{{hours}}小时后过期", + "expired": "已过期", + "grantedBy": "授予者", + "accessLevel": "访问级别", + "lastAccessed": "最后访问", + "accessCount": "访问次数", + "revokeAccess": "撤销访问", + "confirmRevokeAccess": "确定要撤销{{username}}的访问权限吗?", + "hostSharedSuccessfully": "已成功与{{username}}分享主机", + "hostAccessUpdated": "主机访问已更新", + "failedToShareHost": "分享主机失败", + "accessRevokedSuccessfully": "访问权限已成功撤销", + "failedToRevokeAccess": "撤销访问失败", + "shared": "共享", + "sharedHosts": "共享主机", + "sharedWithMe": "与我共享", + "noSharedHosts": "没有与您共享的主机", + "owner": "所有者", + "viewAccessList": "查看访问列表", + "accessList": "访问列表", + "noAccessGranted": "此主机尚未授予任何访问权限", + "noAccessGrantedMessage": "还没有用户被授予此主机的访问权限", + "manageAccessFor": "管理访问权限", + "totalAccessRecords": "{{count}} 条访问记录", + "neverAccessed": "从未访问", + "timesAccessed": "{{count}} 次", + "daysRemaining": "{{days}} 天", + "hoursRemaining": "{{hours}} 小时", + "expired": "已过期", + "failedToFetchAccessList": "获取访问列表失败", + "currentAccess": "当前访问", + "securityWarning": "安全警告", + "securityWarningMessage": "分享凭据会让用户完全访问服务器并执行任何操作,包括更改密码和删除文件。仅与受信任的用户共享。", + "tempUserRecommended": "我们建议启用'创建临时用户'以获得更好的安全性。", + "roleManagement": "角色管理", + "manageRoles": "管理角色", + "manageRolesFor": "管理 {{username}} 的角色", + "assignRole": "分配角色", + "removeRole": "移除角色", + "userRoles": "用户角色", + "permissions": "权限", + "systemRole": "系统角色", + "customRole": "自定义角色", + "roleAssignedSuccessfully": "已成功为{{username}}分配角色", + "failedToAssignRole": "分配角色失败", + "roleRemovedSuccessfully": "已成功从{{username}}移除角色", + "failedToRemoveRole": "移除角色失败", + "cannotRemoveSystemRole": "无法移除系统角色", + "cannotShareWithSelf": "不能与自己共享主机", + "noCustomRolesToAssign": "没有可用的自定义角色。系统角色已自动分配。", + "credentialSharingWarning": "不支持共享使用凭据认证的主机", + "credentialSharingWarningDescription": "此主机使用凭据认证。由于凭据是按用户加密的无法共享,共享用户将无法连接。请为计划共享的主机使用密码或密钥认证。", + "auditLogs": "审计日志", + "viewAuditLogs": "查看审计日志", + "action": "操作", + "resourceType": "资源类型", + "resourceName": "资源名称", + "timestamp": "时间戳", + "ipAddress": "IP地址", + "userAgent": "用户代理", + "success": "成功", + "failed": "失败", + "details": "详情", + "noAuditLogs": "无可用审计日志", + "sessionRecordings": "会话录制", + "viewRecording": "查看录制", + "downloadRecording": "下载录制", + "dangerousCommand": "检测到危险命令", + "commandBlocked": "命令已阻止", + "terminateSession": "终止会话", + "sessionTerminated": "会话已被主机所有者终止", + "sharedAccessExpired": "您对此主机的共享访问权限已过期", + "sharedAccessExpiresIn": "共享访问将在{{hours}}小时后过期", + "roles": { + "label": "角色", + "admin": "管理员", + "user": "用户" + }, + "createRole": "创建角色", + "editRole": "编辑角色", + "roleName": "角色名称", + "displayName": "显示名称", + "description": "描述", + "assignRoles": "分配角色", + "userRoleAssignment": "用户角色分配", + "selectUserPlaceholder": "选择用户", + "searchUsers": "搜索用户...", + "noUserFound": "未找到用户", + "currentRoles": "当前角色", + "noRolesAssigned": "未分配角色", + "assignNewRole": "分配新角色", + "selectRolePlaceholder": "选择角色", + "searchRoles": "搜索角色...", + "noRoleFound": "未找到角色", + "assign": "分配", + "roleCreatedSuccessfully": "角色创建成功", + "roleUpdatedSuccessfully": "角色更新成功", + "roleDeletedSuccessfully": "角色删除成功", + "failedToLoadRoles": "加载角色失败", + "failedToSaveRole": "保存角色失败", + "failedToDeleteRole": "删除角色失败", + "roleDisplayNameRequired": "角色显示名称是必需的", + "roleNameRequired": "角色名称是必需的", + "roleNameHint": "仅使用小写字母、数字、下划线和连字符", + "displayNamePlaceholder": "开发者", + "descriptionPlaceholder": "软件开发人员和工程师", + "confirmDeleteRole": "删除角色", + "confirmDeleteRoleDescription": "确定要删除角色'{{name}}'吗?此操作无法撤销。", + "confirmRemoveRole": "移除角色", + "confirmRemoveRoleDescription": "确定要从用户中移除此角色吗?", + "editRoleDescription": "更新角色信息", + "createRoleDescription": "创建新的自定义角色以分组用户", + "assignRolesDescription": "管理用户的角色分配", + "noRoles": "未找到角色", + "selectRole": "选择角色", + "type": "类型", + "user": "用户", + "role": "角色", + "saveHostFirst": "请先保存主机", + "saveHostFirstDescription": "请先保存主机后再配置分享设置。", + "shareWithUser": "与用户分享", + "shareWithRole": "与角色分享", + "share": "分享", + "target": "目标", + "expires": "过期时间", + "never": "永不", + "noAccessRecords": "未找到访问记录", + "sharedSuccessfully": "分享成功", + "failedToShare": "分享失败", + "confirmRevokeAccessDescription": "确定要撤销此访问权限吗?", + "hours": "小时", + "sharing": "分享", + "selectUserAndRole": "请选择用户和角色", + "view": "仅查看", + "viewDesc": "可以查看和连接主机,但仅限只读模式", + "use": "使用", + "useDesc": "可以正常使用主机,但不能修改主机配置", + "manage": "管理", + "manageDesc": "完全控制,包括修改主机配置和分享设置" + }, "commandPalette": { "searchPlaceholder": "搜索主机或快速操作...", "recentActivity": "最近活动", diff --git a/src/types/index.ts b/src/types/index.ts index 2b8aa1cd..7eae315a 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -40,6 +40,7 @@ export interface SSHHost { enableTerminal: boolean; enableTunnel: boolean; enableFileManager: boolean; + enableDocker: boolean; defaultPath: string; tunnelConnections: TunnelConnection[]; jumpHosts?: JumpHost[]; @@ -56,6 +57,11 @@ export interface SSHHost { createdAt: string; updatedAt: string; + + // Shared access metadata + isShared?: boolean; + permissionLevel?: "view" | "manage"; + sharedExpiresAt?: string; } export interface JumpHostData { @@ -93,6 +99,7 @@ export interface SSHHostData { enableTerminal?: boolean; enableTunnel?: boolean; enableFileManager?: boolean; + enableDocker?: boolean; defaultPath?: string; forceKeyboardInteractive?: boolean; tunnelConnections?: TunnelConnection[]; @@ -143,6 +150,28 @@ export interface Credential { updatedAt: string; } +export interface CredentialBackend { + id: number; + userId: string; + name: string; + description: string | null; + folder: string | null; + tags: string; + authType: "password" | "key"; + username: string; + password: string | null; + key: string; + private_key?: string; + public_key?: string; + key_password: string | null; + keyType?: string; + detectedKeyType: string; + usageCount: number; + lastUsed: string | null; + createdAt: string; + updatedAt: string; +} + export interface CredentialData { name: string; description?: string; @@ -339,6 +368,7 @@ export interface TerminalConfig { startupSnippetId: number | null; autoMosh: boolean; moshCommand: string; + sudoPasswordAutoFill: boolean; } // ============================================================================ @@ -354,7 +384,8 @@ export interface TabContextTab { | "server" | "admin" | "file_manager" - | "user_profile"; + | "user_profile" + | "docker"; title: string; hostConfig?: SSHHost; terminalRef?: any; @@ -680,3 +711,55 @@ export interface RestoreRequestBody { backupPath: string; targetPath?: string; } + +// ============================================================================ +// DOCKER TYPES +// ============================================================================ + +export interface DockerContainer { + id: string; + name: string; + image: string; + status: string; + state: + | "created" + | "running" + | "paused" + | "restarting" + | "removing" + | "exited" + | "dead"; + ports: string; + created: string; + command?: string; + labels?: Record; + networks?: string[]; + mounts?: string[]; +} + +export interface DockerStats { + cpu: string; + memoryUsed: string; + memoryLimit: string; + memoryPercent: string; + netInput: string; + netOutput: string; + blockRead: string; + blockWrite: string; + pids?: string; +} + +export interface DockerLogOptions { + tail?: number; + timestamps?: boolean; + since?: string; + until?: string; + follow?: boolean; +} + +export interface DockerValidation { + available: boolean; + version?: string; + error?: string; + code?: string; +} diff --git a/src/ui/desktop/DesktopApp.tsx b/src/ui/desktop/DesktopApp.tsx index fb015997..99cfa6d9 100644 --- a/src/ui/desktop/DesktopApp.tsx +++ b/src/ui/desktop/DesktopApp.tsx @@ -155,7 +155,9 @@ function AppContent() { const showTerminalView = currentTabData?.type === "terminal" || currentTabData?.type === "server" || - currentTabData?.type === "file_manager"; + currentTabData?.type === "file_manager" || + currentTabData?.type === "tunnel" || + currentTabData?.type === "docker"; const showHome = currentTabData?.type === "home"; const showSshManager = currentTabData?.type === "ssh_manager"; const showAdmin = currentTabData?.type === "admin"; diff --git a/src/ui/desktop/admin/AdminSettings.tsx b/src/ui/desktop/admin/AdminSettings.tsx index 9febd08a..e271db3d 100644 --- a/src/ui/desktop/admin/AdminSettings.tsx +++ b/src/ui/desktop/admin/AdminSettings.tsx @@ -42,6 +42,7 @@ import { Smartphone, Globe, Clock, + UserCog, } from "lucide-react"; import { toast } from "sonner"; import { useTranslation } from "react-i18next"; @@ -66,7 +67,14 @@ import { revokeAllUserSessions, linkOIDCToPasswordAccount, unlinkOIDCFromPasswordAccount, + getUserRoles, + assignRoleToUser, + removeRoleFromUser, + getRoles, + type UserRole, + type Role, } from "@/ui/main-axios.ts"; +import { RoleManagement } from "./RoleManagement.tsx"; interface AdminSettingsProps { isTopbarOpen?: boolean; @@ -119,6 +127,16 @@ export function AdminSettings({ null, ); + // Role management states + const [rolesDialogOpen, setRolesDialogOpen] = React.useState(false); + const [selectedUser, setSelectedUser] = React.useState<{ + id: string; + username: string; + } | null>(null); + const [userRoles, setUserRoles] = React.useState([]); + const [availableRoles, setAvailableRoles] = React.useState([]); + const [rolesLoading, setRolesLoading] = React.useState(false); + const [securityInitialized, setSecurityInitialized] = React.useState(true); const [currentUser, setCurrentUser] = React.useState<{ id: string; @@ -267,6 +285,65 @@ export function AdminSettings({ } }; + // Role management functions + const handleOpenRolesDialog = async (user: { + id: string; + username: string; + }) => { + setSelectedUser(user); + setRolesDialogOpen(true); + setRolesLoading(true); + + try { + // Load user's current roles + const rolesResponse = await getUserRoles(user.id); + setUserRoles(rolesResponse.roles || []); + + // Load all available roles + const allRolesResponse = await getRoles(); + setAvailableRoles(allRolesResponse.roles || []); + } catch (error) { + console.error("Failed to load roles:", error); + toast.error(t("rbac.failedToLoadRoles")); + } finally { + setRolesLoading(false); + } + }; + + const handleAssignRole = async (roleId: number) => { + if (!selectedUser) return; + + try { + await assignRoleToUser(selectedUser.id, roleId); + toast.success( + t("rbac.roleAssignedSuccessfully", { username: selectedUser.username }), + ); + + // Reload user roles + const rolesResponse = await getUserRoles(selectedUser.id); + setUserRoles(rolesResponse.roles || []); + } catch (error) { + toast.error(t("rbac.failedToAssignRole")); + } + }; + + const handleRemoveRole = async (roleId: number) => { + if (!selectedUser) return; + + try { + await removeRoleFromUser(selectedUser.id, roleId); + toast.success( + t("rbac.roleRemovedSuccessfully", { username: selectedUser.username }), + ); + + // Reload user roles + const rolesResponse = await getUserRoles(selectedUser.id); + setUserRoles(rolesResponse.roles || []); + } catch (error) { + toast.error(t("rbac.failedToRemoveRole")); + } + }; + const handleToggleRegistration = async (checked: boolean) => { setRegLoading(true); try { @@ -771,6 +848,10 @@ export function AdminSettings({ {t("admin.adminManagement")} + + + {t("rbac.roles.label")} + {t("admin.databaseSecurity")} @@ -1081,88 +1162,92 @@ export function AdminSettings({ {t("admin.loadingUsers")}
) : ( -
- - - - - {t("admin.username")} - - - {t("admin.type")} - - - {t("admin.actions")} - - - - - {users.map((user) => ( - - - {user.username} - {user.is_admin && ( - - {t("admin.adminBadge")} - - )} - - - {user.is_oidc && user.password_hash - ? "Dual Auth" - : user.is_oidc - ? t("admin.external") - : t("admin.local")} - - -
- {user.is_oidc && !user.password_hash && ( - - )} - {user.is_oidc && user.password_hash && ( - - )} +
+ + + {t("admin.username")} + {t("admin.type")} + {t("admin.actions")} + + + + {users.map((user) => ( + + + {user.username} + {user.is_admin && ( + + {t("admin.adminBadge")} + + )} + + + {user.is_oidc && user.password_hash + ? "Dual Auth" + : user.is_oidc + ? t("admin.external") + : t("admin.local")} + + +
+ {user.is_oidc && !user.password_hash && ( -
-
-
- ))} -
-
-
+ )} + {user.is_oidc && user.password_hash && ( + + )} + + +
+ + + ))} + + )} @@ -1189,115 +1274,107 @@ export function AdminSettings({ No active sessions found. ) : ( -
-
- - - - Device - User - Created - Last Active - Expires - - {t("admin.actions")} - +
+ + + Device + User + Created + Last Active + Expires + {t("admin.actions")} + + + + {sessions.map((session) => { + const DeviceIcon = + session.deviceType === "desktop" + ? Monitor + : session.deviceType === "mobile" + ? Smartphone + : Globe; + + const createdDate = new Date(session.createdAt); + const lastActiveDate = new Date(session.lastActiveAt); + const expiresDate = new Date(session.expiresAt); + + const formatDate = (date: Date) => + date.toLocaleDateString() + + " " + + date.toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }); + + return ( + + +
+ +
+ + {session.deviceInfo} + + {session.isRevoked && ( + + Revoked + + )} +
+
+
+ + {session.username || session.userId} + + + {formatDate(createdDate)} + + + {formatDate(lastActiveDate)} + + + {formatDate(expiresDate)} + + +
+ + {session.username && ( + + )} +
+
- - - {sessions.map((session) => { - const DeviceIcon = - session.deviceType === "desktop" - ? Monitor - : session.deviceType === "mobile" - ? Smartphone - : Globe; - - const createdDate = new Date(session.createdAt); - const lastActiveDate = new Date( - session.lastActiveAt, - ); - const expiresDate = new Date(session.expiresAt); - - const formatDate = (date: Date) => - date.toLocaleDateString() + - " " + - date.toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - }); - - return ( - - -
- -
- - {session.deviceInfo} - - {session.isRevoked && ( - - Revoked - - )} -
-
-
- - {session.username || session.userId} - - - {formatDate(createdDate)} - - - {formatDate(lastActiveDate)} - - - {formatDate(expiresDate)} - - -
- - {session.username && ( - - )} -
-
-
- ); - })} -
-
-
-
+ ); + })} + + )} @@ -1345,59 +1422,55 @@ export function AdminSettings({

{t("admin.currentAdmins")}

-
- - - - - {t("admin.username")} - - - {t("admin.type")} - - - {t("admin.actions")} - - - - - {users - .filter((u) => u.is_admin) - .map((admin) => ( - - - {admin.username} - - {t("admin.adminBadge")} - - - - {admin.is_oidc - ? t("admin.external") - : t("admin.local")} - - - - - - ))} - -
-
+ + + + {t("admin.username")} + {t("admin.type")} + {t("admin.actions")} + + + + {users + .filter((u) => u.is_admin) + .map((admin) => ( + + + {admin.username} + + {t("admin.adminBadge")} + + + + {admin.is_oidc + ? t("admin.external") + : t("admin.local")} + + + + + + ))} + +
+ + + +
@@ -1613,6 +1686,114 @@ export function AdminSettings({ )} + + {/* Role Management Dialog */} + + + + {t("rbac.manageRoles")} + + {t("rbac.manageRolesFor", { + username: selectedUser?.username || "", + })} + + + + {rolesLoading ? ( +
+ {t("common.loading")} +
+ ) : ( +
+ {/* Current Roles */} +
+ + {userRoles.length === 0 ? ( +

+ {t("rbac.noRolesAssigned")} +

+ ) : ( +
+ {userRoles.map((userRole) => ( +
+
+

+ {t(userRole.roleDisplayName)} +

+

+ {userRole.roleName} +

+
+ {userRole.isSystem ? ( + + {t("rbac.systemRole")} + + ) : ( + + )} +
+ ))} +
+ )} +
+ + {/* Assign New Role */} +
+ +
+ {availableRoles + .filter( + (role) => + !role.isSystem && + !userRoles.some((ur) => ur.roleId === role.id), + ) + .map((role) => ( + + ))} + {availableRoles.filter( + (role) => + !role.isSystem && + !userRoles.some((ur) => ur.roleId === role.id), + ).length === 0 && ( +

+ {t("rbac.noCustomRolesToAssign")} +

+ )} +
+
+
+ )} + + + + +
+
); } diff --git a/src/ui/desktop/admin/RoleManagement.tsx b/src/ui/desktop/admin/RoleManagement.tsx new file mode 100644 index 00000000..9bce9002 --- /dev/null +++ b/src/ui/desktop/admin/RoleManagement.tsx @@ -0,0 +1,650 @@ +import React from "react"; +import { Button } from "@/components/ui/button.tsx"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog.tsx"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table.tsx"; +import { Input } from "@/components/ui/input.tsx"; +import { Label } from "@/components/ui/label.tsx"; +import { Textarea } from "@/components/ui/textarea.tsx"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select.tsx"; +import { Badge } from "@/components/ui/badge.tsx"; +import { + Shield, + Plus, + Edit, + Trash2, + Users, + Check, + ChevronsUpDown, +} from "lucide-react"; +import { toast } from "sonner"; +import { useTranslation } from "react-i18next"; +import { useConfirmation } from "@/hooks/use-confirmation.ts"; +import { + getRoles, + createRole, + updateRole, + deleteRole, + getUserList, + getUserRoles, + assignRoleToUser, + removeRoleFromUser, + type Role, + type UserRole, +} from "@/ui/main-axios.ts"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, +} from "@/components/ui/command.tsx"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover.tsx"; +import { cn } from "@/lib/utils"; + +interface User { + id: string; + username: string; + is_admin: boolean; +} + +export function RoleManagement(): React.ReactElement { + const { t } = useTranslation(); + const { confirmWithToast } = useConfirmation(); + + const [roles, setRoles] = React.useState([]); + const [users, setUsers] = React.useState([]); + const [loading, setLoading] = React.useState(false); + + // Create/Edit Role Dialog + const [roleDialogOpen, setRoleDialogOpen] = React.useState(false); + const [editingRole, setEditingRole] = React.useState(null); + const [roleName, setRoleName] = React.useState(""); + const [roleDisplayName, setRoleDisplayName] = React.useState(""); + const [roleDescription, setRoleDescription] = React.useState(""); + + // Assign Role Dialog + const [assignDialogOpen, setAssignDialogOpen] = React.useState(false); + const [selectedUserId, setSelectedUserId] = React.useState(""); + const [selectedRoleId, setSelectedRoleId] = React.useState( + null, + ); + const [userRoles, setUserRoles] = React.useState([]); + + // Combobox states + const [userComboOpen, setUserComboOpen] = React.useState(false); + const [roleComboOpen, setRoleComboOpen] = React.useState(false); + + // Load roles + const loadRoles = React.useCallback(async () => { + setLoading(true); + try { + const response = await getRoles(); + setRoles(response.roles || []); + } catch (error) { + toast.error(t("rbac.failedToLoadRoles")); + console.error("Failed to load roles:", error); + setRoles([]); + } finally { + setLoading(false); + } + }, [t]); + + // Load users + const loadUsers = React.useCallback(async () => { + try { + const response = await getUserList(); + // Map UserInfo to User format + const mappedUsers = (response.users || []).map((user) => ({ + id: user.id, + username: user.username, + is_admin: user.is_admin, + })); + setUsers(mappedUsers); + } catch (error) { + console.error("Failed to load users:", error); + setUsers([]); + } + }, []); + + React.useEffect(() => { + loadRoles(); + loadUsers(); + }, [loadRoles, loadUsers]); + + // Create role + const handleCreateRole = () => { + setEditingRole(null); + setRoleName(""); + setRoleDisplayName(""); + setRoleDescription(""); + setRoleDialogOpen(true); + }; + + // Edit role + const handleEditRole = (role: Role) => { + setEditingRole(role); + setRoleName(role.name); + setRoleDisplayName(role.displayName); + setRoleDescription(role.description || ""); + setRoleDialogOpen(true); + }; + + // Save role + const handleSaveRole = async () => { + if (!roleDisplayName.trim()) { + toast.error(t("rbac.roleDisplayNameRequired")); + return; + } + + if (!editingRole && !roleName.trim()) { + toast.error(t("rbac.roleNameRequired")); + return; + } + + try { + if (editingRole) { + // Update existing role + await updateRole(editingRole.id, { + displayName: roleDisplayName, + description: roleDescription || null, + }); + toast.success(t("rbac.roleUpdatedSuccessfully")); + } else { + // Create new role + await createRole({ + name: roleName, + displayName: roleDisplayName, + description: roleDescription || null, + }); + toast.success(t("rbac.roleCreatedSuccessfully")); + } + + setRoleDialogOpen(false); + loadRoles(); + } catch (error) { + toast.error(t("rbac.failedToSaveRole")); + } + }; + + // Delete role + const handleDeleteRole = async (role: Role) => { + const confirmed = await confirmWithToast({ + title: t("rbac.confirmDeleteRole"), + description: t("rbac.confirmDeleteRoleDescription", { + name: role.displayName, + }), + confirmText: t("common.delete"), + cancelText: t("common.cancel"), + }); + + if (!confirmed) return; + + try { + await deleteRole(role.id); + toast.success(t("rbac.roleDeletedSuccessfully")); + loadRoles(); + } catch (error) { + toast.error(t("rbac.failedToDeleteRole")); + } + }; + + // Open assign dialog + const handleOpenAssignDialog = async () => { + setSelectedUserId(""); + setSelectedRoleId(null); + setUserRoles([]); + setAssignDialogOpen(true); + }; + + // Load user roles when user is selected + const handleUserSelect = async (userId: string) => { + setSelectedUserId(userId); + setUserRoles([]); + + if (!userId) return; + + try { + const response = await getUserRoles(userId); + setUserRoles(response.roles || []); + } catch (error) { + console.error("Failed to load user roles:", error); + setUserRoles([]); + } + }; + + // Assign role to user + const handleAssignRole = async () => { + if (!selectedUserId || !selectedRoleId) { + toast.error(t("rbac.selectUserAndRole")); + return; + } + + try { + await assignRoleToUser(selectedUserId, selectedRoleId); + const selectedUser = users.find((u) => u.id === selectedUserId); + toast.success( + t("rbac.roleAssignedSuccessfully", { + username: selectedUser?.username || selectedUserId, + }), + ); + setSelectedRoleId(null); + handleUserSelect(selectedUserId); + } catch (error) { + toast.error(t("rbac.failedToAssignRole")); + } + }; + + // Remove role from user + const handleRemoveUserRole = async (roleId: number) => { + if (!selectedUserId) return; + + try { + await removeRoleFromUser(selectedUserId, roleId); + const selectedUser = users.find((u) => u.id === selectedUserId); + toast.success( + t("rbac.roleRemovedSuccessfully", { + username: selectedUser?.username || selectedUserId, + }), + ); + handleUserSelect(selectedUserId); + } catch (error) { + toast.error(t("rbac.failedToRemoveRole")); + } + }; + + return ( +
+ {/* Roles Section */} +
+
+

+ + {t("rbac.roleManagement")} +

+ +
+ + + + + {t("rbac.roleName")} + {t("rbac.displayName")} + {t("rbac.description")} + {t("rbac.type")} + + {t("common.actions")} + + + + + {loading ? ( + + + {t("common.loading")} + + + ) : roles.length === 0 ? ( + + + {t("rbac.noRoles")} + + + ) : ( + roles.map((role) => ( + + {role.name} + {t(role.displayName)} + + {role.description || "-"} + + + {role.isSystem ? ( + {t("rbac.systemRole")} + ) : ( + {t("rbac.customRole")} + )} + + +
+ {!role.isSystem && ( + <> + + + + )} +
+
+
+ )) + )} +
+
+
+ + {/* User-Role Assignment Section */} +
+
+

+ + {t("rbac.userRoleAssignment")} +

+ +
+
+ + {/* Create/Edit Role Dialog */} + + + + + {editingRole ? t("rbac.editRole") : t("rbac.createRole")} + + + {editingRole + ? t("rbac.editRoleDescription") + : t("rbac.createRoleDescription")} + + + +
+ {!editingRole && ( +
+ + setRoleName(e.target.value.toLowerCase())} + placeholder="developer" + disabled={!!editingRole} + /> +

+ {t("rbac.roleNameHint")} +

+
+ )} + +
+ + setRoleDisplayName(e.target.value)} + placeholder={t("rbac.displayNamePlaceholder")} + /> +
+ +
+ +